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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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