params_ready_rails5 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/lib/arel/cte_name.rb +20 -0
  3. data/lib/params_ready/builder.rb +161 -0
  4. data/lib/params_ready/error.rb +31 -0
  5. data/lib/params_ready/extensions/class_reader_writer.rb +33 -0
  6. data/lib/params_ready/extensions/collection.rb +43 -0
  7. data/lib/params_ready/extensions/delegation.rb +25 -0
  8. data/lib/params_ready/extensions/finalizer.rb +26 -0
  9. data/lib/params_ready/extensions/freezer.rb +49 -0
  10. data/lib/params_ready/extensions/hash.rb +46 -0
  11. data/lib/params_ready/extensions/late_init.rb +38 -0
  12. data/lib/params_ready/extensions/registry.rb +44 -0
  13. data/lib/params_ready/extensions/undefined.rb +23 -0
  14. data/lib/params_ready/format.rb +132 -0
  15. data/lib/params_ready/helpers/arel_builder.rb +68 -0
  16. data/lib/params_ready/helpers/callable.rb +14 -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/interface_definer.rb +48 -0
  20. data/lib/params_ready/helpers/key_map.rb +176 -0
  21. data/lib/params_ready/helpers/memo.rb +41 -0
  22. data/lib/params_ready/helpers/options.rb +107 -0
  23. data/lib/params_ready/helpers/parameter_definer_class_methods.rb +39 -0
  24. data/lib/params_ready/helpers/parameter_storage_class_methods.rb +63 -0
  25. data/lib/params_ready/helpers/parameter_user_class_methods.rb +35 -0
  26. data/lib/params_ready/helpers/relation_builder_wrapper.rb +35 -0
  27. data/lib/params_ready/helpers/rule.rb +76 -0
  28. data/lib/params_ready/helpers/storage.rb +30 -0
  29. data/lib/params_ready/helpers/usage_rule.rb +36 -0
  30. data/lib/params_ready/input_context.rb +31 -0
  31. data/lib/params_ready/intent.rb +70 -0
  32. data/lib/params_ready/marshaller/array_marshallers.rb +132 -0
  33. data/lib/params_ready/marshaller/builder_module.rb +9 -0
  34. data/lib/params_ready/marshaller/collection.rb +165 -0
  35. data/lib/params_ready/marshaller/definition_module.rb +63 -0
  36. data/lib/params_ready/marshaller/enum_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/struct_marshallers.rb +100 -0
  40. data/lib/params_ready/marshaller/tuple_marshallers.rb +103 -0
  41. data/lib/params_ready/ordering/column.rb +60 -0
  42. data/lib/params_ready/ordering/ordering.rb +276 -0
  43. data/lib/params_ready/output_parameters.rb +138 -0
  44. data/lib/params_ready/pagination/abstract_pagination.rb +18 -0
  45. data/lib/params_ready/pagination/cursor.rb +171 -0
  46. data/lib/params_ready/pagination/direction.rb +148 -0
  47. data/lib/params_ready/pagination/keyset_pagination.rb +254 -0
  48. data/lib/params_ready/pagination/keysets.rb +70 -0
  49. data/lib/params_ready/pagination/nulls.rb +31 -0
  50. data/lib/params_ready/pagination/offset_pagination.rb +130 -0
  51. data/lib/params_ready/pagination/tendency.rb +28 -0
  52. data/lib/params_ready/parameter/abstract_struct_parameter.rb +204 -0
  53. data/lib/params_ready/parameter/array_parameter.rb +197 -0
  54. data/lib/params_ready/parameter/definition.rb +272 -0
  55. data/lib/params_ready/parameter/enum_set_parameter.rb +102 -0
  56. data/lib/params_ready/parameter/parameter.rb +475 -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/struct_parameter.rb +64 -0
  60. data/lib/params_ready/parameter/tuple_parameter.rb +152 -0
  61. data/lib/params_ready/parameter/value_parameter.rb +186 -0
  62. data/lib/params_ready/parameter_definer.rb +14 -0
  63. data/lib/params_ready/parameter_user.rb +35 -0
  64. data/lib/params_ready/query/array_grouping.rb +68 -0
  65. data/lib/params_ready/query/custom_predicate.rb +102 -0
  66. data/lib/params_ready/query/exists_predicate.rb +103 -0
  67. data/lib/params_ready/query/fixed_operator_predicate.rb +77 -0
  68. data/lib/params_ready/query/grouping.rb +177 -0
  69. data/lib/params_ready/query/join_clause.rb +87 -0
  70. data/lib/params_ready/query/nullness_predicate.rb +71 -0
  71. data/lib/params_ready/query/polymorph_predicate.rb +77 -0
  72. data/lib/params_ready/query/predicate.rb +203 -0
  73. data/lib/params_ready/query/predicate_operator.rb +132 -0
  74. data/lib/params_ready/query/relation.rb +337 -0
  75. data/lib/params_ready/query/structured_grouping.rb +58 -0
  76. data/lib/params_ready/query/variable_operator_predicate.rb +125 -0
  77. data/lib/params_ready/query_context.rb +21 -0
  78. data/lib/params_ready/restriction.rb +252 -0
  79. data/lib/params_ready/result.rb +109 -0
  80. data/lib/params_ready/value/coder.rb +210 -0
  81. data/lib/params_ready/value/constraint.rb +198 -0
  82. data/lib/params_ready/value/custom.rb +56 -0
  83. data/lib/params_ready/value/validator.rb +81 -0
  84. data/lib/params_ready/version.rb +7 -0
  85. data/lib/params_ready.rb +28 -0
  86. metadata +227 -0
@@ -0,0 +1,96 @@
1
+ require_relative 'collection'
2
+ require_relative 'struct_marshallers'
3
+
4
+ module ParamsReady
5
+ module Marshaller
6
+ class EnumSetMarshallers
7
+ module AbstractMarshaller
8
+ def canonicalize_collection(definition, context, validator, freeze: false)
9
+ hash = {}
10
+ definition.names.each do |name, definition|
11
+ child = definition.create
12
+ value = yield child
13
+ child.set_from_input(value, context, validator)
14
+ child.freeze if freeze
15
+ hash[name] = child
16
+ end
17
+ [hash, validator]
18
+ end
19
+ end
20
+
21
+ module StructMarshaller
22
+ extend AbstractMarshaller
23
+
24
+ def self.canonicalize(definition, hash, context, validator)
25
+ canonicalize_collection(definition, context, validator) do |child|
26
+ _, value = child.find_in_hash(hash, context)
27
+ value
28
+ end
29
+ end
30
+
31
+ def self.marshal(parameter, intent)
32
+ if intent.marshal? parameter.name_for_formatter
33
+ StructMarshallers::StructMarshaller.marshal(parameter, intent)
34
+ else
35
+ SetMarshaller.marshal(parameter, intent)
36
+ end
37
+ end
38
+
39
+ freeze
40
+ end
41
+
42
+ module SetMarshaller
43
+ extend AbstractMarshaller
44
+
45
+ def self.canonicalize(definition, set, context, validator, freeze: false)
46
+ canonicalize_collection(definition, context, validator, freeze: freeze) do |child|
47
+ value = definition.values[child.name]
48
+ set.member?(value) || set.member?(value.to_s)
49
+ end
50
+ end
51
+
52
+ def self.marshal(parameter, intent)
53
+ intent = parameter.class.intent_for_set(intent)
54
+
55
+ members = parameter.send(:bare_value).select do |_, m|
56
+ m.unwrap_or(false) == true && m.eligible_for_output?(intent)
57
+ end.map do |key, _|
58
+ parameter.definition.values[key]
59
+ end
60
+
61
+ members.to_set
62
+ end
63
+
64
+ freeze
65
+ end
66
+
67
+ module ArrayMarshaller
68
+ extend AbstractMarshaller
69
+
70
+ def self.canonicalize(definition, array, context, validator)
71
+ set = array.to_set
72
+ SetMarshaller.canonicalize(definition, set, context, validator)
73
+ end
74
+
75
+ def self.marshal(parameter, intent)
76
+ set = SetMarshaller.marshal(parameter, intent)
77
+ set.to_a
78
+ end
79
+
80
+ freeze
81
+ end
82
+
83
+ def self.collection
84
+ @collection ||= begin
85
+ c = ClassCollection.new Hash
86
+ c.add_instance Hash, StructMarshaller
87
+ c.add_instance Set, SetMarshaller
88
+ c.add_instance Array, ArrayMarshaller
89
+ c.default!(Hash)
90
+ c.freeze
91
+ c
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,11 @@
1
+ module ParamsReady
2
+ module Marshaller
3
+ module ParameterModule
4
+ def marshal(intent)
5
+ return nil if is_nil?
6
+
7
+ definition.marshal(self, intent)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,67 @@
1
+ require_relative 'collection'
2
+
3
+ module ParamsReady
4
+ module Marshaller
5
+ class PolymorphMarshallers
6
+ class StructMarshaller
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, StructMarshaller
61
+ c.freeze
62
+ c
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,100 @@
1
+ require 'json'
2
+ require 'base64'
3
+ require_relative 'collection'
4
+
5
+ module ParamsReady
6
+ module Marshaller
7
+ class StructMarshallers
8
+ module AbstractMarshaller
9
+ def extract_bare_value(parameter, intent)
10
+ parameter.names.keys.reduce({}) do |result, name|
11
+ c = parameter[name]
12
+ hash = c.to_hash_if_eligible(intent)
13
+ if hash.nil?
14
+ result
15
+ else
16
+ result.deep_merge(hash)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ module Base64Marshaller
23
+ def self.instance
24
+ [String, self]
25
+ end
26
+
27
+ def self.canonicalize(definition, string, context, validator)
28
+ json = Base64.urlsafe_decode64(string)
29
+ hash = JSON.parse(json)
30
+ StructMarshaller.canonicalize(definition, hash, context, validator)
31
+ end
32
+
33
+ def self.marshal(parameter, intent)
34
+ hash = StructMarshaller.marshal(parameter, intent)
35
+ json = JSON.generate(hash)
36
+ Base64.urlsafe_encode64(json)
37
+ end
38
+
39
+ freeze
40
+ end
41
+
42
+ module StructMarshaller
43
+ extend AbstractMarshaller
44
+
45
+ def self.canonicalize(definition, hash, context, validator, freeze: false)
46
+ hash = if definition.respond_to?(:remap?) && definition.remap?(context)
47
+ definition.key_map.to_standard(hash)
48
+ else
49
+ hash
50
+ end
51
+
52
+ value = definition.names.each_with_object({}) do |(name, child_def), result|
53
+ child = child_def.create
54
+ child.set_from_hash(hash, validator: validator&.for_child(name), context: context)
55
+ child.freeze if freeze
56
+ result[name] = child
57
+ end
58
+ [value, validator]
59
+ end
60
+
61
+ def self.marshal(parameter, intent)
62
+ value = extract_bare_value(parameter, intent)
63
+
64
+ definition = parameter.definition
65
+
66
+ if value == {}
67
+ if intent.marshal?(definition.name_for_formatter)
68
+ if definition.optional? || definition.default_defined?
69
+ parameter.class::EMPTY_HASH
70
+ elsif intent.omit?(parameter)
71
+ nil
72
+ else
73
+ value
74
+ end
75
+ else
76
+ value
77
+ end
78
+ elsif definition.respond_to?(:remap?) && definition.remap?(intent)
79
+ definition.key_map.to_alternative(value)
80
+ else
81
+ value
82
+ end
83
+ end
84
+
85
+ freeze
86
+ end
87
+
88
+ def self.collection
89
+ @collection ||= begin
90
+ c = ClassCollection.new Hash
91
+ c.add_instance Hash, StructMarshaller
92
+ c.add_factory :base64, Base64Marshaller
93
+ c.default!(Hash)
94
+ c.freeze
95
+ c
96
+ end
97
+ end
98
+ end
99
+ end
100
+ 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 StructMarshaller
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, StructMarshaller
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_index.each_with_object(Hash.new([:none, nil])) do |(tuple, index), hash|
31
+ hash[tuple[0].unwrap] = [tuple[1].unwrap, index]
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