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,127 @@
1
+ require_relative 'intent'
2
+
3
+ module ParamsReady
4
+ class OutputParameters
5
+ attr_reader :scoped_id, :parameter
6
+
7
+ def method_missing(name, *args, &block)
8
+ if @parameter.respond_to? name, false
9
+ @parameter.send(name, *args, &block)
10
+ else
11
+ super
12
+ end
13
+ end
14
+
15
+ def respond_to_missing?(name, include_private = false)
16
+ if @parameter.respond_to? name, include_private
17
+ true
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def self.decorate(parameter, *args)
24
+ intent = case args.length
25
+ when 0
26
+ Intent.instance(:frontend)
27
+ when 1
28
+ Intent.resolve(args[0])
29
+ when 2
30
+ format = args[0]
31
+ restriction = args[1]
32
+ Intent.new format, restriction
33
+ else
34
+ msg = "ArgumentError: wrong number of arguments (given #{args.length + 1}, expected 1..3)"
35
+ raise ParamsReadyError, msg
36
+ end
37
+ new parameter, intent
38
+ end
39
+
40
+ def initialize(parameter, intent, scoped_name = nil, scoped_id = nil)
41
+ raise ParamsReadyError, "Expected parameter '#{parameter.name}' to be frozen" unless parameter.frozen?
42
+ @parameter = parameter
43
+ @intent = Intent.resolve(intent)
44
+ @tree = {}
45
+ @scoped_name = scoped_name || @intent.hash_key(parameter).to_s
46
+ @scoped_id = scoped_id || @intent.hash_key(parameter).to_s
47
+ end
48
+
49
+ def scoped_name(multiple: false)
50
+ return @scoped_name unless multiple
51
+ @scoped_name + "[]"
52
+ end
53
+
54
+ def [](key)
55
+ if @tree.key? key
56
+ @tree[key]
57
+ elsif @parameter.respond_to? :[]
58
+ child = @parameter[key]
59
+ formatted_name = if @parameter.definition.is_a? Parameter::ArrayParameterDefinition::ArrayLike
60
+ key.to_s
61
+ else
62
+ @intent.hash_key(child).to_s
63
+ end
64
+ child_scoped_name = @scoped_name.empty? ? formatted_name : "#{@scoped_name}[#{formatted_name}]"
65
+ child_scoped_id = @scoped_id.empty? ? formatted_name : "#{@scoped_id}_#{formatted_name}"
66
+ intent = @parameter.intent_for_children(@intent)
67
+ decorated = OutputParameters.new(child, intent, child_scoped_name, child_scoped_id)
68
+ @tree[key] = decorated
69
+ decorated
70
+ else
71
+ raise ParamsReadyError, "Parameter '#{@parameter.name}' doesn't support square brackets access"
72
+ end
73
+ end
74
+
75
+ def flat_pairs(format = @intent.format, restriction: @intent.restriction, data: @intent.data)
76
+ self.class.flatten_hash(for_output(format, restriction: restriction, data: data), scoped_name)
77
+ end
78
+
79
+ def self.flatten_hash(hash, scope)
80
+ hash.flat_map do |key, value|
81
+ nested = scope.empty? ? key.to_s : "#{scope}[#{key}]"
82
+ if value.is_a? Hash
83
+ flatten_hash(value, nested)
84
+ else
85
+ [[nested, value]]
86
+ end
87
+ end
88
+ end
89
+
90
+ def to_hash(format = @intent.format, restriction: nil, data: @intent.data)
91
+ restriction = if restriction.nil?
92
+ Restriction.permit(name => @intent.restriction)
93
+ else
94
+ restriction
95
+ end
96
+ @parameter.to_hash(format, restriction: restriction, data: data)
97
+ end
98
+
99
+ def for_output(format = @intent.format, restriction: @intent.restriction, data: @intent.data)
100
+ @parameter.for_output(format, restriction: restriction, data: data)
101
+ end
102
+
103
+ def for_frontend(restriction: @intent.restriction, data: @intent.data)
104
+ @parameter.for_frontend(restriction: restriction, data: data)
105
+ end
106
+
107
+ def for_model(restriction: @intent.restriction)
108
+ @parameter.for_model(restriction: restriction)
109
+ end
110
+
111
+ def format(format = @intent)
112
+ @parameter.format(format)
113
+ end
114
+
115
+ def build_select(context: @intent.restriction, **opts)
116
+ @parameter.build_select(context: context, **opts)
117
+ end
118
+
119
+ def build_relation(context: @intent.restriction, **opts)
120
+ @parameter.build_relation(context: context, **opts)
121
+ end
122
+
123
+ def perform_count(context: @intent.restriction, **opts)
124
+ @parameter.perform_count(context: context, **opts)
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,18 @@
1
+ module ParamsReady
2
+ module Pagination
3
+ module AbstractPagination
4
+ def num_pages(count:)
5
+ raise ParamsReadyError, 'Negative count unexpected' if count < 0
6
+ (count.to_f / limit.to_f).ceil.to_i
7
+ end
8
+
9
+ def first_page
10
+ update_in(first_page_value, [])
11
+ end
12
+
13
+ def last_page(*args)
14
+ update_in(last_page_value(*args), [])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,171 @@
1
+ require_relative '../helpers/arel_builder'
2
+ require_relative '../../arel/cte_name'
3
+
4
+ module ParamsReady
5
+ module Pagination
6
+ class CursorBuilder
7
+ def initialize(keyset, arel_table, context)
8
+ @keyset = keyset.freeze
9
+ @arel_table = arel_table
10
+ @context = context
11
+ @select_list = []
12
+ end
13
+
14
+ def add(key, column)
15
+ attribute = if @keyset.key? key
16
+ Literal.new(key, @keyset[key], column.pk)
17
+ else
18
+ Selector.new(key, column)
19
+ end
20
+
21
+ @select_list << attribute
22
+ end
23
+
24
+ def build
25
+ cursor = Cursor.new(@select_list, @arel_table, @context)
26
+ @select_list = nil
27
+ freeze
28
+ cursor
29
+ end
30
+
31
+ class Selector
32
+ attr_reader :key
33
+ attr_reader :column
34
+
35
+ def initialize(key, column)
36
+ @key = key
37
+ @column = column
38
+ freeze
39
+ end
40
+
41
+ def expression(arel_table, context)
42
+ column.attribute(key, arel_table, context)
43
+ end
44
+
45
+ def rvalue(cte)
46
+ cte.project(key)
47
+ end
48
+ end
49
+
50
+ class Literal
51
+ attr_reader :key
52
+ attr_reader :pk
53
+
54
+ def initialize(key, value, pk)
55
+ @key = key
56
+ @value = Arel::Nodes::Quoted.new(value)
57
+ @pk = pk
58
+ freeze
59
+ end
60
+
61
+ def quoted
62
+ @value
63
+ end
64
+
65
+ def value
66
+ @value.value
67
+ end
68
+
69
+ def rvalue(_)
70
+ @value
71
+ end
72
+ end
73
+
74
+ class Cursor
75
+ attr_reader :select_list, :selectors, :literals, :cte
76
+
77
+ def initialize(select_list, arel_table, context)
78
+ @hash = select_list_to_hash(select_list)
79
+ @selectors, @literals = select_list.partition { |attr| attr.is_a? Selector }
80
+ @arel_table = arel_table
81
+ @context = context
82
+ names = column_names(@selectors)
83
+ @cte_ref = Arel::Table.new(cte_reference(names))
84
+ @cte_def = cte_definition(@cte_ref, names)
85
+
86
+ freeze
87
+ end
88
+
89
+ def select_list_to_hash(select_list)
90
+ res = select_list.each_with_object({}) do |item, hash|
91
+ raise ParamsReadyError, "Repeated key in select list: '#{item.key}'" if hash.key? item.key
92
+
93
+ hash[item.key] = item
94
+ end
95
+ res.freeze
96
+ end
97
+
98
+ def cte_for_relation(relation)
99
+ return nil if selectors.empty?
100
+
101
+ expressions = column_expressions(selectors)
102
+ relation = relation.where(**active_record_predicates(literals))
103
+ .select(*expressions)
104
+ select = Arel::Nodes::SqlLiteral.new(relation.to_sql)
105
+ grouping = Arel::Nodes::Grouping.new(select)
106
+ as = Arel::Nodes::As.new(@cte_def, grouping)
107
+ Arel::Nodes::With.new([as])
108
+ end
109
+
110
+ def cte_for_query(query, arel_table)
111
+ return nil if selectors.empty?
112
+
113
+ query = query.deep_dup
114
+ expressions = column_expressions(selectors)
115
+ query = query.where(arel_predicates(literals, arel_table))
116
+ .project(*expressions)
117
+ grouping = Arel::Nodes::Grouping.new(query)
118
+ Arel::Nodes::As.new(@cte_def, grouping)
119
+ end
120
+
121
+ def active_record_predicates(literals)
122
+ literals.select do |literal|
123
+ literal.pk
124
+ end.map do |literal|
125
+ [literal.key, literal.value]
126
+ end.to_h
127
+ end
128
+
129
+ def arel_predicates(literals, arel_table)
130
+ literals.reduce(nil) do |query, literal|
131
+ next query unless literal.pk
132
+
133
+ predicate = arel_table[literal.key].eq(literal.quoted)
134
+ next predicate if query.nil?
135
+
136
+ query.and(predicate)
137
+ end
138
+ end
139
+
140
+ def column_names(selectors)
141
+ selectors.lazy.map(&:key).map(&:to_s).force
142
+ end
143
+
144
+ def column_expressions(selectors)
145
+ selectors.map do |selector|
146
+ selector.expression(@arel_table, @context)
147
+ end
148
+ end
149
+
150
+ def cte_reference(names)
151
+ unsafe_name = "#{names.join('_')}_cte"
152
+ Helpers::ArelBuilder.safe_name(unsafe_name)
153
+ end
154
+
155
+ def cte_definition(reference, names)
156
+ node = Arel::Nodes::SqlLiteral.new(names.join(', '))
157
+ grouping = Arel::Nodes::Grouping.new(node)
158
+ # The name must be literal, otherwise
159
+ # it will be quoted by the visitor
160
+ expression = "#{reference.name} #{grouping.to_sql}"
161
+
162
+ Arel::Nodes::CteName.new(expression)
163
+ end
164
+
165
+ def rvalue(key)
166
+ @hash[key].rvalue(@cte_ref)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,148 @@
1
+ require_relative '../error'
2
+ require_relative 'tendency'
3
+ require_relative 'nulls'
4
+ require_relative 'cursor'
5
+ require_relative 'keysets'
6
+
7
+ module ParamsReady
8
+ module Pagination
9
+ module Direction
10
+ def self.instance(dir)
11
+ case dir
12
+ when :bfr, :before then Before
13
+ when :aft, :after then After
14
+ else
15
+ raise ParamsReadyError, "Unexpected direction: '#{dir}'"
16
+ end
17
+ end
18
+
19
+ def cursor_predicates(keyset, ordering, arel_table, context)
20
+ primary_keys = ordering.definition.primary_keys.dup
21
+ return [nil, nil] unless check_primary_keys_presence(keyset, primary_keys)
22
+
23
+ cursor = build_cursor(keyset, ordering, arel_table, context)
24
+ columns = ordering.to_array_with_context(context)
25
+
26
+ predicate = cursor_predicate(columns, cursor, ordering, arel_table, context, primary_keys)
27
+ grouping = Arel::Nodes::Grouping.new(predicate)
28
+ [cursor, grouping]
29
+ end
30
+
31
+ def check_primary_keys_presence(keyset, primary_keys)
32
+ primary_keys.all? do |pk|
33
+ keyset.key?(pk) && !keyset[pk].nil?
34
+ end
35
+ end
36
+
37
+ def cursor_predicate(columns, cursor, ordering, arel_table, context, primary_keys)
38
+ tuple, *rest = columns
39
+ key, column_ordering = tuple
40
+ column = ordering.definition.columns[key]
41
+
42
+ value_expression = cursor.rvalue(key)
43
+ column_expression = column.attribute(key, arel_table, context)
44
+
45
+ primary_keys.delete(key) if column.pk
46
+
47
+ if column.pk && primary_keys.empty?
48
+ pk_predicate(column_ordering, column_expression, value_expression)
49
+ else
50
+ nested = cursor_predicate(rest, cursor, ordering, arel_table, context, primary_keys)
51
+ if column.nulls == :default
52
+ non_nullable_predicate(column_ordering, column_expression, value_expression, nested)
53
+ else
54
+ nullable_predicate(column_ordering, column.nulls, column_expression, value_expression, nested)
55
+ end
56
+ end
57
+ end
58
+
59
+ def build_cursor(keyset, ordering, arel_table, context)
60
+ builder = CursorBuilder.new(keyset, arel_table, context)
61
+ ordering.to_array_with_context(context).each do |(key, _)|
62
+ column = ordering.definition.columns[key]
63
+ builder.add(key, column)
64
+ end
65
+ builder.build
66
+ end
67
+
68
+ def pk_predicate(ordering, column, value)
69
+ tendency(ordering).comparison_predicate(column, value)
70
+ end
71
+
72
+ def non_nullable_predicate(ordering, column, value, nested)
73
+ tendency(ordering).non_nullable_predicate(column, value, nested)
74
+ end
75
+
76
+ def nullable_predicate(ordering, nulls, column, value, nested)
77
+ strategy = nulls_strategy(nulls)
78
+ if_null = strategy.if_null_predicate(column, nested)
79
+ tendency = tendency(ordering)
80
+ expression = Arel::Nodes::Grouping.new(value)
81
+ if_not_null = strategy.if_not_null_predicate(tendency, column, value, nested)
82
+ Arel::Nodes::Case.new.when(expression.eq(nil))
83
+ .then(if_null)
84
+ .else(if_not_null)
85
+ end
86
+
87
+ module Before
88
+ extend Direction
89
+
90
+ def self.invert_ordering?
91
+ true
92
+ end
93
+
94
+ def self.tendency(ordering)
95
+ case ordering
96
+ when :desc then Tendency::Growing
97
+ when :asc then Tendency::Falling
98
+ else
99
+ raise ParamsReadyError, "Unexpected ordering: '#{ordering}'"
100
+ end
101
+ end
102
+
103
+ def self.nulls_strategy(strategy)
104
+ case strategy
105
+ when :first then Nulls::Last
106
+ when :last then Nulls::First
107
+ else
108
+ raise ParamsReadyError, "Unexpected nulls strategy: '#{strategy}'"
109
+ end
110
+ end
111
+
112
+ def self.keysets(_, keysets, &block)
113
+ BeforeKeysets.new(keysets, &block)
114
+ end
115
+ end
116
+
117
+ module After
118
+ extend Direction
119
+
120
+ def self.invert_ordering?
121
+ false
122
+ end
123
+
124
+ def self.tendency(ordering)
125
+ case ordering
126
+ when :asc then Tendency::Growing
127
+ when :desc then Tendency::Falling
128
+ else
129
+ raise ParamsReadyError, "Unexpected ordering: '#{ordering}'"
130
+ end
131
+ end
132
+
133
+ def self.nulls_strategy(strategy)
134
+ case strategy
135
+ when :first then Nulls::First
136
+ when :last then Nulls::Last
137
+ else
138
+ raise ParamsReadyError, "Unexpected nulls strategy: '#{strategy}'"
139
+ end
140
+ end
141
+
142
+ def self.keysets(last, keysets, &block)
143
+ AfterKeysets.new(last, keysets, &block)
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end