praxis 0.22.pre.2 → 2.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +323 -324
  3. data/lib/praxis/action_definition.rb +7 -9
  4. data/lib/praxis/api_definition.rb +27 -44
  5. data/lib/praxis/api_general_info.rb +2 -3
  6. data/lib/praxis/application.rb +14 -141
  7. data/lib/praxis/bootloader.rb +1 -2
  8. data/lib/praxis/bootloader_stages/environment.rb +13 -0
  9. data/lib/praxis/controller.rb +0 -2
  10. data/lib/praxis/dispatcher.rb +4 -6
  11. data/lib/praxis/docs/generator.rb +8 -18
  12. data/lib/praxis/docs/link_builder.rb +1 -1
  13. data/lib/praxis/error_handler.rb +5 -5
  14. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +1 -1
  15. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
  16. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +16 -18
  17. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +5 -5
  18. data/lib/praxis/extensions/field_selection.rb +1 -12
  19. data/lib/praxis/extensions/rendering.rb +1 -1
  20. data/lib/praxis/file_group.rb +1 -1
  21. data/lib/praxis/handlers/xml.rb +1 -1
  22. data/lib/praxis/mapper/active_model_compat.rb +63 -0
  23. data/lib/praxis/mapper/resource.rb +242 -0
  24. data/lib/praxis/mapper/selector_generator.rb +126 -0
  25. data/lib/praxis/mapper/sequel_compat.rb +37 -0
  26. data/lib/praxis/middleware_app.rb +13 -15
  27. data/lib/praxis/multipart/part.rb +3 -5
  28. data/lib/praxis/plugins/mapper_plugin.rb +50 -0
  29. data/lib/praxis/request.rb +14 -7
  30. data/lib/praxis/request_stages/response.rb +2 -3
  31. data/lib/praxis/resource_definition.rb +10 -14
  32. data/lib/praxis/response.rb +6 -5
  33. data/lib/praxis/response_definition.rb +5 -7
  34. data/lib/praxis/response_template.rb +3 -4
  35. data/lib/praxis/responses/http.rb +36 -0
  36. data/lib/praxis/responses/internal_server_error.rb +12 -3
  37. data/lib/praxis/responses/multipart_ok.rb +11 -4
  38. data/lib/praxis/responses/validation_error.rb +10 -1
  39. data/lib/praxis/router.rb +3 -3
  40. data/lib/praxis/tasks/api_docs.rb +2 -10
  41. data/lib/praxis/tasks/routes.rb +0 -1
  42. data/lib/praxis/version.rb +1 -1
  43. data/lib/praxis.rb +13 -9
  44. data/praxis.gemspec +2 -3
  45. data/spec/functional_spec.rb +0 -1
  46. data/spec/praxis/action_definition_spec.rb +15 -26
  47. data/spec/praxis/api_definition_spec.rb +8 -13
  48. data/spec/praxis/api_general_info_spec.rb +8 -3
  49. data/spec/praxis/application_spec.rb +7 -13
  50. data/spec/praxis/handlers/xml_spec.rb +2 -2
  51. data/spec/praxis/mapper/resource_spec.rb +169 -0
  52. data/spec/praxis/mapper/selector_generator_spec.rb +301 -0
  53. data/spec/praxis/middleware_app_spec.rb +15 -9
  54. data/spec/praxis/request_spec.rb +7 -17
  55. data/spec/praxis/request_stages/validate_spec.rb +1 -1
  56. data/spec/praxis/resource_definition_spec.rb +10 -12
  57. data/spec/praxis/response_definition_spec.rb +5 -22
  58. data/spec/praxis/response_spec.rb +5 -12
  59. data/spec/praxis/responses/internal_server_error_spec.rb +5 -2
  60. data/spec/praxis/router_spec.rb +4 -8
  61. data/spec/spec_app/app/models/person.rb +3 -3
  62. data/spec/spec_app/config/environment.rb +3 -21
  63. data/spec/spec_app/config.ru +6 -1
  64. data/spec/spec_helper.rb +2 -17
  65. data/spec/support/spec_resources.rb +131 -0
  66. metadata +19 -31
  67. data/lib/praxis/extensions/attribute_filtering/query_builder.rb +0 -39
  68. data/lib/praxis/extensions/attribute_filtering.rb +0 -28
  69. data/lib/praxis/extensions/mapper_selectors.rb +0 -16
  70. data/lib/praxis/media_type_collection.rb +0 -127
  71. data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
  72. data/spec/praxis/media_type_collection_spec.rb +0 -157
  73. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+ module Praxis
4
+ module Extensions
5
+ class SequelFilterQueryBuilder
6
+ attr_reader :query, :root
7
+
8
+ # Abstract class, which needs to be used by subclassing it through the .for method, to set the mapping of attributes
9
+ class << self
10
+ def for(definition)
11
+ Class.new(self) do
12
+ @attr_to_column = case definition
13
+ when Hash
14
+ definition
15
+ when Array
16
+ definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
17
+ else
18
+ raise "Cannot use FilterQueryBuilder.of without passing an array or a hash (Got: #{definition.class.name})"
19
+ end
20
+ class << self
21
+ attr_reader :attr_to_column
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # Base query to build upon
28
+ # table is necessary when use the strin queries, when the query has multiple tables involved
29
+ # (to disambiguate)
30
+ def initialize(query:, model: )
31
+ @query = query
32
+ @root = model.table_name
33
+ end
34
+
35
+ # By default we'll simply use the incoming op and value, and will map
36
+ # the attribute based on what's on the `attr_to_column` hash
37
+ def build_clause(filters)
38
+ seen_associations = Set.new
39
+ filters.each do |(attr, spec)|
40
+ column_name = attr_to_column[attr]
41
+ raise "Filtering by #{attr} not allowed (no mapping found)" unless column_name
42
+ if column_name.is_a?(Proc)
43
+ bindings = column_name.call(spec)
44
+ # A hash of bindings, consisting of a key with column name and a value to the query value
45
+ bindings.each{|col,val| expand_binding(column_name: col, op: spec[:op], value: val )}
46
+ else
47
+ expand_binding(column_name: column_name, **spec)
48
+ end
49
+ end
50
+ query
51
+ end
52
+
53
+ def expand_binding(column_name:,op:,value:)
54
+ assoc_or_field, *rest = column_name.to_s.split('.')
55
+ if rest.empty?
56
+ column_name = Sequel.qualify(root,column_name)
57
+ else
58
+ puts "Adding eager graph for #{assoc_or_field} due to being used in filter"
59
+ # Ensure the joined table is aliased properly (to the association name) so we can add the condition appropriately
60
+ @query = query.eager_graph(Sequel.as(assoc_or_field.to_sym, assoc_or_field.to_sym) )
61
+ column_name = Sequel.qualify(assoc_or_field, rest.first)
62
+ end
63
+ add_clause(attr: column_name, op: op, value: value)
64
+ end
65
+
66
+ def attr_to_column
67
+ # Class method defined by the subclassing Class (using .for)
68
+ self.class.attr_to_column
69
+ end
70
+
71
+ # Private to try to funnel all column names through `build_clause` that restricts
72
+ # the attribute names better (to allow more difficult SQL injections )
73
+ private def add_clause(attr:, op:, value:)
74
+ # TODO: partial matching
75
+ #components = attr.to_s.split('.')
76
+ #attr_selector = Sequel.qualify(*components)
77
+ attr_selector = attr
78
+ # HERE!! if we have "association.name" we should properly join it ...!
79
+
80
+ #> ds.eager_graph(:device).where{{device[:name] => 'A%'}}.select(:accountID)
81
+ #=> #<Sequel::Mysql2::Dataset: "SELECT `accountID` FROM `EventData`
82
+ # LEFT OUTER JOIN `Device` AS `device` ON
83
+ # ((`device`.`accountID` = `EventData`.`accountID`) AND (`device`.`deviceID` = `EventData`.`deviceID`))
84
+ # WHERE (`device`.`name` = 'A%')">
85
+ likeval = get_like_value(value)
86
+ @query = case op
87
+ when '='
88
+ if likeval
89
+ query.where(Sequel.like(attr_selector, likeval))
90
+ else
91
+ query.where(attr_selector => value)
92
+ end
93
+ when '!='
94
+ if likeval
95
+ query.exclude(Sequel.like(attr_selector, likeval))
96
+ else
97
+ query.exclude(attr_selector => value)
98
+ end
99
+ when '>'
100
+ #query.where(Sequel.lit("#{attr_selector} > ?", value))
101
+ query.where{attr_selector > value}
102
+ when '<'
103
+ query.where{attr_selector < value}
104
+ when '>='
105
+ query.where{attr_selector >= value}
106
+ when '<='
107
+ query.where{attr_selector <= value}
108
+ else
109
+ raise "Unsupported Operator!!! #{op}"
110
+ end
111
+ end
112
+
113
+ # Returns nil if the value was not a fuzzzy pattern
114
+ def get_like_value(value)
115
+ if value.is_a?(String) && (value[-1] == '*' || value[0] == '*')
116
+ likeval = value.dup
117
+ likeval[-1] = '%' if value[-1] == '*'
118
+ likeval[0] = '%' if value[0] == '*'
119
+ likeval
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ # rubocop:enable all
@@ -3,49 +3,47 @@ module Praxis
3
3
  module Extensions
4
4
  module FieldSelection
5
5
  class ActiveRecordQuerySelector
6
- attr_reader :selector, :ds, :top_model, :resolved, :root
6
+ attr_reader :selector, :query, :top_model, :resolved, :root
7
7
  # Gets a dataset, a selector...and should return a dataset with the selector definition applied.
8
- def initialize(ds:, model:, selectors:, resolved:)
8
+ def initialize(query:, model:, selectors:, resolved:)
9
9
  @selector = selectors
10
- @ds = ds
10
+ @query = query
11
11
  @top_model = model
12
12
  @resolved = resolved
13
13
  @seen = Set.new
14
14
  @root = model.table_name
15
15
  end
16
16
 
17
- def add_select(ds:, model:, table_name:)
17
+ def add_select(query:, model:, table_name:)
18
18
  if (fields = fields_for(model))
19
19
  # Note, let's always add the pk fields so that associations can load properly
20
20
  fields = fields | [model.primary_key.to_sym]
21
- ds.select(*fields)
21
+ query.select(*fields)
22
22
  else
23
- ds
23
+ query
24
24
  end
25
25
  end
26
26
 
27
27
  def generate
28
28
  # TODO: unfortunately, I think we can only control the select clauses for the top model
29
29
  # (as I'm not sure ActiveRecord supports expressing it in the join...)
30
- @ds = add_select(ds: ds, model: top_model, table_name: root)
30
+ @query = add_select(query: query, model: top_model, table_name: root)
31
31
 
32
- @ds.includes(_eager(top_model, resolved) )
32
+ @query.includes(_eager(top_model, resolved) )
33
33
  end
34
34
 
35
35
  def _eager(model, resolved)
36
- # Cannot select fields in included rels...boooo :()
37
- # d = add_select(ds: dset, model: model, table_name: model.table_name)
38
- tracks = only_assoc_for(model, resolved)
39
- tracks.inject([]) do |dataset, track|
40
- next dataset if @seen.include?([model, track])
41
- @seen << [model, track]
42
- assoc_model = model.associations[track][:model]
43
- dataset << { track => _eager(assoc_model, resolved[track]) }
44
- end
36
+ tracks = only_assoc_for(model, resolved)
37
+ tracks.inject([]) do |dataset, track|
38
+ next dataset if @seen.include?([model, track])
39
+ @seen << [model, track]
40
+ assoc_model = model._praxis_associations[track][:model]
41
+ dataset << { track => _eager(assoc_model, resolved[track]) }
42
+ end
45
43
  end
46
44
 
47
45
  def only_assoc_for(model, hash)
48
- hash.keys.reject { |assoc| model.associations[assoc].nil? }
46
+ hash.keys.reject { |assoc| model._praxis_associations[assoc].nil? }
49
47
  end
50
48
 
51
49
  def fields_for(model)
@@ -5,9 +5,9 @@ module Praxis
5
5
  class SequelQuerySelector
6
6
  attr_reader :selector, :ds, :top_model, :resolved, :root
7
7
  # Gets a dataset, a selector...and should return a dataset with the selector definition applied.
8
- def initialize(ds:, model:, selectors:, resolved:)
8
+ def initialize(query:, model:, selectors:, resolved:)
9
9
  @selector = selectors
10
- @ds = ds
10
+ @ds = query
11
11
  @top_model = model
12
12
  @resolved = resolved
13
13
  @seen = Set.new
@@ -32,7 +32,7 @@ module Praxis
32
32
  @ds = tracks.inject(@ds) do |dataset, track|
33
33
  next dataset if @seen.include?([top_model, track])
34
34
  @seen << [top_model, track]
35
- assoc_model = top_model.associations[track][:model]
35
+ assoc_model = top_model._praxis_associations[track][:model]
36
36
  # hash[track] = _eager(assoc_model, resolved[track])
37
37
  dataset.eager(track => _eager(assoc_model, resolved[track]))
38
38
  end
@@ -46,14 +46,14 @@ module Praxis
46
46
  tracks.inject(d) do |dataset, track|
47
47
  next dataset if @seen.include?([model, track])
48
48
  @seen << [model, track]
49
- assoc_model = model.associations[track][:model]
49
+ assoc_model = model._praxis_associations[track][:model]
50
50
  dataset.eager(track => _eager(assoc_model, resolved[track]))
51
51
  end
52
52
  end
53
53
  end
54
54
 
55
55
  def only_assoc_for(model, hash)
56
- hash.keys.reject { |assoc| model.associations[assoc].nil? }
56
+ hash.keys.reject { |assoc| model._praxis_associations[assoc].nil? }
57
57
  end
58
58
 
59
59
  def fields_for(model)
@@ -1,13 +1,2 @@
1
1
  require 'attributor/extras/field_selector'
2
-
3
- require 'praxis/extensions/field_selection/field_selector'
4
- # TODO: we should conditionally require it based on what ORM/s we want...
5
- require 'praxis/extensions/field_selection/active_record_query_selector'
6
-
7
-
8
- module Praxis
9
- module Extensions
10
- module FieldSelection
11
- end
12
- end
13
- end
2
+ require 'praxis/extensions/field_selection/field_selector'
@@ -24,7 +24,7 @@ module Praxis
24
24
  response.body = render(object, include_nil: include_nil)
25
25
  response
26
26
  rescue Praxis::Renderer::CircularRenderingError => e
27
- Praxis::Application.current_instance.validation_handler.handle!(
27
+ Praxis::Application.instance.validation_handler.handle!(
28
28
  summary: "Circular Rendering Error when rendering response. " +
29
29
  "Please especify a view to narrow the dependent fields, or narrow your field set.",
30
30
  exception: e,
@@ -7,7 +7,7 @@ module Praxis
7
7
  def initialize(base, &block)
8
8
  if base.nil?
9
9
  raise ArgumentError, "base must not be nil." \
10
- "Have you forgot to call 'setup' on the Praxis application instance?"
10
+ "Are you missing a call Praxis::Application.instance.setup?"
11
11
  end
12
12
 
13
13
 
@@ -60,7 +60,7 @@ module Praxis
60
60
  when "symbol"
61
61
  return node.content.to_sym
62
62
  when "decimal"
63
- return BigDecimal.new(node.content)
63
+ return BigDecimal(node.content)
64
64
  when "float"
65
65
  return Float(node.content)
66
66
  when "boolean"
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ require 'praxis/extensions/field_selection/active_record_query_selector'
6
+
7
+ module Praxis
8
+ module Mapper
9
+ module ActiveModelCompat
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ attr_accessor :_resource
14
+ end
15
+
16
+ module ClassMethods
17
+ def _filter_query_builder_class
18
+ Praxis::Extensions::ActiveRecordFilterQueryBuilder
19
+ end
20
+
21
+ def _field_selector_query_builder_class
22
+ Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector
23
+ end
24
+
25
+ def _praxis_associations
26
+ orig = self.reflections.clone
27
+
28
+ orig.each_with_object({}) do |(k, v), hash|
29
+ # Assume an 'id' primary key if the system is initializing without AR connected
30
+ # (or without the tables created). This probably means that it's a rake task initializing or so...
31
+ pkey = \
32
+ if v.klass.connected? && v.klass.table_exists?
33
+ v.klass.primary_key
34
+ else
35
+ 'id'
36
+ end
37
+ info = { model: v.klass, primary_key: pkey }
38
+ info[:type] = \
39
+ case v
40
+ when ActiveRecord::Reflection::BelongsToReflection
41
+ :many_to_one
42
+ when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasOneReflection
43
+ :one_to_many
44
+ when ActiveRecord::Reflection::ThroughReflection
45
+ :many_to_many
46
+ else
47
+ raise "Unknown association type: #{v.class.name} on #{v.klass.name} for #{v.name}"
48
+ end
49
+
50
+ if v.is_a?(ActiveRecord::Reflection::ThroughReflection)
51
+ info[:through] = v.through_reflection.name # TODO: is this correct?
52
+ end
53
+
54
+ # TODO: add more keys for the association to make true praxis mapper functions happy
55
+ hash[k.to_sym] = info
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,242 @@
1
+ # A resource creates a data store and instantiates a list of models that it wishes to load, building up the overall set of data that it will need.
2
+ # Once that is complete, the data set is iterated and a resultant view is generated.
3
+ module Praxis::Mapper
4
+
5
+ class Resource
6
+ extend Praxis::Finalizable
7
+
8
+ attr_accessor :record
9
+
10
+ @properties = {}
11
+
12
+ class << self
13
+ attr_reader :model_map
14
+ attr_reader :properties
15
+ end
16
+
17
+ # TODO: also support an attribute of sorts on the versioned resource module. ie, V1::Resources.api_version.
18
+ # replacing the self.superclass == Praxis::Mapper::Resource condition below.
19
+ def self.inherited(klass)
20
+ super
21
+
22
+ klass.instance_eval do
23
+ # It is expected that each versioned set of resources
24
+ # will have a common Base class, and so should share
25
+ # a model_map
26
+ if self.superclass == Praxis::Mapper::Resource
27
+ @model_map = Hash.new
28
+ else
29
+ @model_map = self.superclass.model_map
30
+ end
31
+
32
+ @properties = self.superclass.properties.clone
33
+ end
34
+
35
+ end
36
+
37
+ #TODO: Take symbol/string and resolve the klass (but lazily, so we don't care about load order)
38
+ def self.model(klass=nil)
39
+ if klass
40
+ raise "Model #{klass.name} must be compatible with Praxis. Use ActiveModelCompat or similar compatability plugin." unless klass.methods.include?(:_praxis_associations)
41
+ @model = klass
42
+ self.model_map[klass] = self
43
+ else
44
+ @model
45
+ end
46
+ end
47
+
48
+ def self.property(name, **options)
49
+ self.properties[name] = options
50
+ end
51
+
52
+ def self._finalize!
53
+ finalize_resource_delegates
54
+ define_model_accessors
55
+
56
+ super
57
+ end
58
+
59
+ def self.finalize_resource_delegates
60
+ return unless @resource_delegates
61
+
62
+ @resource_delegates.each do |record_name, record_attributes|
63
+ record_attributes.each do |record_attribute|
64
+ self.define_resource_delegate(record_name, record_attribute)
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ def self.define_model_accessors
71
+ return if model.nil?
72
+
73
+ model._praxis_associations.each do |k,v|
74
+ unless self.instance_methods.include? k
75
+ define_model_association_accessor(k,v)
76
+ end
77
+ end
78
+ end
79
+
80
+ def self.for_record(record)
81
+ return record._resource if record._resource
82
+
83
+ if resource_class_for_record = model_map[record.class]
84
+ return record._resource = resource_class_for_record.new(record)
85
+ else
86
+ version = self.name.split("::")[0..-2].join("::")
87
+ resource_name = record.class.name.split("::").last
88
+
89
+ raise "No resource class corresponding to the model class '#{record.class}' is defined. (Did you forget to define '#{version}::#{resource_name}'?)"
90
+ end
91
+ end
92
+
93
+
94
+ def self.wrap(records)
95
+ if records.nil?
96
+ return []
97
+ elsif( records.is_a?(Enumerable) )
98
+ return records.compact.map { |record| self.for_record(record) }
99
+ elsif ( records.respond_to?(:to_a) )
100
+ return records.to_a.compact.map { |record| self.for_record(record) }
101
+ else
102
+ return self.for_record(records)
103
+ end
104
+ end
105
+
106
+
107
+ def self.get(condition)
108
+ record = self.model.get(condition)
109
+
110
+ self.wrap(record)
111
+ end
112
+
113
+ def self.all(condition={})
114
+ records = self.model.all(condition)
115
+
116
+ self.wrap(records)
117
+ end
118
+
119
+
120
+ def self.resource_delegates
121
+ @resource_delegates ||= {}
122
+ end
123
+
124
+ def self.resource_delegate(spec)
125
+ spec.each do |resource_name, attributes|
126
+ resource_delegates[resource_name] = attributes
127
+ end
128
+ end
129
+
130
+ # Defines wrappers for model associations that return Resources
131
+ def self.define_model_association_accessor(name, association_spec)
132
+ association_model = association_spec.fetch(:model)
133
+ association_resource_class = model_map[association_model]
134
+
135
+ if association_resource_class
136
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
137
+ def #{name}
138
+ records = record.#{name}
139
+ return nil if records.nil?
140
+ @__#{name} ||= #{association_resource_class}.wrap(records)
141
+ end
142
+ RUBY
143
+ end
144
+ end
145
+
146
+ def self.define_resource_delegate(resource_name, resource_attribute)
147
+ related_model = model._praxis_associations[resource_name][:model]
148
+ related_association = related_model._praxis_associations[resource_attribute]
149
+
150
+ if related_association
151
+ self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
152
+ else
153
+ self.define_delegation_for_related_attribute(resource_name, resource_attribute)
154
+ end
155
+ end
156
+
157
+
158
+ def self.define_delegation_for_related_attribute(resource_name, resource_attribute)
159
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
160
+ def #{resource_attribute}
161
+ @__#{resource_attribute} ||= if (rec = self.#{resource_name})
162
+ rec.#{resource_attribute}
163
+ end
164
+ end
165
+ RUBY
166
+ end
167
+
168
+ def self.define_delegation_for_related_association(resource_name, resource_attribute, related_association)
169
+ related_resource_class = model_map[related_association[:model]]
170
+ return unless related_resource_class
171
+
172
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
173
+ def #{resource_attribute}
174
+ @__#{resource_attribute} ||= if (rec = self.#{resource_name})
175
+ if (related = rec.#{resource_attribute})
176
+ #{related_resource_class.name}.wrap(related)
177
+ end
178
+ end
179
+ end
180
+ RUBY
181
+ end
182
+
183
+ def self.define_accessor(name)
184
+ if name.to_s =~ /\?/
185
+ ivar_name = "is_#{name.to_s[0..-2]}"
186
+ else
187
+ ivar_name = "#{name}"
188
+ end
189
+
190
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
191
+ def #{name}
192
+ return @__#{ivar_name} if defined? @__#{ivar_name}
193
+ @__#{ivar_name} = record.#{name}
194
+ end
195
+ RUBY
196
+ end
197
+
198
+ # TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
199
+ def self.filters_mapping(hash)
200
+ @_filter_query_builder_class = model._filter_query_builder_class.for(**hash)
201
+ end
202
+
203
+ def self._filter_query_builder_class
204
+ @_filter_query_builder_class
205
+ end
206
+
207
+ def self.craft_filter_query(base_query, filters:) # rubocop:disable Metrics/AbcSize
208
+ if filters && _filter_query_builder_class
209
+ base_query = _filter_query_builder_class.new(query: base_query, model: model).build_clause(filters)
210
+ end
211
+
212
+ base_query
213
+ end
214
+
215
+ def self.craft_field_selection_query(base_query, selectors:, resolved:) # rubocop:disable Metrics/AbcSize
216
+ if selectors && model._field_selector_query_builder_class
217
+ base_query = model._field_selector_query_builder_class.new(query: base_query, model: self.model,
218
+ selectors: selectors, resolved: resolved).generate
219
+ end
220
+
221
+ base_query
222
+ end
223
+
224
+ def initialize(record)
225
+ @record = record
226
+ end
227
+
228
+ def respond_to_missing?(name,*)
229
+ @record.respond_to?(name) || super
230
+ end
231
+
232
+ def method_missing(name,*args)
233
+ if @record.respond_to?(name)
234
+ self.class.define_accessor(name)
235
+ self.send(name)
236
+ else
237
+ super
238
+ end
239
+ end
240
+
241
+ end
242
+ end