class MODL::Parser::ClassProcessor

This class handles the conversion of objects that refer to classes into instances of those classes. It works recursively since class usage can be nested.

Constants

MAX_RECURSION_DEPTH

How deep can the class structure be?

Public Class Methods

process(global, obj) click to toggle source

global is a GlobalParseContext and obj is the extracted Array or Hash from MODL::Parser::Parsed.extract_json

# File lib/modl/parser/class_processor.rb, line 33
def self.process(global, obj)
  # Process each object in the array or just process the object if its a hash.
  # Any other object is ignored.
  raise StandardError, 'parameter "global" should be a GlobalParseContext' unless global.is_a?(GlobalParseContext)

  if obj.is_a? Array
    root_class = global.classs 'root'
    unless root_class.nil?
      raise StandardError, 'root class has no *assign statement.' if root_class.assign.nil?
      raise StandardError, 'root class *assign statement should be of the form "*assign=[[class_name]]".' if root_class.assign.length > 1 || root_class.assign[0].length > 1
      root_class_assign = root_class.assign[0][0]

      array_class = global.classs root_class_assign
      classes = array_class.keylist_of_length obj.length
      new_obj = []

      # The top level array can be an array of arrays or an array of hashes, so we need to handle both.
      obj.each_index do |i|
        item = obj[i]
        if item.is_a? Array
          new_obj << {classes[i] => item}
        elsif item.is_a? Hash
          new_obj << item
        end
      end

      obj = new_obj
      process_recursive global, obj

      result = []
      obj.each_index do |i|
        result << obj[i][global.classs(classes[i]).name_or_id]
      end

      return result
    end
  end
  process_recursive global, obj
  return obj
end

Private Class Methods

all_assignment_keys_are_classes?(clazz, global) click to toggle source
# File lib/modl/parser/class_processor.rb, line 252
def self.all_assignment_keys_are_classes? clazz, global
  lists = key_lists global, clazz
  result = true
  lists.each do |list|
    list.each do |item|
      item = Sutil.head(item) if item.end_with? '*'
      global_class = global.classs(item)
      result &= (!global_class.nil? && has_assign_statement?(global_class, global))
    end
  end
  result
end
copy_from_superclasses(clazz, global, new_value, v) click to toggle source

Bring down values from the superclass hierarchy

# File lib/modl/parser/class_processor.rb, line 266
def self.copy_from_superclasses(clazz, global, new_value, v)
  if v.is_a? Hash
    new_value = v.merge(new_value)
  end

  clazz.merge_content(new_value)

  depth = 0
  loop do
    clazz = global.classs(clazz.superclass)
    break if clazz.nil? || depth > MAX_RECURSION_DEPTH

    clazz.merge_content(new_value)
    depth += 1
  end
  new_value
end
copy_keys_to_new_value(new_value, val) click to toggle source

Transfer the keys from val to the new_value object.

# File lib/modl/parser/class_processor.rb, line 285
def self.copy_keys_to_new_value(new_value, val)
  val.each do |value|
    next unless value&.is_a?(Hash)

    new_value.merge!(value)
  end
end
has_assign_statement?(clazz, global, depth = 0) click to toggle source
# File lib/modl/parser/class_processor.rb, line 119
def self.has_assign_statement?(clazz, global, depth = 0)
  # Check for *assign statements
  return if depth > MAX_RECURSION_DEPTH
  return nil? if clazz.nil?
  return true unless clazz.assign.nil?

  superclass = clazz.superclass
  c = global.classs(superclass)
  return has_assign_statement?(c, global, depth + 1) if c

  false
end
has_inherited_pairs?(clazz, global, depth = 0) click to toggle source
# File lib/modl/parser/class_processor.rb, line 132
def self.has_inherited_pairs?(clazz, global, depth = 0)
  # Check for *assign statements
  return if depth > MAX_RECURSION_DEPTH
  return nil? if clazz.nil?
  return true unless clazz.content.empty?

  superclass = clazz.superclass
  c = global.classs(superclass)
  return has_inherited_pairs?(c, global, depth + 1) if c

  false
end
key_list(global, clazz, len) click to toggle source

Find a *assign key list of a specific length

# File lib/modl/parser/class_processor.rb, line 381
def self.key_list(global, clazz, len)
  return [] if clazz.nil?
  list = clazz.keylist_of_length(len)
  return list if !list.nil? && list.length > 0
  superclass = global.classs(clazz.superclass)
  key_list(global, superclass, len)
end
key_lists(global, clazz) click to toggle source

Find all *assign key lists

# File lib/modl/parser/class_processor.rb, line 390
def self.key_lists(global, clazz)
  return [] if clazz.nil?
  list = clazz.assign
  superclass = global.classs(clazz.superclass)
  return list + key_lists(global, superclass)
end
process_class(global, k, v) click to toggle source
# File lib/modl/parser/class_processor.rb, line 145
def self.process_class(global, k, v)
  clazz = global.classs(k)
  if k != clazz.id && !(v.is_a?(Hash) || v.is_a?(Array))
    return [k, v]
  end

  if k == clazz.name && !v.is_a?(Array)
    new_value = transform_to_class(clazz, global, [v], v.is_a?(Hash))
    if new_value.is_a?(Array) && new_value.length == 1
      return [clazz.name_or_id, new_value[0]]
    else
      return [clazz.name_or_id, new_value]
    end
  end

  new_value = transform_to_class(clazz, global, v, v.is_a?(Hash))

  if v.is_a?(Array)
    new_value = v if new_value.empty?
  elsif v.is_a?(String)
    # Safe to ignore
  else
    # Safe to ignore
  end

  # Check the top class and do some type-specific processing
  tc = top_class(clazz, global)
  if tc.nil?
    # There is no defined top class so we need to infer it base on the value
    # and the rules defined here: https://github.com/MODLanguage/grammar/wiki/Class-Supertype-Processing
    #
    if has_assign_statement?(clazz, global)
      if all_assignment_keys_are_classes?(clazz, global)
        tc = 'arr'
      else
        tc = 'map'
      end
    else
      if has_inherited_pairs?(clazz, global)
        tc = 'map'
      else
        if v.is_a? String
          tc = 'str'
        elsif v.is_a? Numeric
          tc = 'num'
        elsif (v.is_a? TrueClass) || (v.is_a? FalseClass)
          tc = 'bool'
        elsif v.nil?
          tc = 'null'
        elsif v.is_a? Array
          tc = 'arr'
        elsif v.is_a? Hash
          tc = 'map'
        end
      end
    end
  end
  if tc == 'str'
    raise InterpreterError, "Interpreter Error: Cannot convert null value to string." if v.nil?
    new_value = v.to_s
  elsif tc == 'num'
    if (v.is_a? String) && (v.to_i.to_s == v.to_s)
      new_value = v.to_i
    elsif (v.is_a? String) && (v.to_f.to_s == v.to_s)
      new_value = v.to_f
    elsif v.is_a? TrueClass
      new_value = 1
    elsif v.is_a? FalseClass
      new_value = 0
    elsif v.is_a? Numeric
      new_value = v
    else
      raise InterpreterError, 'Superclass of "' + clazz.id + '" is num - cannot assign value "' + v.to_s + '"'
    end
  elsif tc == 'bool'
    new_value = v
  elsif tc == 'null'
    new_value = nil
  elsif tc == 'arr'
    if v.is_a? Array
      if new_value.is_a? Hash
        new_value = new_value.values
      end
    elsif v.is_a? Hash
      raise InterpreterError, 'Interpreter Error: Cannot convert map to array: ' + v.to_s
    else
      new_value = [v]
    end
  elsif tc == 'map'
    if new_value.is_a? Hash
      # Bring down values from the superclass hierarchy
      new_value = copy_from_superclasses(clazz, global, new_value, v)
    elsif v.is_a? Array
      raise InterpreterError, 'Interpreter Error: Cannot convert array to map: ' + v.to_s
    else
      new_value = {}
      new_value['value'] = v
      # Bring down values from the superclass hierarchy
      new_value = copy_from_superclasses(clazz, global, new_value, v)
    end
  elsif tc.nil? && (v.is_a? Hash)
    new_value = v
  end

  [clazz.name_or_id, new_value]
end
process_nested_classes(global, new_value) click to toggle source

If the new_value has nested class references then process those recursively as well.

# File lib/modl/parser/class_processor.rb, line 294
def self.process_nested_classes(global, new_value)
  if new_value.is_a?(Hash)

    new_value.keys.each do |nk|
      clazz = global.classs(nk)
      nv = new_value[nk]
      next unless clazz # skip it if it doesn't refer to a class

      if !nv.nil? && !nv.is_a?(String) && !nv.is_a?(Numeric)
        new_k, new_v = process_class global, nk, nv
      else
        new_k = clazz.name_or_id
        new_v = nv
      end

      # Replace the value for this key if we've changed anything.
      if new_value[new_k] != new_v
        replace_value(new_value, new_k, new_k, new_v)
      end
    end
  elsif new_value.is_a?(Array)
    new_value.each do |nk|
      process_nested_classes(global, nk)
    end
  end
end
process_obj(global, obj) click to toggle source

Process the contents of the supplied hash obj

# File lib/modl/parser/class_processor.rb, line 87
def self.process_obj(global, obj)
  obj.keys.each do |k|
    value = obj[k]
    # Does the key refer to a class that we have parsed or loaded?
    clazz = global.classs(k)
    if clazz
      # Yes so convert this value to an instance of that class
      new_k, new_v = process_class global, k, value
      # Replace the existing object with the new class instance and a new key
      # We need to keep the same key order, hence this method below
      replace_value(obj, k, new_k, new_v)
    else
      new_v = value
    end
    # Recurse into the value in case it has contents that also refer to classes.
    process_recursive global, new_v
  end
end
process_recursive(global, obj) click to toggle source
# File lib/modl/parser/class_processor.rb, line 76
def self.process_recursive(global, obj)
  if obj.is_a? Array
    obj.each do |o|
      process_obj global, o if o.is_a? Hash
    end
  elsif obj.is_a? Hash
    process_obj global, obj
  end
end
replace_value(obj, old_k, new_k, new_v) click to toggle source
# File lib/modl/parser/class_processor.rb, line 106
def self.replace_value(obj, old_k, new_k, new_v)
  tmp = obj.dup
  obj.clear
  tmp.keys.each do |tmpk|
    tmpv = tmp[tmpk]
    if tmpk == old_k
      obj[new_k] = new_v
    else
      obj[tmpk] = tmpv
    end
  end
end
top_class(clazz, global, depth = 0) click to toggle source

Get the top level class for the supplied class

# File lib/modl/parser/class_processor.rb, line 398
def self.top_class(clazz, global, depth = 0)
  # Check for self-referential classes that cause infinite recursion
  return if depth > MAX_RECURSION_DEPTH
  return nil? if clazz.nil?

  superclass = clazz.superclass
  c = global.classs(superclass)
  return top_class(c, global, depth + 1) if c

  superclass
end
transform_to_class(clazz, global, v, ignore_assign = false) click to toggle source

Process the *assign lists ('keylist' in this code) and any extra pairs defined by the class. The id, name, and superclass can be ignored here.

# File lib/modl/parser/class_processor.rb, line 323
def self.transform_to_class(clazz, global, v, ignore_assign = false)
  new_value = {} # the replacement for val after conversion to a class instance
  process_nested_classes(global, v)

  # Process the key list if we found one otherwise raise an error
  # Slightly different processing for hashes and arrays
  unless ignore_assign
    raise StandardError, 'cannot use "*assign" to populate a map: ' + clazz.id if has_assign_statement?(clazz, global) && clazz.superclass == 'map' && !v.is_a?(Array)
    if v.is_a? Array
      keys = key_list(global, clazz, v.length)
      if keys.empty?
        return v
      else
        lam = ->(i) { v[i] }
      end
    elsif !v.is_a?(Hash)
      keys = key_list(global, clazz, 1)
      lam = ->(i) { v }
      return v if keys.length.zero?
    else
      raise StandardError, 'cannot use "*assign" to populate a map: ' + clazz.id if has_assign_statement?(clazz, global)
    end
  end

  if keys.nil?
    if v.is_a?(Hash)
      new_value.merge! v
    elsif v.is_a?(Array) && v.length > 0
      v.each do |item|
        if item.is_a?(Hash)
          new_value.merge! item
        end
      end
    end
    new_value.keys do |nk|
      process_obj global, new_value[nk]
    end

    process_nested_classes(global, new_value)
    clazz.merge_content(new_value)
  elsif v.is_a? String
    new_value[keys[0]] = v
    new_value
  else
    keys.each_index do |i|
      tmp_value = {keys[i] => v[i]}
      process_obj global, tmp_value
      if !global.classs(keys[i]).nil? && !tmp_value[keys[i]].nil? && (tmp_value[keys[i]].is_a?(Hash) || tmp_value[keys[i]].is_a?(Array))
        new_value[i] = tmp_value[keys[i]]
      else
        new_value.merge! tmp_value
      end
    end
    new_value
  end
end