Class: Praxis::Mapper::Resource
- Inherits:
-
Object
- Object
- Praxis::Mapper::Resource
show all
- Extended by:
- Finalizable
- Defined in:
- lib/praxis/mapper/resource.rb
Class Attribute Summary collapse
Instance Attribute Summary collapse
Class Method Summary
collapse
-
._finalize! ⇒ Object
-
.all(condition = {}) ⇒ Object
-
.batch_computed(attribute, with_instance_method: true, &block) ⇒ Object
-
.batched_attributes ⇒ Object
-
.craft_field_selection_query(base_query, selectors:) ⇒ Object
-
.craft_filter_query(base_query, filters:) ⇒ Object
-
.craft_pagination_query(base_query, pagination:, selectors:) ⇒ Object
-
.define_accessor(name) ⇒ Object
-
.define_aliased_methods ⇒ Object
-
.define_batch_processors ⇒ Object
-
.define_delegation_for_related_association(resource_name, resource_attribute, related_association) ⇒ Object
-
.define_delegation_for_related_attribute(resource_name, resource_attribute) ⇒ Object
-
.define_model_accessors ⇒ Object
-
.define_model_association_accessor(name, association_spec) ⇒ Object
Defines wrappers for model associations that return Resources.
-
.define_property_groups ⇒ Object
Defines the dependencies and the method of a property group The dependencies are going to be defined as the methods that wrap the group’s attributes i.e., ‘group_attribute1’ The method defined will return a ForwardingStruct object instance, that will simply define a method name for each existing property which simply calls the underlying ‘group name’ prefixed methods on the original object For example: if we have a group named ‘grouping’, which has ‘name’ and ‘phone’ attributes defined.
-
.define_resource_delegate(resource_name, resource_attribute) ⇒ Object
-
.detect_invalid_properties ⇒ Object
Verifies if the system has badly defined properties For example, properties that correspond to an underlying association method (for which there is no overriden method in the resource) must not have dependencies defined, as it is clear the association is the only one.
-
.filters_mapping(definition = {}) ⇒ Object
TODO: this shouldn’t be needed if we incorporate it with the properties of the mapper…
-
.finalize_resource_delegates ⇒ Object
-
.for_record(record) ⇒ Object
-
.get(condition) ⇒ Object
-
.hookup_callbacks ⇒ Object
-
.inherited(klass) ⇒ Object
TODO: also support an attribute of sorts on the versioned resource module.
-
.model(klass = nil) ⇒ Object
TODO: Take symbol/string and resolve the klass (but lazily, so we don’t care about load order).
-
.order_mapping(definition = nil) ⇒ Object
-
.property(name, dependencies: nil, as: nil) ⇒ Object
The ‘as:` can be used for properties that correspond to an underlying association of a different name.
-
.property_group(name, media_type) ⇒ Object
Saves the name of the group, and the associated mediatype where the group attributes are defined at.
-
.resource_delegate(spec) ⇒ Object
-
.resource_delegates ⇒ Object
-
.validate_associations_path(model, path) ⇒ Object
-
.validate_properties ⇒ Object
-
.wrap(records) ⇒ Object
Instance Method Summary
collapse
_finalize!, extended, finalizable, finalize!, finalized?, inherited
Constructor Details
#initialize(record) ⇒ Resource
Returns a new instance of Resource.
497
498
499
|
# File 'lib/praxis/mapper/resource.rb', line 497
def initialize(record)
@record = record
end
|
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(name, *args) ⇒ Object
522
523
524
525
526
527
528
529
|
# File 'lib/praxis/mapper/resource.rb', line 522
def method_missing(name, *args)
if @record.respond_to?(name)
self.class.define_accessor(name)
send(name)
else
super
end
end
|
Class Attribute Details
.cached_forwarders ⇒ Object
Returns the value of attribute cached_forwarders.
51
52
53
|
# File 'lib/praxis/mapper/resource.rb', line 51
def cached_forwarders
@cached_forwarders
end
|
.memoized_variables ⇒ Object
Names of the memoizable things (without the @__ prefix)
53
54
55
|
# File 'lib/praxis/mapper/resource.rb', line 53
def memoized_variables
@memoized_variables
end
|
.model_map ⇒ Object
Returns the value of attribute model_map.
51
52
53
|
# File 'lib/praxis/mapper/resource.rb', line 51
def model_map
@model_map
end
|
.properties ⇒ Object
Returns the value of attribute properties.
51
52
53
|
# File 'lib/praxis/mapper/resource.rb', line 51
def properties
@properties
end
|
.property_groups ⇒ Object
Returns the value of attribute property_groups.
51
52
53
|
# File 'lib/praxis/mapper/resource.rb', line 51
def property_groups
@property_groups
end
|
Instance Attribute Details
#record ⇒ Object
Returns the value of attribute record.
39
40
41
|
# File 'lib/praxis/mapper/resource.rb', line 39
def record
@record
end
|
Class Method Details
._finalize! ⇒ Object
127
128
129
130
131
132
133
134
135
136
|
# File 'lib/praxis/mapper/resource.rb', line 127
def self._finalize!
validate_properties
finalize_resource_delegates
define_batch_processors
define_model_accessors
define_property_groups
hookup_callbacks
super
end
|
.all(condition = {}) ⇒ Object
334
335
336
337
338
|
# File 'lib/praxis/mapper/resource.rb', line 334
def self.all(condition = {})
records = model.all(condition)
wrap(records)
end
|
.batch_computed(attribute, with_instance_method: true, &block) ⇒ Object
113
114
115
116
117
118
119
120
121
|
# File 'lib/praxis/mapper/resource.rb', line 113
def self.batch_computed(attribute, with_instance_method: true, &block)
raise "This resource (#{name})is already finalized. Defining batch_computed attributes needs to be done before finalization" if @finalized
raise 'It is necessary to pass a block when using the batch_computed method' unless block_given?
required_params = block.parameters.select { |t, _n| t == :keyreq }.map { |_a, b| b }.uniq
raise 'The block for batch_computed can only accept one required kw param named :rows_by_id' unless required_params == [:rows_by_id]
@registered_batch_computations[attribute.to_sym] = { proc: block.to_proc, with_instance_method: with_instance_method }
end
|
.batched_attributes ⇒ Object
123
124
125
|
# File 'lib/praxis/mapper/resource.rb', line 123
def self.batched_attributes
@registered_batch_computations.keys
end
|
.craft_field_selection_query(base_query, selectors:) ⇒ Object
477
478
479
480
481
482
483
484
|
# File 'lib/praxis/mapper/resource.rb', line 477
def self.craft_field_selection_query(base_query, selectors:)
if selectors && model._field_selector_query_builder_class
debug = Praxis::Application.instance.config.mapper.debug_queries
base_query = model._field_selector_query_builder_class.new(query: base_query, selectors: selectors, debug: debug).generate
end
base_query
end
|
.craft_filter_query(base_query, filters:) ⇒ Object
466
467
468
469
470
471
472
473
474
475
|
# File 'lib/praxis/mapper/resource.rb', line 466
def self.craft_filter_query(base_query, filters:)
if filters
raise "To use API filtering, you must define the mapping of api-names to resource properties (using the `filters_mapping` method in #{self})" unless @_filters_map
debug = Praxis::Application.instance.config.mapper.debug_queries
base_query = model._filter_query_builder_class.new(query: base_query, model: model, filters_map: @_filters_map, debug: debug).generate(filters)
end
base_query
end
|
486
487
488
489
490
491
492
493
494
495
|
# File 'lib/praxis/mapper/resource.rb', line 486
def self.(base_query, pagination:, selectors:)
handler_klass = model.
return base_query unless handler_klass && (.paginator || .order)
.total_count = handler_klass.count(base_query.dup) if .paginator&.total_count
base_query = handler_klass.order(base_query, .order, root_resource: selectors.resource)
handler_klass.paginate(base_query, , root_resource: selectors.resource)
end
|
.define_accessor(name) ⇒ Object
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
|
# File 'lib/praxis/mapper/resource.rb', line 419
def self.define_accessor(name)
ivar_name = case name.to_s
when /\?/
"is_#{name.to_s[0..-2]}"
when /!/
"#{name.to_s[0..-2]}_bang"
else
name.to_s
end
memoized_variables << ivar_name
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{name}
return @__#{ivar_name} if instance_variable_defined?("@__#{ivar_name}")
@__#{ivar_name} = record.#{name}
end
RUBY
end
|
.define_aliased_methods ⇒ Object
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
|
# File 'lib/praxis/mapper/resource.rb', line 225
def self.define_aliased_methods
with_different_alias_name = properties.reject { |name, opts| name == opts[:as] || opts[:as].nil? }
with_different_alias_name.each do |prop_name, opts|
next if instance_methods.include? prop_name
unless opts[:as] == :self
raise "Cannot define property #{prop_name} with an `as:` option (#{opts[:as]}) for resource (#{name}) because it does not have associations!" unless model.respond_to?(:_praxis_associations)
raise "Invalid property definition named #{prop_name} for `as:` value '#{opts[:as]}': this association name/path does not exist" if validate_associations_path(model, opts[:as].to_s.split('.').map(&:to_sym))
end
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{prop_name}
#{opts[:as]}
end
RUBY
end
end
|
.define_batch_processors ⇒ Object
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
|
# File 'lib/praxis/mapper/resource.rb', line 177
def self.define_batch_processors
return unless @registered_batch_computations.presence
const_set(:BatchProcessors, Module.new)
@registered_batch_computations.each do |name, opts|
self::BatchProcessors.module_eval do
define_singleton_method(name, opts[:proc])
end
next unless opts[:with_instance_method]
define_method(name) do
self.class::BatchProcessors.send(name, rows_by_id: { id => self })[_pk]
end
end
end
|
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
|
# File 'lib/praxis/mapper/resource.rb', line 403
def self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
related_resource_class = model_map[related_association[:model]]
return unless related_resource_class
memoized_variables << resource_attribute
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{resource_attribute}
@__#{resource_attribute} ||= if (rec = self.#{resource_name})
if (related = rec.#{resource_attribute})
#{related_resource_class.name}.wrap(related)
end
end
end
RUBY
end
|
392
393
394
395
396
397
398
399
400
401
|
# File 'lib/praxis/mapper/resource.rb', line 392
def self.define_delegation_for_related_attribute(resource_name, resource_attribute)
memoized_variables << resource_attribute
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{resource_attribute}
@__#{resource_attribute} ||= if (rec = self.#{resource_name})
rec.#{resource_attribute}
end
end
RUBY
end
|
.define_model_accessors ⇒ Object
206
207
208
209
210
211
212
213
214
|
# File 'lib/praxis/mapper/resource.rb', line 206
def self.define_model_accessors
return if model.nil?
define_aliased_methods
model._praxis_associations.each do |k, v|
define_model_association_accessor(k, v) unless instance_methods.include? k
end
end
|
.define_model_association_accessor(name, association_spec) ⇒ Object
Defines wrappers for model associations that return Resources
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
|
# File 'lib/praxis/mapper/resource.rb', line 351
def self.define_model_association_accessor(name, association_spec)
association_model = association_spec.fetch(:model)
association_resource_class = model_map[association_model]
return unless association_resource_class
association_resource_class_name = "::#{association_resource_class}" memoized_variables << name
wrapping = \
case association_spec.fetch(:type)
when :one_to_many, :many_to_many
"@__#{name} ||= #{association_resource_class_name}.wrap(records)"
else
"@__#{name} ||= #{association_resource_class_name}.for_record(records)"
end
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{name}
return @__#{name} if instance_variable_defined?("@__#{name}")
records = record.#{name}
return nil if records.nil?
#{wrapping}
end
RUBY
end
|
.define_property_groups ⇒ Object
Defines the dependencies and the method of a property group The dependencies are going to be defined as the methods that wrap the group’s attributes i.e., ‘group_attribute1’ The method defined will return a ForwardingStruct object instance, that will simply define a method name for each existing property which simply calls the underlying ‘group name’ prefixed methods on the original object For example: if we have a group named ‘grouping’, which has ‘name’ and ‘phone’ attributes defined.
-
the property dependencies will be defined as: property :grouping, dependencies: [:name, :phone]
-
the ‘grouping’ method will return an instance object, that will respond to ‘name’ (and forward to ‘grouping_name’) and to ‘phone’ (and forward to ‘grouping_phone’)
255
256
257
258
259
260
261
262
263
264
265
266
267
268
|
# File 'lib/praxis/mapper/resource.rb', line 255
def self.define_property_groups
property_groups.each do |(name, media_type)|
prefixed_property_deps = media_type.attribute.attributes[name].type.attributes.keys.each_with_object({}) do |key, hash|
hash[key] = "#{name}_#{key}".to_sym
end
property name, dependencies: prefixed_property_deps.values
@cached_forwarders[name] = ForwardingStruct.for(prefixed_property_deps)
define_method(name) do
self.class.cached_forwarders[name].new(self)
end
end
end
|
.define_resource_delegate(resource_name, resource_attribute) ⇒ Object
381
382
383
384
385
386
387
388
389
390
|
# File 'lib/praxis/mapper/resource.rb', line 381
def self.define_resource_delegate(resource_name, resource_attribute)
related_model = model._praxis_associations[resource_name][:model]
related_association = related_model._praxis_associations[resource_attribute]
if related_association
define_delegation_for_related_association(resource_name, resource_attribute, related_association)
else
define_delegation_for_related_attribute(resource_name, resource_attribute)
end
end
|
.detect_invalid_properties ⇒ Object
Verifies if the system has badly defined properties For example, properties that correspond to an underlying association method (for which there is no overriden method in the resource) must not have dependencies defined, as it is clear the association is the only one
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
# File 'lib/praxis/mapper/resource.rb', line 149
def self.detect_invalid_properties
return nil unless !model.nil? && model.respond_to?(:_praxis_associations)
invalid = {}
existing_associations = model._praxis_associations.keys
properties.slice(*existing_associations).each do |prop_name, data|
next if instance_methods.include? prop_name
example_def = "property #{prop_name} "
example_def.concat("dependencies: #{data[:dependencies]}") if data[:dependencies].presence
example_def.concat("as: #{data[:as]}") if data[:as].presence
error = "Bad definition of property '#{prop_name}'. Method #{prop_name} is already an association " \
"which will be properly wrapped with an accessor, so you do not need to define it as a property.\n" \
"Current definition looks like: #{example_def}\n"
invalid[prop_name] = error
end
unless invalid.empty?
msg = "Error defining one or more propeties in resource #{name}.\n".dup
invalid.each_value { |err| msg.concat err }
msg.concat 'Only define properties for methods that you override in the resource, as a way to specify which dependencies ' \
"that requires to use inside it\n"
return msg
end
nil
end
|
.filters_mapping(definition = {}) ⇒ Object
TODO: this shouldn’t be needed if we incorporate it with the properties of the mapper… …maybe what this means is that we can change it for a better DSL in the resource?
439
440
441
442
443
444
445
446
447
448
449
|
# File 'lib/praxis/mapper/resource.rb', line 439
def self.filters_mapping(definition = {})
@_filters_map = \
case definition
when Hash
definition
when Array
definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
else
raise 'Resource.filters_mapping only allows a hash or an array'
end
end
|
.finalize_resource_delegates ⇒ Object
196
197
198
199
200
201
202
203
204
|
# File 'lib/praxis/mapper/resource.rb', line 196
def self.finalize_resource_delegates
return unless @resource_delegates
@resource_delegates.each do |record_name, record_attributes|
record_attributes.each do |record_attribute|
define_resource_delegate(record_name, record_attribute)
end
end
end
|
.for_record(record) ⇒ Object
303
304
305
306
307
308
309
310
311
312
313
314
|
# File 'lib/praxis/mapper/resource.rb', line 303
def self.for_record(record)
return record._resource if record._resource
if (resource_class_for_record = model_map[record.class])
record._resource = resource_class_for_record.new(record)
else
version = name.split('::')[0..-2].join('::')
resource_name = record.class.name.split('::').last
raise "No resource class corresponding to the model class '#{record.class}' is defined. (Did you forget to define '#{version}::#{resource_name}'?)"
end
end
|
.get(condition) ⇒ Object
328
329
330
331
332
|
# File 'lib/praxis/mapper/resource.rb', line 328
def self.get(condition)
record = model.get(condition)
wrap(record)
end
|
.hookup_callbacks ⇒ Object
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
|
# File 'lib/praxis/mapper/resource.rb', line 270
def self.hookup_callbacks
return unless ancestors.include?(Praxis::Mapper::Resources::Callbacks)
instance_module = nil
class_module = nil
affected_methods = (before_callbacks.keys + after_callbacks.keys + around_callbacks.keys).uniq
affected_methods&.each do |method|
calls = {}
calls[:before] = before_callbacks[method] if before_callbacks.key?(method)
calls[:around] = around_callbacks[method] if around_callbacks.key?(method)
calls[:after] = after_callbacks[method] if after_callbacks.key?(method)
if method.start_with?('self.')
simple_name = method.to_s.gsub(/^self./, '').to_sym
raise "Error building callback: Class-level method #{method} is not defined in class #{name}" unless methods.include?(simple_name)
class_module ||= Module.new
create_override_module(mod: class_module, method: method(simple_name), calls: calls)
else
raise "Error building callback: Instance method #{method} is not defined in class #{name}" unless method_defined?(method)
instance_module ||= Module.new
create_override_module(mod: instance_module, method: instance_method(method), calls: calls)
end
end
prepend instance_module if instance_module
singleton_class.send(:prepend, class_module) if class_module
end
|
.inherited(klass) ⇒ Object
TODO: also support an attribute of sorts on the versioned resource module. ie, V1::Resources.api_version.
replacing the self.superclass == Praxis::Mapper::Resource condition below.
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
# File 'lib/praxis/mapper/resource.rb', line 58
def self.inherited(klass)
super
klass.instance_eval do
@model_map = if superclass == Praxis::Mapper::Resource
{}
else
superclass.model_map
end
@properties = superclass.properties.clone
@property_groups = superclass.property_groups.clone
@cached_forwarders = superclass.cached_forwarders.clone
@registered_batch_computations = {} @_filters_map = {}
@_order_map = {}
@memoized_variables = []
end
end
|
.model(klass = nil) ⇒ Object
TODO: Take symbol/string and resolve the klass (but lazily, so we don’t care about load order)
82
83
84
85
86
87
88
89
90
91
|
# File 'lib/praxis/mapper/resource.rb', line 82
def self.model(klass = nil)
if klass
raise "Model #{klass.name} must be compatible with Praxis. Use ActiveModelCompat or similar compatability plugin." unless klass.methods.include?(:_praxis_associations)
@model = klass
model_map[klass] = self
else
@model
end
end
|
.order_mapping(definition = nil) ⇒ Object
451
452
453
454
455
456
457
458
459
460
461
462
463
464
|
# File 'lib/praxis/mapper/resource.rb', line 451
def self.order_mapping(definition = nil)
if definition.nil?
@_order_map ||= {} return @_order_map
end
@_order_map = \
case definition
when Hash
definition.transform_values(&:to_s)
else
raise 'Resource.orders_mapping only allows a hash'
end
end
|
.property(name, dependencies: nil, as: nil) ⇒ Object
The ‘as:` can be used for properties that correspond to an underlying association of a different name. With this, the selector generator, is able to follow and pass any incoming nested fields when necessary (as opposed to only add dependencies and discard nested fields) No dependencies are allowed to be defined if `as:` is used (as the dependencies should be defined at the final aliased property)
96
97
98
99
100
101
102
103
104
105
106
|
# File 'lib/praxis/mapper/resource.rb', line 96
def self.property(name, dependencies: nil, as: nil)
raise "Error defining property '#{name}' in #{self}. Property names must be symbols, not strings." unless name.is_a? Symbol
h = { dependencies: dependencies }
if as
raise 'Cannot use dependencies for a property when using the "as:" keyword' if dependencies.presence
h.merge!({ as: as })
end
properties[name] = h
end
|
.property_group(name, media_type) ⇒ Object
Saves the name of the group, and the associated mediatype where the group attributes are defined at
109
110
111
|
# File 'lib/praxis/mapper/resource.rb', line 109
def self.property_group(name, media_type)
property_groups[name] = media_type
end
|
.resource_delegate(spec) ⇒ Object
344
345
346
347
348
|
# File 'lib/praxis/mapper/resource.rb', line 344
def self.resource_delegate(spec)
spec.each do |resource_name, attributes|
resource_delegates[resource_name] = attributes
end
end
|
.resource_delegates ⇒ Object
340
341
342
|
# File 'lib/praxis/mapper/resource.rb', line 340
def self.resource_delegates
@resource_delegates ||= {}
end
|
.validate_associations_path(model, path) ⇒ Object
216
217
218
219
220
221
222
223
|
# File 'lib/praxis/mapper/resource.rb', line 216
def self.validate_associations_path(model, path)
first, *rest = path
assoc = model._praxis_associations[first]
return first unless assoc
rest.presence ? validate_associations_path(assoc[:model], rest) : nil
end
|
.validate_properties ⇒ Object
138
139
140
141
142
143
144
|
# File 'lib/praxis/mapper/resource.rb', line 138
def self.validate_properties
end
|
.wrap(records) ⇒ Object
316
317
318
319
320
321
322
323
324
325
326
|
# File 'lib/praxis/mapper/resource.rb', line 316
def self.wrap(records)
if records.nil?
[]
elsif records.is_a?(Enumerable)
records.compact.map { |record| for_record(record) }
elsif records.respond_to?(:to_a)
records.to_a.compact.map { |record| for_record(record) }
else
for_record(records)
end
end
|
Instance Method Details
#_pk ⇒ Object
By default every resource will have the main identifier (by default the id method) accessible through ‘_pk’
46
47
48
|
# File 'lib/praxis/mapper/resource.rb', line 46
def _pk
id
end
|
#clear_memoization ⇒ Object
507
508
509
510
511
512
|
# File 'lib/praxis/mapper/resource.rb', line 507
def clear_memoization
self.class.memoized_variables.each do |name|
ivar = "@__#{name}"
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
end
end
|
#reload ⇒ Object
501
502
503
504
505
|
# File 'lib/praxis/mapper/resource.rb', line 501
def reload
clear_memoization
reload_record
self
end
|
#reload_record ⇒ Object
514
515
516
|
# File 'lib/praxis/mapper/resource.rb', line 514
def reload_record
record.reload
end
|
#respond_to_missing?(name) ⇒ Boolean
518
519
520
|
# File 'lib/praxis/mapper/resource.rb', line 518
def respond_to_missing?(name, *)
@record.respond_to?(name) || super
end
|