Class: Praxis::Mapper::SelectorGeneratorNode

Inherits:
Object
  • Object
show all
Defined in:
lib/praxis/mapper/selector_generator.rb

Defined Under Namespace

Classes: FieldDependenciesNode

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(resource) ⇒ SelectorGeneratorNode

Returns a new instance of SelectorGeneratorNode.



76
77
78
79
80
81
82
# 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

Instance Attribute Details

#fields_nodeObject (readonly)

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



7
8
9
# File 'lib/praxis/mapper/selector_generator.rb', line 7

def fields_node
  @fields_node
end

#modelObject (readonly)

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



7
8
9
# File 'lib/praxis/mapper/selector_generator.rb', line 7

def model
  @model
end

#resourceObject (readonly)

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



7
8
9
# File 'lib/praxis/mapper/selector_generator.rb', line 7

def resource
  @resource
end

#selectObject (readonly)

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



7
8
9
# File 'lib/praxis/mapper/selector_generator.rb', line 7

def select
  @select
end

#tracksObject (readonly)

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



7
8
9
# File 'lib/praxis/mapper/selector_generator.rb', line 7

def tracks
  @tracks
end

Instance Method Details

#add(fields) ⇒ Object



88
89
90
91
92
93
94
# 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) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# 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) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# 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) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# 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) ⇒ Object



173
174
175
176
177
178
179
180
# 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) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# 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) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
# 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) ⇒ Object

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



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# 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

#inspectObject



84
85
86
# 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) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# 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) ⇒ Object



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# 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