params_ready 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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