params_ready 0.0.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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/lib/arel/cte_name.rb +20 -0
  3. data/lib/params_ready.rb +36 -0
  4. data/lib/params_ready/builder.rb +140 -0
  5. data/lib/params_ready/error.rb +31 -0
  6. data/lib/params_ready/extensions/class_reader_writer.rb +33 -0
  7. data/lib/params_ready/extensions/collection.rb +43 -0
  8. data/lib/params_ready/extensions/delegation.rb +25 -0
  9. data/lib/params_ready/extensions/finalizer.rb +26 -0
  10. data/lib/params_ready/extensions/freezer.rb +49 -0
  11. data/lib/params_ready/extensions/hash.rb +46 -0
  12. data/lib/params_ready/extensions/late_init.rb +38 -0
  13. data/lib/params_ready/extensions/registry.rb +44 -0
  14. data/lib/params_ready/extensions/undefined.rb +15 -0
  15. data/lib/params_ready/format.rb +130 -0
  16. data/lib/params_ready/helpers/arel_builder.rb +68 -0
  17. data/lib/params_ready/helpers/conditional_block.rb +31 -0
  18. data/lib/params_ready/helpers/find_in_hash.rb +22 -0
  19. data/lib/params_ready/helpers/key_map.rb +176 -0
  20. data/lib/params_ready/helpers/memo.rb +42 -0
  21. data/lib/params_ready/helpers/options.rb +39 -0
  22. data/lib/params_ready/helpers/parameter_definer_class_methods.rb +39 -0
  23. data/lib/params_ready/helpers/parameter_storage_class_methods.rb +36 -0
  24. data/lib/params_ready/helpers/parameter_user_class_methods.rb +31 -0
  25. data/lib/params_ready/helpers/relation_builder_wrapper.rb +35 -0
  26. data/lib/params_ready/helpers/rule.rb +57 -0
  27. data/lib/params_ready/helpers/storage.rb +30 -0
  28. data/lib/params_ready/helpers/usage_rule.rb +18 -0
  29. data/lib/params_ready/input_context.rb +31 -0
  30. data/lib/params_ready/intent.rb +70 -0
  31. data/lib/params_ready/marshaller/array_marshallers.rb +132 -0
  32. data/lib/params_ready/marshaller/builder_module.rb +9 -0
  33. data/lib/params_ready/marshaller/collection.rb +165 -0
  34. data/lib/params_ready/marshaller/definition_module.rb +63 -0
  35. data/lib/params_ready/marshaller/hash_marshallers.rb +100 -0
  36. data/lib/params_ready/marshaller/hash_set_marshallers.rb +96 -0
  37. data/lib/params_ready/marshaller/parameter_module.rb +11 -0
  38. data/lib/params_ready/marshaller/polymorph_marshallers.rb +67 -0
  39. data/lib/params_ready/marshaller/tuple_marshallers.rb +103 -0
  40. data/lib/params_ready/ordering/column.rb +60 -0
  41. data/lib/params_ready/ordering/ordering.rb +276 -0
  42. data/lib/params_ready/output_parameters.rb +127 -0
  43. data/lib/params_ready/pagination/abstract_pagination.rb +18 -0
  44. data/lib/params_ready/pagination/cursor.rb +171 -0
  45. data/lib/params_ready/pagination/direction.rb +148 -0
  46. data/lib/params_ready/pagination/keyset_pagination.rb +254 -0
  47. data/lib/params_ready/pagination/keysets.rb +70 -0
  48. data/lib/params_ready/pagination/nulls.rb +31 -0
  49. data/lib/params_ready/pagination/offset_pagination.rb +130 -0
  50. data/lib/params_ready/pagination/tendency.rb +28 -0
  51. data/lib/params_ready/parameter/abstract_hash_parameter.rb +204 -0
  52. data/lib/params_ready/parameter/array_parameter.rb +197 -0
  53. data/lib/params_ready/parameter/definition.rb +264 -0
  54. data/lib/params_ready/parameter/hash_parameter.rb +63 -0
  55. data/lib/params_ready/parameter/hash_set_parameter.rb +101 -0
  56. data/lib/params_ready/parameter/parameter.rb +456 -0
  57. data/lib/params_ready/parameter/polymorph_parameter.rb +172 -0
  58. data/lib/params_ready/parameter/state.rb +132 -0
  59. data/lib/params_ready/parameter/tuple_parameter.rb +152 -0
  60. data/lib/params_ready/parameter/value_parameter.rb +182 -0
  61. data/lib/params_ready/parameter_definer.rb +14 -0
  62. data/lib/params_ready/parameter_user.rb +43 -0
  63. data/lib/params_ready/query/array_grouping.rb +68 -0
  64. data/lib/params_ready/query/custom_predicate.rb +102 -0
  65. data/lib/params_ready/query/exists_predicate.rb +103 -0
  66. data/lib/params_ready/query/fixed_operator_predicate.rb +77 -0
  67. data/lib/params_ready/query/grouping.rb +177 -0
  68. data/lib/params_ready/query/join_clause.rb +87 -0
  69. data/lib/params_ready/query/nullness_predicate.rb +71 -0
  70. data/lib/params_ready/query/polymorph_predicate.rb +77 -0
  71. data/lib/params_ready/query/predicate.rb +203 -0
  72. data/lib/params_ready/query/predicate_operator.rb +132 -0
  73. data/lib/params_ready/query/relation.rb +337 -0
  74. data/lib/params_ready/query/structured_grouping.rb +58 -0
  75. data/lib/params_ready/query/variable_operator_predicate.rb +125 -0
  76. data/lib/params_ready/query_context.rb +21 -0
  77. data/lib/params_ready/restriction.rb +252 -0
  78. data/lib/params_ready/result.rb +109 -0
  79. data/lib/params_ready/value/coder.rb +181 -0
  80. data/lib/params_ready/value/constraint.rb +198 -0
  81. data/lib/params_ready/value/custom.rb +56 -0
  82. data/lib/params_ready/value/validator.rb +68 -0
  83. metadata +181 -0
@@ -0,0 +1,67 @@
1
+ require_relative 'collection'
2
+
3
+ module ParamsReady
4
+ module Marshaller
5
+ class PolymorphMarshallers
6
+ class HashMarshaller
7
+ attr_reader :type_identifier
8
+
9
+ def self.instance(type_identifier:)
10
+ marshaller = new type_identifier
11
+ marshaller.freeze
12
+ [Hash, marshaller]
13
+ end
14
+
15
+ def initialize(type_identifier)
16
+ @type_identifier = type_identifier.to_sym
17
+ end
18
+
19
+ def reserved?(name)
20
+ name == type_identifier
21
+ end
22
+
23
+ def canonicalize(definition, hash, context, validator)
24
+ raise ParamsReadyError, "Type key can't be retrieved" unless hash.length == 1
25
+ key = hash.keys.first
26
+ value = hash.values.first
27
+ value = type(definition, key, value, context, validator)
28
+
29
+
30
+ [value, validator]
31
+ end
32
+
33
+ def type(definition, key, value, context, validator)
34
+ type_key = key.to_sym == type_identifier ? value : key
35
+ prototype = definition.type(type_key, context)
36
+ raise ParamsReadyError, "Unexpected type for #{definition.name}: #{type_key}" if prototype.nil?
37
+
38
+ type = prototype.create
39
+ return type if type_key == value
40
+ type.set_from_input(value, context, validator)
41
+ type
42
+ end
43
+
44
+ def marshal(parameter, intent)
45
+ type = parameter.send(:bare_value)
46
+
47
+ hash = type.to_hash_if_eligible(intent)
48
+ return hash unless hash.nil?
49
+
50
+ value = type.hash_key(intent)
51
+ { type_identifier => value }
52
+ end
53
+
54
+ freeze
55
+ end
56
+
57
+ def self.collection
58
+ @collection ||= begin
59
+ c = ClassCollection.new Hash
60
+ c.add_factory :hash, HashMarshaller
61
+ c.freeze
62
+ c
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,103 @@
1
+ require_relative 'collection'
2
+ require_relative '../extensions/undefined'
3
+ require_relative '../extensions/hash'
4
+
5
+ module ParamsReady
6
+ module Marshaller
7
+ class TupleMarshallers
8
+ module AbstractMarshaller
9
+ def self.marshal_fields(fields, intent)
10
+ fields.map do |field|
11
+ field.format(intent)
12
+ end
13
+ end
14
+
15
+ def marshal(parameter, intent)
16
+ fields = parameter.send(:bare_value)
17
+ fields = AbstractMarshaller.marshal_fields(fields, intent)
18
+ do_marshal(fields, intent)
19
+ end
20
+ end
21
+
22
+ module ArrayMarshaller
23
+ extend AbstractMarshaller
24
+
25
+ def self.canonicalize(definition, array, context, validator, freeze: false)
26
+ if array.length != definition.arity
27
+ raise ParamsReadyError, "Unexpected array length: #{array.length}"
28
+ end
29
+
30
+ canonical = definition.fields.each_with_index.map do |field_definition, index|
31
+ element = field_definition.create
32
+ element.set_from_input(array[index], context, validator)
33
+ element.freeze if freeze
34
+ element
35
+ end
36
+ [canonical, validator]
37
+ end
38
+
39
+ def self.do_marshal(fields, _)
40
+ fields
41
+ end
42
+
43
+ freeze
44
+ end
45
+
46
+ module HashMarshaller
47
+ extend AbstractMarshaller
48
+
49
+ def self.canonicalize(definition, hash, context, validator)
50
+ array = (0...definition.arity).map do |idx|
51
+ Extensions::Hash.indifferent_access(hash, idx, Extensions::Undefined)
52
+ end
53
+ ArrayMarshaller.canonicalize(definition, array, context, validator)
54
+ end
55
+
56
+ def self.do_marshal(fields, _)
57
+ fields.each_with_index.map do |field, index|
58
+ [index.to_s, field]
59
+ end.to_h
60
+ end
61
+
62
+ freeze
63
+ end
64
+
65
+ class StringMarshaller
66
+ include AbstractMarshaller
67
+
68
+ attr_reader :separator
69
+
70
+ def self.instance(separator:)
71
+ instance = new separator
72
+ [String, instance.freeze]
73
+ end
74
+
75
+ def initialize(separator)
76
+ @separator = separator.to_s.freeze
77
+ end
78
+
79
+ def canonicalize(definition, string, context, validator)
80
+ array = string.split(separator)
81
+ ArrayMarshaller.canonicalize(definition, array, context, validator)
82
+ end
83
+
84
+ def do_marshal(fields, _)
85
+ fields.join(separator)
86
+ end
87
+
88
+ freeze
89
+ end
90
+
91
+ def self.collection
92
+ @collection ||= begin
93
+ c = ClassCollection.new Array
94
+ c.add_instance Array, ArrayMarshaller
95
+ c.add_instance Hash, HashMarshaller
96
+ c.add_factory :string, StringMarshaller
97
+ c.freeze
98
+ c
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,60 @@
1
+ require_relative '../error'
2
+ require_relative '../helpers/arel_builder'
3
+
4
+ module ParamsReady
5
+ module Ordering
6
+ class Column
7
+ DIRECTIONS = Set.new %i(none asc desc)
8
+
9
+ attr_reader :ordering, :table, :nulls, :required, :pk
10
+
11
+ NULLS_FIRST = { null: 0, not_null: 1 }.freeze
12
+ NULLS_LAST = { null: 1, not_null: 0 }.freeze
13
+
14
+ def initialize(ordering, arel_table:, expression:, nulls: :default, required: false, pk: false)
15
+ raise ParamsReadyError, "Invalid ordering value: #{ordering}" unless DIRECTIONS.include? ordering
16
+ @ordering = ordering
17
+ @table = arel_table
18
+ @expression = expression
19
+ @nulls = nulls
20
+ @required = required
21
+ @pk = pk
22
+ end
23
+
24
+ def expression(name)
25
+ @expression || name
26
+ end
27
+
28
+ def attribute(name, default_table, context)
29
+ arel_table = table || default_table
30
+ arel_builder = Helpers::ArelBuilder.instance(expression(name), arel_table: arel_table)
31
+ arel_builder.to_arel(arel_table, context, self)
32
+ end
33
+
34
+ def clauses(attribute, direction, inverted: false)
35
+ clause = attribute.send direction
36
+ if nulls == :default
37
+ [clause]
38
+ else
39
+ values = null_substitution_values(nulls, inverted)
40
+
41
+ nulls_last = Arel::Nodes::Case.new
42
+ .when(attribute.eq(nil)).then(values[:null])
43
+ .else(values[:not_null])
44
+ [nulls_last, clause]
45
+ end
46
+ end
47
+
48
+ def null_substitution_values(policy, inverted)
49
+ case [policy, inverted]
50
+ when [:first, false], [:last, true]
51
+ NULLS_FIRST
52
+ when [:last, false], [:first, true]
53
+ NULLS_LAST
54
+ else
55
+ raise ParamsReadyError, "Unimplemented null handling policy: '#{nulls}' (inverted: #{inverted})"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,276 @@
1
+ require 'set'
2
+ require_relative '../parameter/definition'
3
+ require_relative '../builder'
4
+ require_relative '../parameter/parameter'
5
+ require_relative 'column'
6
+
7
+ module ParamsReady
8
+ module Ordering
9
+ class OrderingParameter < Parameter::ArrayParameter
10
+ def_delegators :definition, :required?
11
+
12
+ def marshal(intent)
13
+ arr = to_array(intent)
14
+ return arr unless intent.marshal?(name_for_formatter)
15
+
16
+ arr.join(definition.class::COLUMN_DELIMITER)
17
+ end
18
+
19
+ def to_array(intent = Intent.instance(:backend))
20
+ arr = bare_value
21
+ arr.map do |tuple|
22
+ name = tuple.first.unwrap
23
+ next unless intent.name_permitted?(name) || definition.required?(name)
24
+
25
+ tuple.format(intent)
26
+ end.compact
27
+ end
28
+
29
+ def by_columns
30
+ bare_value.each_with_object(Hash.new(:none)) do |tuple, hash|
31
+ hash[tuple[0].unwrap] = tuple[1].unwrap
32
+ end
33
+ end
34
+
35
+ def toggle_schema(schema)
36
+ case schema
37
+ when :desc
38
+ [:desc, :asc]
39
+ when :asc
40
+ [:asc, :desc]
41
+ else
42
+ [:none, :none]
43
+ end
44
+ end
45
+
46
+ def filtered(name)
47
+ bare_value.map do |tuple|
48
+ tuple.format(Intent.instance(:backend))
49
+ end.partition do |item|
50
+ next item[0] == name
51
+ end
52
+ end
53
+
54
+ def prepend_item(name, direction, array)
55
+ return array if direction == :none
56
+ [[name, direction], *array]
57
+ end
58
+
59
+ def inverted_order_value
60
+ bare_value.map do |tuple|
61
+ name = tuple.first.unwrap
62
+ case tuple.second.unwrap
63
+ when :asc
64
+ [name, :desc]
65
+ when :desc
66
+ [name, :asc]
67
+ else
68
+ raise ParamsReadyError, "Unexpected ordering: '#{tuple.second.unwrap}'"
69
+ end
70
+ end
71
+ end
72
+
73
+ def inverted_order
74
+ update_in(inverted_order_value, [])
75
+ end
76
+
77
+ def toggled_order_value(name)
78
+ drop, save = filtered(name)
79
+
80
+ old_dir = drop.count > 0 ? drop.first[1] : :none
81
+ primary, secondary = toggle_schema(definition.columns[name.to_sym].ordering)
82
+ new_dir = old_dir == primary ? secondary : primary
83
+
84
+ prepend_item(name, new_dir, save)
85
+ end
86
+
87
+ def toggled_order(name)
88
+ update_in(toggled_order_value(name), [])
89
+ end
90
+
91
+ def reordered_value(name, new_dir)
92
+ _, save = filtered(name)
93
+ prepend_item(name, new_dir, save)
94
+ end
95
+
96
+ def reordered(name, new_dir)
97
+ update_in(reordered_value(name, new_dir), [])
98
+ end
99
+
100
+ def restriction_from_context(context)
101
+ restriction = context.to_restriction
102
+ return restriction if restriction.name_permitted? :ordering || !required?
103
+
104
+ Restriction.permit({ ordering: [] })
105
+ end
106
+
107
+ def to_arel(default_table, context: Restriction.blanket_permission, inverted: false)
108
+ ordering = inverted ? inverted_order : self
109
+ ordering.to_array_with_context(context).flat_map do |(column_name, direction)|
110
+ column = definition.columns[column_name]
111
+ attribute = column.attribute(column_name, default_table, context)
112
+ column.clauses(attribute, direction, inverted: inverted)
113
+ end
114
+ end
115
+
116
+ def to_array_with_context(context)
117
+ intent = Intent.instance(:backend).clone(restriction: restriction_from_context(context))
118
+ to_array(intent.for_children(self))
119
+ end
120
+
121
+ def order_for(name)
122
+ order = bare_value.find do |tuple|
123
+ tuple[0].unwrap == name
124
+ end
125
+ return :none if order.nil?
126
+
127
+ order[1].unwrap
128
+ end
129
+ end
130
+
131
+ class OrderingParameterBuilder < Builder
132
+ def self.instance
133
+ new OrderingParameterDefinition.new({})
134
+ end
135
+
136
+ def column(name, ordering, arel_table: nil, expression: nil, nulls: :default, required: false, pk: false)
137
+ @definition.add_column(
138
+ name,
139
+ ordering,
140
+ arel_table: arel_table,
141
+ expression: expression,
142
+ nulls: nulls,
143
+ required: required,
144
+ pk: pk
145
+ )
146
+ end
147
+
148
+ def default(*array)
149
+ super array
150
+ end
151
+ end
152
+
153
+ class OrderingParameterDefinition < Parameter::ArrayParameterDefinition
154
+ COLUMN_DELIMITER = '|'
155
+ FIELD_DELIMITER = '-'
156
+ attr_reader :columns, :primary_keys
157
+
158
+ parameter_class OrderingParameter
159
+
160
+ def initialize(columns, default = Extensions::Undefined)
161
+ invalid = columns.values.uniq.reject do |column|
162
+ column.is_a? Column
163
+ end
164
+ raise ParamsReadyError, "Invalid ordering values: #{invalid.join(", ")}" unless invalid.length == 0
165
+ @columns = columns.transform_keys { |k| k.to_sym }
166
+ @required_columns = nil
167
+ @primary_keys = Set.new
168
+ super :ordering, altn: :ord, prototype: nil, default: default
169
+ end
170
+
171
+ def set_default(value)
172
+ raise ParamsReadyError, "Prototype for ordering expected to be nil" unless @prototype.nil?
173
+ set_required_columns
174
+
175
+ @prototype = create_prototype columns
176
+ super value
177
+ end
178
+
179
+ def set_required_columns
180
+ return unless @required_columns.nil?
181
+
182
+ @required_columns = @columns.select do |_name, value|
183
+ value.required
184
+ end.map do |name, _value|
185
+ name
186
+ end
187
+ end
188
+
189
+ def required?(name = nil)
190
+ return !@required_columns.empty? if name.nil?
191
+
192
+ @required_columns.member?(name)
193
+ end
194
+
195
+ def add_column(
196
+ name,
197
+ ordering,
198
+ expression:,
199
+ arel_table:,
200
+ nulls: :default,
201
+ required: false,
202
+ pk: false
203
+ )
204
+ raise ParamsReadyError, "Column name taken: #{name}" if @columns.key? name
205
+ raise ParamsReadyError, "Can't add column after default defined" unless @default == Extensions::Undefined
206
+ @primary_keys << name if pk == true
207
+
208
+ column = Column.new(
209
+ ordering,
210
+ expression: expression,
211
+ arel_table: arel_table,
212
+ nulls: nulls,
213
+ required: required,
214
+ pk: pk
215
+ )
216
+ @columns[name] = column
217
+ end
218
+
219
+ def create_prototype(columns)
220
+ Builder.define_tuple(:column) do
221
+ marshal using: :string, separator: FIELD_DELIMITER
222
+ field :symbol, :column_name do
223
+ constrain Value::EnumConstraint.new(columns.keys)
224
+ end
225
+ field :symbol, :column_ordering do
226
+ constrain Value::EnumConstraint.new(Column::DIRECTIONS)
227
+ end
228
+ end
229
+ end
230
+
231
+ def try_canonicalize(input, context, validator = nil, freeze: false)
232
+ input ||= [%w(none none)]
233
+ canonical, validator = case input
234
+ when String
235
+ raise ParamsReadyError, "Freeze option expected to be false" if freeze
236
+ array = input.split(COLUMN_DELIMITER)
237
+ try_canonicalize(array, context, validator, freeze: false)
238
+ when Array
239
+ super
240
+ else
241
+ raise ParamsReadyError, "Unexpected type for #{name}: #{input.class.name}"
242
+ end
243
+ unique_columns = unique_columns(canonical)
244
+ with_required = with_required(unique_columns)
245
+ [with_required.values, validator]
246
+ end
247
+
248
+ def unique_columns(array)
249
+ array.each_with_object({}) do |column, hash|
250
+ name = column.first.unwrap
251
+ next if hash.key? name
252
+
253
+ hash[name] = column
254
+ end
255
+ end
256
+
257
+ def with_required(hash)
258
+ @required_columns.each_with_object(hash) do |name, result|
259
+ next if result.key? name
260
+ column = @columns[name]
261
+ _, tuple = @prototype.from_input([name, column.ordering])
262
+ hash[name] = tuple
263
+ end
264
+ end
265
+
266
+ def finish
267
+ raise ParamsReadyError, "No ordering column defined" if @columns.empty?
268
+ set_required_columns
269
+ set_default([]) unless default_defined?
270
+ super
271
+ end
272
+
273
+ freeze_variables :columns, :required_columns, :primary_keys, :prototype
274
+ end
275
+ end
276
+ end