prato 0.1.0

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 (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +938 -0
  5. data/lib/prato/configuration.rb +99 -0
  6. data/lib/prato/internal/active_record_version.rb +24 -0
  7. data/lib/prato/internal/join_helper.rb +48 -0
  8. data/lib/prato/internal/join_helper_legacy.rb +171 -0
  9. data/lib/prato/internal/lazy_loader_cache.rb +25 -0
  10. data/lib/prato/internal/pipeline/filtering.rb +277 -0
  11. data/lib/prato/internal/pipeline/pagination.rb +30 -0
  12. data/lib/prato/internal/pipeline/serializer.rb +87 -0
  13. data/lib/prato/internal/pipeline/sorting.rb +78 -0
  14. data/lib/prato/internal/query_executor.rb +105 -0
  15. data/lib/prato/internal/query_state.rb +90 -0
  16. data/lib/prato/internal/specification.rb +101 -0
  17. data/lib/prato/internal/specification_builder.rb +361 -0
  18. data/lib/prato/internal/sql_support.rb +118 -0
  19. data/lib/prato/query/and_filter.rb +13 -0
  20. data/lib/prato/query/default_parser.rb +148 -0
  21. data/lib/prato/query/field_resolver.rb +23 -0
  22. data/lib/prato/query/filter.rb +15 -0
  23. data/lib/prato/query/or_filter.rb +13 -0
  24. data/lib/prato/query/parameters.rb +17 -0
  25. data/lib/prato/query/sort.rb +14 -0
  26. data/lib/prato/table.rb +39 -0
  27. data/lib/prato/table_builder.rb +40 -0
  28. data/lib/prato/types/aggregate_column.rb +93 -0
  29. data/lib/prato/types/association_column.rb +37 -0
  30. data/lib/prato/types/direct_column.rb +27 -0
  31. data/lib/prato/types/expression_column.rb +38 -0
  32. data/lib/prato/types/ruby_column.rb +31 -0
  33. data/lib/prato/version.rb +5 -0
  34. data/lib/prato.rb +66 -0
  35. metadata +96 -0
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Internal
5
+ class SpecificationBuilder
6
+ attr_reader :draft_columns, :ruby_loaders
7
+
8
+ def initialize
9
+ @config = Prato::Configuration.config
10
+ @draft_columns = []
11
+ @ruby_loaders = nil
12
+ end
13
+
14
+ AGGREGATE_FUNCTIONS = [:count, :sum, :avg, :min, :max].freeze
15
+ COMMON_RESERVED_KEYWORDS = [:queryable].freeze
16
+ RESERVED_COLUMN_SYMBOLS = ([:format, :expression, :filter] + COMMON_RESERVED_KEYWORDS + AGGREGATE_FUNCTIONS).freeze
17
+ RESERVED_RUBY_COLUMN_SYMBOLS = ([:key, :filter, :includes] + COMMON_RESERVED_KEYWORDS).freeze
18
+
19
+ def inner_column(*args, **kwargs)
20
+ draft = build_draft(args, kwargs)
21
+ @draft_columns << draft
22
+ end
23
+
24
+ def inner_query_column(*args, **kwargs)
25
+ draft = build_draft(args, kwargs, query_only: true)
26
+ @draft_columns << draft
27
+ end
28
+
29
+ def inner_ruby_column(*args, **kwargs, &block)
30
+ name_map, options = extract_name_and_options(args, kwargs, RESERVED_RUBY_COLUMN_SYMBOLS)
31
+ display_name, loader_id = parse_name_map(name_map)
32
+
33
+ key = parse_accessor(options[:key])
34
+ queryable = resolve_queryable_option!(options, VALID_COLUMN_QUERYABLE)
35
+ validate_filter_option!(options[:filter])
36
+ column = ::Prato::Types::RubyColumn.new(loader_id, key: key, filter: options[:filter], includes: options[:includes])
37
+
38
+ @draft_columns << DraftColumn.new(display_name, loader_id, column, queryable: queryable)
39
+ inner_ruby_loader(loader_id, &block) if block_given?
40
+ end
41
+
42
+ def inner_section(id, &block)
43
+ section = SectionBuilder.new
44
+
45
+ raise ArgumentError, "No block given to section" unless block_given?
46
+
47
+ section.instance_exec(&block)
48
+
49
+ section.spec.draft_columns.each do |nested_draft|
50
+ new_output_path = [id] + nested_draft.output_paths
51
+
52
+ draft = DraftColumn.new(
53
+ nested_draft.override_name,
54
+ nested_draft.accessor_name,
55
+ nested_draft.column,
56
+ queryable: nested_draft.queryable,
57
+ query_only: nested_draft.query_only,
58
+ output_paths: new_output_path
59
+ )
60
+
61
+ @draft_columns << draft
62
+ end
63
+ return if section.spec.ruby_loaders.nil?
64
+
65
+ @ruby_loaders ||= {}
66
+ @ruby_loaders.merge!(section.spec.ruby_loaders)
67
+ end
68
+
69
+ def inner_ruby_loader(id, includes: nil, &block)
70
+ @ruby_loaders ||= {}
71
+ @ruby_loaders[id] = {
72
+ block: block,
73
+ includes: includes
74
+ }
75
+ end
76
+
77
+ def inner_config(config)
78
+ @config = config
79
+ end
80
+
81
+ def build(base_model)
82
+ columns = {}
83
+ visible_fields = []
84
+ filterable_fields = Set.new
85
+ sortable_fields = Set.new
86
+ output_paths = {}
87
+ field_lookup = {}
88
+
89
+ @draft_columns.each do |draft|
90
+ column_output_path = draft.output_paths.map do |path|
91
+ transform_key_part(path, @config.key_transformation)
92
+ end
93
+ internal_column_name = Query::FieldResolver.join(draft.output_paths)
94
+
95
+ if columns.key?(internal_column_name)
96
+ raise ArgumentError,
97
+ "Column '#{draft.name}' (internal id: #{internal_column_name}) has already been defined."
98
+ end
99
+
100
+ column = draft.column
101
+
102
+ if column.is_a?(Types::DirectColumn) ||
103
+ column.is_a?(Types::AssociationColumn) ||
104
+ column.is_a?(Types::ExpressionColumn) ||
105
+ column.is_a?(Types::AggregateColumn)
106
+ column.resolve_arel!(base_model, internal_column_name)
107
+ end
108
+
109
+ filterable = resolve_capability(:filter, draft)
110
+ sortable = resolve_capability(:sort, draft)
111
+
112
+ columns[internal_column_name] = column
113
+ visible_fields << internal_column_name unless draft.query_only
114
+ filterable_fields << internal_column_name if filterable
115
+ sortable_fields << internal_column_name if sortable
116
+ output_paths[internal_column_name] = column_output_path.map(&:to_sym)
117
+ field_lookup[column_output_path.map(&:to_s)] = internal_column_name
118
+ end
119
+
120
+ Specification.new(
121
+ columns: columns,
122
+ visible_fields: visible_fields,
123
+ filterable_fields: filterable_fields,
124
+ sortable_fields: sortable_fields,
125
+ output_paths: output_paths,
126
+ field_lookup: field_lookup,
127
+ ruby_loaders: @ruby_loaders,
128
+ config: @config
129
+ )
130
+ end
131
+
132
+ private
133
+
134
+ def resolve_capability(capability, draft)
135
+ return true if capability == :filter && !draft.column.filter.nil?
136
+
137
+ queryable = resolve_queryable(draft)
138
+
139
+ queryable == :all || queryable == capability
140
+ end
141
+
142
+ def resolve_queryable(draft)
143
+ return draft.queryable if draft.queryable
144
+ return :all if draft.query_only
145
+
146
+ if draft.column.is_a?(Types::RubyColumn)
147
+ @config.default_ruby_column_queryable
148
+ else
149
+ @config.default_queryable
150
+ end
151
+ end
152
+
153
+ def validate_ruby_loader!(draft, loaders)
154
+ column = draft.column
155
+ return unless column.is_a?(Prato::Types::RubyColumn)
156
+
157
+ loader_name = column.loader
158
+
159
+ raise ArgumentError, "Ruby column '#{draft.name || column.key}' is missing a loader." if loader_name.nil?
160
+ raise ArgumentError, "No ruby loader registered for '#{loader_name}'." if loaders.nil?
161
+
162
+ loader = loaders[loader_name]
163
+ raise ArgumentError, "No ruby loader registered for '#{loader_name}'." unless loaders.key?(loader_name)
164
+ return if loader.is_a?(Hash) && loader[:block].respond_to?(:call)
165
+
166
+ raise ArgumentError, "Ruby loader '#{loader_name}' must respond to #call."
167
+ end
168
+
169
+ VALID_COLUMN_QUERYABLE = [:all, :none, :filter, :sort].freeze
170
+ VALID_QUERY_COLUMN_QUERYABLE = [:all, :filter, :sort].freeze
171
+
172
+ def build_draft(args, kwargs, query_only: false)
173
+ queryable_options = {}
174
+ queryable_options[:queryable] = kwargs.delete(:queryable) if kwargs.key?(:queryable)
175
+ queryable = resolve_queryable_option!(queryable_options,
176
+ query_only ? VALID_QUERY_COLUMN_QUERYABLE : VALID_COLUMN_QUERYABLE)
177
+
178
+ name_map, options = extract_name_and_options(args, kwargs, RESERVED_COLUMN_SYMBOLS)
179
+ aggregate_function, aggregate_accessor = extract_aggregate(options)
180
+ override_name, accessor = parse_name_map(name_map)
181
+ accessor = parse_accessor(accessor)
182
+ validate_filter_option!(options[:filter])
183
+
184
+ if aggregate_function
185
+ column = ::Prato::Types::AggregateColumn.new(aggregate_function, aggregate_accessor,
186
+ format: options[:format], filter: options[:filter])
187
+ DraftColumn.new(accessor, nil, column, queryable: queryable, query_only: query_only)
188
+ elsif options[:expression]
189
+ column = ::Prato::Types::ExpressionColumn.new(options[:expression], format: options[:format], filter: options[:filter])
190
+ DraftColumn.new(override_name, accessor, column, queryable: queryable, query_only: query_only)
191
+ elsif accessor.is_a?(Array) && accessor.length > 1
192
+ column = ::Prato::Types::AssociationColumn.new(accessor, format: options[:format], filter: options[:filter])
193
+ DraftColumn.new(override_name, accessor, column, queryable: queryable, query_only: query_only)
194
+ else
195
+ column = ::Prato::Types::DirectColumn.new(accessor, format: options[:format], filter: options[:filter])
196
+ DraftColumn.new(override_name, accessor, column, queryable: queryable, query_only: query_only)
197
+ end
198
+ end
199
+
200
+ def validate_filter_option!(filter)
201
+ return if filter.nil? || filter.is_a?(Proc)
202
+ return if filter.is_a?(Array) && filter.all? { |operator| operator.is_a?(Symbol) }
203
+
204
+ raise ArgumentError, "filter must be nil, an Array of symbols, or a Proc"
205
+ end
206
+
207
+ def resolve_queryable_option!(options, valid)
208
+ return nil unless options.key?(:queryable)
209
+
210
+ queryable = options[:queryable]
211
+ raise ArgumentError, "queryable: must be a Symbol, got #{queryable.class}" unless queryable.is_a?(Symbol)
212
+
213
+ unless valid.include?(queryable)
214
+ raise ArgumentError, "queryable: must be one of #{valid.map(&:inspect).join(", ")}, got #{queryable.inspect}"
215
+ end
216
+
217
+ queryable
218
+ end
219
+
220
+ def extract_name_and_options(args, kwargs, reserved)
221
+ if args.any?
222
+ [args, kwargs]
223
+ else
224
+ option_keys = kwargs.keys & reserved
225
+ options = kwargs.slice(*option_keys)
226
+ name_map = kwargs.except(*reserved)
227
+ [name_map, options]
228
+ end
229
+ end
230
+
231
+ def extract_aggregate(kwargs)
232
+ AGGREGATE_FUNCTIONS.each do |func|
233
+ value = kwargs.delete(func)
234
+ return [func, value] if value
235
+ end
236
+ nil
237
+ end
238
+
239
+ def parse_name_map(name_map)
240
+ case name_map
241
+ when Symbol
242
+ [nil, name_map]
243
+ when Array
244
+ if name_map.size == 1 && name_map.first.is_a?(Hash)
245
+ parse_name_map(name_map.first)
246
+ elsif name_map.size == 2
247
+ [name_map.first, name_map.second]
248
+ else
249
+ [nil, name_map.first]
250
+ end
251
+ when Hash
252
+ entry = name_map.first
253
+ [entry.first, entry.second]
254
+ else
255
+ raise ArgumentError, "name_map must be a Symbol or Array"
256
+ end
257
+ end
258
+
259
+ def parse_accessor(value)
260
+ case value
261
+ when Symbol
262
+ value
263
+ when Hash
264
+ flatten_hash_to_array(value)
265
+ else
266
+ value
267
+ end
268
+ end
269
+
270
+ def flatten_hash_to_array(hash)
271
+ return [] if hash.empty?
272
+
273
+ key, value = hash.first
274
+ case value
275
+ when Symbol
276
+ [key, value]
277
+ when Hash
278
+ [key] + flatten_hash_to_array(value)
279
+ else
280
+ [key]
281
+ end
282
+ end
283
+
284
+ def transform_key_part(part, transformation)
285
+ return part if part.is_a?(String)
286
+
287
+ case transformation
288
+ when :camelCase then to_camel_case(part)
289
+ when :snake_case then to_snake_case(part)
290
+ when :none then part
291
+ end
292
+ end
293
+
294
+ def to_snake_case(value)
295
+ s = value.to_s.dup
296
+ s.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
297
+ s.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
298
+ s.tr!("-", "_")
299
+ s.tr!(" ", "_")
300
+ s.downcase!
301
+ s
302
+ end
303
+
304
+ def to_camel_case(value)
305
+ parts = to_snake_case(value).split("_")
306
+ parts.first + parts.drop(1).map(&:capitalize).join
307
+ end
308
+
309
+ private_constant :RESERVED_COLUMN_SYMBOLS
310
+ private_constant :RESERVED_RUBY_COLUMN_SYMBOLS
311
+ private_constant :COMMON_RESERVED_KEYWORDS
312
+ private_constant :AGGREGATE_FUNCTIONS
313
+ private_constant :VALID_COLUMN_QUERYABLE
314
+ private_constant :VALID_QUERY_COLUMN_QUERYABLE
315
+ end
316
+
317
+ class DraftColumn
318
+ attr_reader :override_name, :accessor_name, :column, :queryable, :query_only, :output_paths
319
+
320
+ def initialize(override_name, accessor_name, column, queryable: nil, query_only: false,
321
+ output_paths: [])
322
+ @override_name = override_name
323
+ @accessor_name = accessor_name
324
+ @column = column
325
+ @queryable = queryable
326
+ @query_only = query_only
327
+ @output_paths = output_paths.empty? ? [name] : output_paths
328
+ end
329
+
330
+ def name
331
+ @override_name || @accessor_name
332
+ end
333
+ end
334
+
335
+ class SectionBuilder
336
+ attr_reader :spec
337
+
338
+ def initialize
339
+ @spec = SpecificationBuilder.new
340
+ end
341
+
342
+ def column(*args, **kwargs)
343
+ @spec.inner_column(*args, **kwargs)
344
+ self
345
+ end
346
+
347
+ def ruby_column(*args, **kwargs, &block)
348
+ @spec.inner_ruby_column(*args, **kwargs, &block)
349
+ self
350
+ end
351
+
352
+ def section(id, &block)
353
+ @spec.inner_section(id, &block)
354
+ self
355
+ end
356
+ end
357
+
358
+ private_constant :DraftColumn
359
+ private_constant :SectionBuilder
360
+ end
361
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Internal
5
+ module SqlSupport
6
+ extend self
7
+
8
+ def table_for(scope, association_path)
9
+ return scope.model.arel_table if association_path.empty?
10
+
11
+ expanded = expand_through_associations(scope.model, association_path)
12
+ join_sources = scope.arel.join_sources
13
+
14
+ table = walk_join_path(scope.model, scope.model.arel_table, expanded, join_sources)
15
+
16
+ unless table
17
+ raise ArgumentError,
18
+ "Unable to resolve table alias for association path #{association_path.inspect}"
19
+ end
20
+
21
+ table
22
+ end
23
+
24
+ private
25
+
26
+ def expand_through_associations(model, path)
27
+ expanded = []
28
+ current_model = model
29
+
30
+ path.each do |assoc_name|
31
+ reflection = current_model.reflect_on_association(assoc_name)
32
+ raise ArgumentError, "Unknown association '#{assoc_name}' on #{current_model}" unless reflection
33
+
34
+ expand_reflection(reflection, expanded)
35
+ current_model = reflection.klass
36
+ end
37
+
38
+ expanded
39
+ end
40
+
41
+ def expand_reflection(reflection, result)
42
+ if reflection.through_reflection?
43
+ expand_reflection(reflection.through_reflection, result)
44
+ expand_reflection(reflection.source_reflection, result)
45
+ else
46
+ result << reflection.name
47
+ end
48
+ end
49
+
50
+ def walk_join_path(model, parent_table, path, join_sources)
51
+ return parent_table if path.empty?
52
+
53
+ assoc_name, *rest = path
54
+ reflection = model.reflect_on_association(assoc_name)
55
+ return nil unless reflection
56
+
57
+ fk = reflection.foreign_key.to_s
58
+ target_table_name = reflection.klass.table_name
59
+ parent_id = table_identifier(parent_table)
60
+
61
+ join_sources.each do |join|
62
+ joined_table = join.left
63
+ next unless base_table_name(joined_table) == target_table_name
64
+ next unless on_has_fk?(join.right.expr, fk)
65
+ next unless on_references?(join.right.expr, parent_id)
66
+
67
+ result = walk_join_path(reflection.klass, joined_table, rest, join_sources)
68
+ return result if result
69
+ end
70
+
71
+ nil
72
+ end
73
+
74
+ def base_table_name(table)
75
+ case table
76
+ when Arel::Nodes::TableAlias then table.left.name
77
+ when Arel::Table then table.name
78
+ end
79
+ end
80
+
81
+ def table_identifier(table)
82
+ case table
83
+ when Arel::Nodes::TableAlias then table.right.to_s
84
+ when Arel::Table then table.name
85
+ end
86
+ end
87
+
88
+ def on_has_fk?(expr, fk)
89
+ each_equality(expr).any? do |eq|
90
+ attr_named?(eq.left, fk) || attr_named?(eq.right, fk)
91
+ end
92
+ end
93
+
94
+ def on_references?(expr, parent_id)
95
+ each_equality(expr).any? do |eq|
96
+ attr_from_table?(eq.left, parent_id) || attr_from_table?(eq.right, parent_id)
97
+ end
98
+ end
99
+
100
+ def attr_named?(node, name)
101
+ node.respond_to?(:name) && node.name.to_s == name
102
+ end
103
+
104
+ def attr_from_table?(node, table_id)
105
+ node.respond_to?(:relation) && table_identifier(node.relation) == table_id
106
+ end
107
+
108
+ def each_equality(expr, &block)
109
+ return enum_for(:each_equality, expr) unless block
110
+
111
+ case expr
112
+ when Arel::Nodes::Equality then yield expr
113
+ when Arel::Nodes::And then expr.children.each { |c| each_equality(c, &block) }
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Query
5
+ class AndFilter
6
+ attr_reader :filters
7
+
8
+ def initialize(filters)
9
+ @filters = filters
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Prato
6
+ module Query
7
+ class DefaultParser
8
+ def parse_parameters(input, field_lookup)
9
+ page = extract_page(input)
10
+ per_page = extract_per_page(input)
11
+ filters = extract_filters(input)
12
+ sorts = extract_sorts(input)
13
+ fields = extract_fields(input)
14
+
15
+ Prato::Query::Parameters.new(
16
+ page: parse_page(page),
17
+ per_page: parse_per_page(per_page),
18
+ filters: parse_filters(filters, field_lookup),
19
+ sorts: parse_sorts(sorts, field_lookup),
20
+ fields: parse_fields(fields, field_lookup)
21
+ )
22
+ end
23
+
24
+ def extract_page(input)
25
+ hash_access(input, "page")
26
+ end
27
+
28
+ def parse_page(raw_value)
29
+ safe_parse_integer(raw_value)
30
+ end
31
+
32
+ def extract_per_page(input)
33
+ hash_access(input, "per_page")
34
+ end
35
+
36
+ def parse_per_page(raw_value)
37
+ safe_parse_integer(raw_value)
38
+ end
39
+
40
+ def extract_filters(input)
41
+ hash_access(input, "filters")
42
+ end
43
+
44
+ def parse_filters(input, field_lookup)
45
+ return nil if input.nil?
46
+
47
+ entries = normalize_entries_to_hash(input)
48
+ filters = parse_filter_entries(entries, field_lookup)
49
+ filters.nil? || filters.empty? ? nil : filters
50
+ end
51
+
52
+ def parse_filter_entries(entries, field_lookup, depth = 0)
53
+ raise ArgumentError, "Filter nesting too deep (maximum depth: 10)" if depth == 10
54
+
55
+ Array.wrap(entries).map do |entry|
56
+ if hash_access(entry, "or")
57
+ nested = parse_filter_entries(hash_access(entry, "or"), field_lookup, depth + 1)
58
+ next if nested.nil? || nested.empty?
59
+
60
+ Prato::Query::OrFilter.new(nested)
61
+ elsif hash_access(entry, "and")
62
+ nested = parse_filter_entries(hash_access(entry, "and"), field_lookup, depth + 1)
63
+ next if nested.nil? || nested.empty?
64
+
65
+ Prato::Query::AndFilter.new(nested)
66
+ else
67
+ field = hash_access(entry, "field")
68
+ operator = hash_access(entry, "operator")
69
+ value = hash_access(entry, "value")
70
+
71
+ Prato::Query::Filter.new(
72
+ parse_field(field, field_lookup),
73
+ operator.to_sym,
74
+ value
75
+ )
76
+ end
77
+ end.compact
78
+ end
79
+
80
+ def extract_sorts(input)
81
+ hash_access(input, "sorts")
82
+ end
83
+
84
+ def parse_sorts(input, field_lookup)
85
+ return nil if input.nil?
86
+
87
+ entries = normalize_entries_to_hash(input)
88
+
89
+ Array.wrap(entries).map do |entry|
90
+ field = hash_access(entry, "field")
91
+ order = hash_access(entry, "order")
92
+ is_desc = %w[desc descending].include?(order)
93
+
94
+ Prato::Query::Sort.new(parse_field(field, field_lookup), is_desc)
95
+ end
96
+ end
97
+
98
+ def extract_fields(input)
99
+ hash_access(input, "fields")
100
+ end
101
+
102
+ def parse_fields(input, field_lookup)
103
+ return nil if input.nil?
104
+
105
+ entries = normalize_entries_to_hash(input)
106
+
107
+ Array.wrap(entries).map do |entry|
108
+ parse_field(entry, field_lookup)
109
+ end
110
+ end
111
+
112
+ def parse_field(field, field_resolver)
113
+ fields = field.split(".")
114
+ field_resolver.call(fields)
115
+ end
116
+
117
+ def safe_parse_integer(number)
118
+ return nil if number.nil?
119
+
120
+ begin
121
+ Integer(number)
122
+ rescue ArgumentError
123
+ nil
124
+ end
125
+ end
126
+
127
+ def normalize_entries_to_hash(input)
128
+ case input
129
+ when String
130
+ JSON.parse(input)
131
+ when Array, Hash
132
+ input
133
+ else
134
+ raise ArgumentError, "Invalid filters type: #{input.class}"
135
+ end
136
+ end
137
+
138
+ def hash_access(entry, key)
139
+ value = entry[key]
140
+ if value.nil?
141
+ entry[key.to_sym]
142
+ else
143
+ value
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Query
5
+ # Module that handles converting user input fields into internal fields.
6
+ module FieldResolver
7
+ extend self
8
+
9
+ SEPARATOR = "___".freeze
10
+
11
+ def join(parts)
12
+ parts = Array(parts)
13
+ parts.length == 1 ? parts.first.to_sym : parts.map(&:to_s).join(SEPARATOR).to_sym
14
+ end
15
+
16
+ def resolve_context(field_lookup)
17
+ ->(fields) do
18
+ field_lookup[fields]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Query
5
+ class Filter
6
+ attr_reader :field, :operator, :value
7
+
8
+ def initialize(field, operator, value)
9
+ @field = field
10
+ @operator = operator
11
+ @value = value
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prato
4
+ module Query
5
+ class OrFilter
6
+ attr_reader :filters
7
+
8
+ def initialize(filters)
9
+ @filters = filters
10
+ end
11
+ end
12
+ end
13
+ end