class Praxis::Mapper::SelectorGeneratorNode

Attributes

fields_node[R]

prepend SelectorGeneratorNodeDebugger # Uncomment this to see the traces of how methods are called

model[R]

prepend SelectorGeneratorNodeDebugger # Uncomment this to see the traces of how methods are called

resource[R]

prepend SelectorGeneratorNodeDebugger # Uncomment this to see the traces of how methods are called

select[R]

prepend SelectorGeneratorNodeDebugger # Uncomment this to see the traces of how methods are called

tracks[R]

prepend SelectorGeneratorNodeDebugger # Uncomment this to see the traces of how methods are called

Public Class Methods

new(resource) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 76
def initialize(resource)
  @resource = resource
  @select = Set.new
  @select_star = false
  @fields_node = FieldDependenciesNode.new(name: '/', selector_node: self)
  @tracks = {}
end

Public Instance Methods

add(fields) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 88
def add(fields)
  fields.each do |name, field|
    fields_node.start_field(name)
    map_property(name, field)
    fields_node.end_field
  end
end
add_association(name, fields) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 146
def add_association(name, fields)
  # fields_node.retrieve_last_of_chain = true
  association = resource.model._praxis_associations.fetch(name) do
    raise "missing association for #{resource} with name #{name}"
  end
  associated_resource = resource.model_map[association[:model]]
  raise "Whoops! could not find a resource associated with model #{association[:model]} (root resource #{resource})" unless associated_resource

  # Add the required columns in this model to make sure the association can be loaded
  association[:local_key_columns].each { |col| add_select(col, add_field: false) }

  node = SelectorGeneratorNode.new(associated_resource)
  unless association[:remote_key_columns].empty?
    # Make sure we add the required columns for this association to the remote model query
    fields = {} if fields == true
    new_fields_as_hash = association[:remote_key_columns].each_with_object({}) do |key, hash|
      hash[key] = true
    end
    fields = fields.merge(new_fields_as_hash)
  end

  node.add(fields) unless fields == true

  merge_track(name, node)
  node
end
add_fwding_property(name, fields) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 182
def add_fwding_property(name, fields)
  aliased_as = resource.properties[name][:as]
  if aliased_as == :self
    # Special keyword to add itself as the association, but still continue procesing the fields
    # This is useful when we expose resource fields tucked inside another sub-struct, this way
    # we can make sure that if the fields necessary to compute things inside the struct, they are preloaded
    add(fields) unless fields == true
  else
    # Assumes (as: option of the property DSL should check check) that all forwarded properties need to be pure associations
    # We know we've now added the chain of association dependencies under our node...so we'll start getting the 'first' of them
    # and recurse down the node until the leaf.
    # Then, we need to apply the incoming fields to that.
    leaf_node = add_string_association(*aliased_as.to_s.split('.').map(&:to_sym))
    leaf_node.add(fields) unless fields == true # If true, no fields to apply
    leaf_node
  end
end
add_property(name, fields) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 200
def add_property(name, fields)
  dependencies = resource.properties[name][:dependencies]
  # Always add the underlying association if we're overriding the name...
  if (praxis_compat_model = resource.model&.respond_to?(:_praxis_associations))
    aliased_as = resource.properties[name][:as]
    if aliased_as
      if aliased_as == :self
        # Special keyword to add itself as the association, but still continue procesing the fields
        # This is useful when we expose resource fields tucked inside another sub-struct, this way
        # we can make sure that if the fields necessary to compute things inside the struct, they are preloaded
        add(fields)
      else
        first, *rest = aliased_as.to_s.split('.').map(&:to_sym)

        extended_fields = \
          if rest.empty?
            fields
          else
            rest.reverse.inject(fields) do |accum, prop|
              { prop => accum }
            end
          end

        add_association(first, extended_fields) if resource.model._praxis_associations[first]
      end
    elsif resource.model._praxis_associations[name]
      # Not aliased ... but if there is an existing association for the propety name, we add it (and ignore any deps in place)
      add_association(name, fields)
    end
  end
  # If we have a property group, and the subfields want to selectively restrict what to depend on
  if fields != true && resource.property_groups[name]
    # Prepend the group name to fields if it's an inner hash
    prefixed_fields = fields == true ? {} : fields.keys.each_with_object({}) { |k, h| h["#{name}_#{k}".to_sym] = k }
    # Try to match all inner fields
    prefixed_fields.each do |prefixedname, origfieldname|
      next unless dependencies.include?(prefixedname)

      fields_node.start_field(origfieldname) # Mark it as orig name
      apply_dependency(prefixedname, fields[origfieldname])
      fields_node.end_field
    end
  else
    dependencies&.each do |dependency|
      # To detect recursion, let's allow mapping depending fields to the same name of the property
      # but properly detecting if it's a real association...in which case we've already added it above
      if dependency == name
        add_select(name) unless praxis_compat_model && resource.model._praxis_associations.key?(name)
      else
        apply_dependency(dependency)
      end
    end
  end
end
add_select(name, add_field: true) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 173
def add_select(name, add_field: true)
  return @select_star = true if name == :*
  return if @select_star

  # Do not add a field dependency, if we know we're just adding a Local/FK constraint
  @fields_node.add_local_dep(name) if add_field
  @select.add name
end
add_string_association(first, *rest) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 120
def add_string_association(first, *rest)
  association = resource.model._praxis_associations.fetch(first) do
    raise "missing association for #{resource} with name #{first}"
  end
  associated_resource = resource.model_map[association[:model]]
  raise "Whoops! could not find a resource associated with model #{association[:model]} (root resource #{resource})" unless associated_resource

  # Add the required columns in this model to make sure the association can be loaded
  association[:local_key_columns].each { |col| add_select(col, add_field: false) }

  node = SelectorGeneratorNode.new(associated_resource)
  unless association[:remote_key_columns].empty?
    # Make sure we add the required columns for this association to the remote model query
    fields = {}
    new_fields_as_hash = association[:remote_key_columns].each_with_object({}) do |key, hash|
      hash[key] = true
    end
    fields = fields.merge(new_fields_as_hash)
  end

  node.add(fields) unless fields == true
  leaf_node = rest.empty? ? nil : node.add_string_association(*rest)
  merge_track(first, node)
  leaf_node || node # Return the leaf (i.e., us, if we're the last component or the result of the string_association if there was one)
end
apply_dependency(dependency, fields = true) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 255
def apply_dependency(dependency, fields = true)
  case dependency
  when Symbol
    map_property(dependency, fields, as_dependency: true)
  when String
    head, *tail = dependency.split('.').collect(&:to_sym)
    raise 'String dependencies can not be singular' if tail.nil?

    add_association(head, tail.reverse.inject(true) { |hash, dep| { dep => hash } })
  end
end
dump(mode: :columns_and_tracks) click to toggle source

Debugging method for rspec, to easily match the desired output By default it only outputs the info related to computing columns and track dependencies. Overriding the mode will allow to dump the model and only the field dependencies

# File lib/praxis/mapper/selector_generator.rb, line 286
def dump(mode: :columns_and_tracks)
  hash = {}
  hash[:model] = resource.model
  case mode
  when :columns_and_tracks
    if !@select.empty? || @select_star
      hash[:columns] = @select_star ? [:*] : @select.to_a
    end
  when :fields
    dumped_fields_node = @fields_node.dump
    raise "Fields node has more keys than fields!! #{dumped_fields_node}" if dumped_fields_node.keys.size > 1

    hash[:fields] = dumped_fields_node[:fields] if dumped_fields_node[:fields]
  else
    raise "Unknown mode #{mode} for dumping SelectorGenerator"
  end
  hash[:tracks] = @tracks.transform_values { |v| v.dump(mode: mode) } unless @tracks.empty?
  hash
end
inspect() click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 84
def inspect
  "<#{self.class}# @resource=#{@resource.name} @select=#{@select} @select_star=#{@select_star} tracking: #{@tracks.keys} (recursion omited)>"
end
map_property(name, fields, as_dependency: false) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 96
def map_property(name, fields, as_dependency: false)
  praxis_compat_model = resource.model&.respond_to?(:_praxis_associations)
  if resource.properties.key?(name)
    if (target = resource.properties[name][:as])
      leaf_node = add_fwding_property(name, fields)
      fields_node.save_reference(leaf_node) unless target == :self
    else
      add_property(name, fields)
    end
    fields_node.add_local_dep(name)
  elsif praxis_compat_model && resource.model._praxis_associations.key?(name)
    add_association(name, fields)
    # Single association properties are also pointing to the corresponding tracked SelectorGeneratorNode
    # but only if they are implicit properties, without dependencies
    if as_dependency
      fields_node.add_local_dep(name)
    else
      fields_node.save_reference(tracks[name])
    end
  else
    add_select(name)
  end
end
merge_track(track_name, node) click to toggle source
# File lib/praxis/mapper/selector_generator.rb, line 267
def merge_track(track_name, node)
  raise "Cannot merge another node for association #{track_name}: incompatible model" unless node.model == model

  existing = tracks[track_name]
  if existing
    node.select.each do |col_name|
      existing.add_select(col_name)
    end
    node.tracks.each do |name, n|
      existing.merge_track(name, n)
    end
  else
    tracks[track_name] = node
  end
end