apiwork 0.0.0.pre → 0.1.2

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +2 -2
  3. data/README.md +117 -1
  4. data/Rakefile +5 -3
  5. data/app/controllers/apiwork/errors_controller.rb +13 -0
  6. data/app/controllers/apiwork/exports_controller.rb +22 -0
  7. data/lib/apiwork/abstractable.rb +26 -0
  8. data/lib/apiwork/adapter/base.rb +369 -0
  9. data/lib/apiwork/adapter/builder/api/base.rb +66 -0
  10. data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
  11. data/lib/apiwork/adapter/capability/api/base.rb +51 -0
  12. data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
  13. data/lib/apiwork/adapter/capability/base.rb +291 -0
  14. data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
  15. data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
  16. data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
  17. data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
  18. data/lib/apiwork/adapter/capability/result.rb +21 -0
  19. data/lib/apiwork/adapter/capability/runner.rb +56 -0
  20. data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
  21. data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
  22. data/lib/apiwork/adapter/registry.rb +16 -0
  23. data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
  24. data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
  25. data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
  26. data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
  27. data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
  28. data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
  29. data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
  30. data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
  31. data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
  32. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
  33. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
  34. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
  35. data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
  36. data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
  37. data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
  38. data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
  39. data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
  40. data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
  41. data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
  42. data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
  43. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
  44. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
  45. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
  46. data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
  47. data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
  48. data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
  49. data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
  50. data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
  51. data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
  52. data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
  53. data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
  54. data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
  55. data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
  56. data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
  57. data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
  58. data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
  59. data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
  60. data/lib/apiwork/adapter/standard.rb +22 -0
  61. data/lib/apiwork/adapter/wrapper/base.rb +70 -0
  62. data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
  63. data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
  64. data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
  65. data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
  66. data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
  67. data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
  68. data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
  69. data/lib/apiwork/adapter.rb +50 -0
  70. data/lib/apiwork/api/base.rb +802 -0
  71. data/lib/apiwork/api/element.rb +110 -0
  72. data/lib/apiwork/api/enum_registry/definition.rb +51 -0
  73. data/lib/apiwork/api/enum_registry.rb +98 -0
  74. data/lib/apiwork/api/info/contact.rb +67 -0
  75. data/lib/apiwork/api/info/license.rb +50 -0
  76. data/lib/apiwork/api/info/server.rb +50 -0
  77. data/lib/apiwork/api/info.rb +221 -0
  78. data/lib/apiwork/api/object.rb +235 -0
  79. data/lib/apiwork/api/registry.rb +33 -0
  80. data/lib/apiwork/api/representation_registry.rb +76 -0
  81. data/lib/apiwork/api/resource/action.rb +41 -0
  82. data/lib/apiwork/api/resource.rb +648 -0
  83. data/lib/apiwork/api/router.rb +104 -0
  84. data/lib/apiwork/api/type_registry/definition.rb +117 -0
  85. data/lib/apiwork/api/type_registry.rb +99 -0
  86. data/lib/apiwork/api/union.rb +49 -0
  87. data/lib/apiwork/api.rb +85 -0
  88. data/lib/apiwork/configurable.rb +71 -0
  89. data/lib/apiwork/configuration/option.rb +125 -0
  90. data/lib/apiwork/configuration/validatable.rb +25 -0
  91. data/lib/apiwork/configuration.rb +95 -0
  92. data/lib/apiwork/configuration_error.rb +6 -0
  93. data/lib/apiwork/constraint_error.rb +20 -0
  94. data/lib/apiwork/contract/action/request.rb +79 -0
  95. data/lib/apiwork/contract/action/response.rb +87 -0
  96. data/lib/apiwork/contract/action.rb +258 -0
  97. data/lib/apiwork/contract/base.rb +714 -0
  98. data/lib/apiwork/contract/element.rb +130 -0
  99. data/lib/apiwork/contract/object/coercer.rb +194 -0
  100. data/lib/apiwork/contract/object/deserializer.rb +101 -0
  101. data/lib/apiwork/contract/object/transformer.rb +95 -0
  102. data/lib/apiwork/contract/object/validator/result.rb +27 -0
  103. data/lib/apiwork/contract/object/validator.rb +734 -0
  104. data/lib/apiwork/contract/object.rb +566 -0
  105. data/lib/apiwork/contract/request_parser/result.rb +25 -0
  106. data/lib/apiwork/contract/request_parser.rb +72 -0
  107. data/lib/apiwork/contract/response_parser/result.rb +25 -0
  108. data/lib/apiwork/contract/response_parser.rb +35 -0
  109. data/lib/apiwork/contract/union.rb +56 -0
  110. data/lib/apiwork/contract_error.rb +9 -0
  111. data/lib/apiwork/controller.rb +300 -0
  112. data/lib/apiwork/domain_error.rb +13 -0
  113. data/lib/apiwork/element.rb +386 -0
  114. data/lib/apiwork/engine.rb +20 -0
  115. data/lib/apiwork/error.rb +6 -0
  116. data/lib/apiwork/error_code/definition.rb +63 -0
  117. data/lib/apiwork/error_code/registry.rb +18 -0
  118. data/lib/apiwork/error_code.rb +132 -0
  119. data/lib/apiwork/export/base.rb +291 -0
  120. data/lib/apiwork/export/open_api.rb +600 -0
  121. data/lib/apiwork/export/pipeline/writer.rb +66 -0
  122. data/lib/apiwork/export/pipeline.rb +84 -0
  123. data/lib/apiwork/export/registry.rb +16 -0
  124. data/lib/apiwork/export/surface_resolver.rb +189 -0
  125. data/lib/apiwork/export/type_analysis.rb +170 -0
  126. data/lib/apiwork/export/type_script.rb +23 -0
  127. data/lib/apiwork/export/type_script_mapper.rb +349 -0
  128. data/lib/apiwork/export/zod.rb +39 -0
  129. data/lib/apiwork/export/zod_mapper.rb +421 -0
  130. data/lib/apiwork/export.rb +80 -0
  131. data/lib/apiwork/http_error.rb +16 -0
  132. data/lib/apiwork/introspection/action/request.rb +66 -0
  133. data/lib/apiwork/introspection/action/response.rb +57 -0
  134. data/lib/apiwork/introspection/action.rb +124 -0
  135. data/lib/apiwork/introspection/api/info/contact.rb +59 -0
  136. data/lib/apiwork/introspection/api/info/license.rb +49 -0
  137. data/lib/apiwork/introspection/api/info/server.rb +50 -0
  138. data/lib/apiwork/introspection/api/info.rb +107 -0
  139. data/lib/apiwork/introspection/api/resource.rb +83 -0
  140. data/lib/apiwork/introspection/api.rb +92 -0
  141. data/lib/apiwork/introspection/contract.rb +63 -0
  142. data/lib/apiwork/introspection/dump/action.rb +101 -0
  143. data/lib/apiwork/introspection/dump/api.rb +119 -0
  144. data/lib/apiwork/introspection/dump/contract.rb +129 -0
  145. data/lib/apiwork/introspection/dump/param.rb +486 -0
  146. data/lib/apiwork/introspection/dump/resource.rb +112 -0
  147. data/lib/apiwork/introspection/dump/type.rb +339 -0
  148. data/lib/apiwork/introspection/dump.rb +17 -0
  149. data/lib/apiwork/introspection/enum.rb +63 -0
  150. data/lib/apiwork/introspection/error_code.rb +44 -0
  151. data/lib/apiwork/introspection/param/array.rb +88 -0
  152. data/lib/apiwork/introspection/param/base.rb +285 -0
  153. data/lib/apiwork/introspection/param/binary.rb +73 -0
  154. data/lib/apiwork/introspection/param/boolean.rb +73 -0
  155. data/lib/apiwork/introspection/param/date.rb +73 -0
  156. data/lib/apiwork/introspection/param/date_time.rb +73 -0
  157. data/lib/apiwork/introspection/param/decimal.rb +121 -0
  158. data/lib/apiwork/introspection/param/integer.rb +131 -0
  159. data/lib/apiwork/introspection/param/literal.rb +45 -0
  160. data/lib/apiwork/introspection/param/number.rb +121 -0
  161. data/lib/apiwork/introspection/param/object.rb +59 -0
  162. data/lib/apiwork/introspection/param/reference.rb +45 -0
  163. data/lib/apiwork/introspection/param/string.rb +122 -0
  164. data/lib/apiwork/introspection/param/time.rb +73 -0
  165. data/lib/apiwork/introspection/param/union.rb +57 -0
  166. data/lib/apiwork/introspection/param/unknown.rb +26 -0
  167. data/lib/apiwork/introspection/param/uuid.rb +73 -0
  168. data/lib/apiwork/introspection/param.rb +31 -0
  169. data/lib/apiwork/introspection/type.rb +129 -0
  170. data/lib/apiwork/introspection.rb +28 -0
  171. data/lib/apiwork/issue.rb +80 -0
  172. data/lib/apiwork/json_pointer.rb +21 -0
  173. data/lib/apiwork/object.rb +1618 -0
  174. data/lib/apiwork/reference_generator.rb +638 -0
  175. data/lib/apiwork/registry.rb +56 -0
  176. data/lib/apiwork/representation/association.rb +391 -0
  177. data/lib/apiwork/representation/attribute.rb +335 -0
  178. data/lib/apiwork/representation/base.rb +819 -0
  179. data/lib/apiwork/representation/deserializer.rb +95 -0
  180. data/lib/apiwork/representation/element.rb +128 -0
  181. data/lib/apiwork/representation/inheritance.rb +78 -0
  182. data/lib/apiwork/representation/model_detector.rb +75 -0
  183. data/lib/apiwork/representation/root_key.rb +35 -0
  184. data/lib/apiwork/representation/serializer.rb +127 -0
  185. data/lib/apiwork/request.rb +79 -0
  186. data/lib/apiwork/response.rb +56 -0
  187. data/lib/apiwork/union.rb +102 -0
  188. data/lib/apiwork/version.rb +2 -2
  189. data/lib/apiwork.rb +61 -3
  190. data/lib/generators/apiwork/api_generator.rb +38 -0
  191. data/lib/generators/apiwork/contract_generator.rb +25 -0
  192. data/lib/generators/apiwork/install_generator.rb +27 -0
  193. data/lib/generators/apiwork/representation_generator.rb +25 -0
  194. data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
  195. data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
  196. data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
  197. data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
  198. data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
  199. data/lib/tasks/apiwork.rake +102 -0
  200. metadata +319 -19
  201. data/.rubocop.yml +0 -8
  202. data/sig/apiwork.rbs +0 -4
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Filtering
8
+ class Operation
9
+ class Filter
10
+ class OperatorBuilder
11
+ attr_reader :column,
12
+ :field_name,
13
+ :valid_operators
14
+
15
+ def initialize(column, field_name, valid_operators:)
16
+ @column = column
17
+ @field_name = field_name
18
+ @valid_operators = valid_operators
19
+ end
20
+
21
+ def build(operator_hash)
22
+ operator_hash.filter_map do |operator, compare_value|
23
+ operator = operator.to_sym
24
+ next unless valid_operators.include?(operator)
25
+
26
+ yield(operator, compare_value)
27
+ end.reduce(:and)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,462 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Filtering
8
+ class Operation
9
+ class Filter
10
+ attr_reader :representation_class
11
+
12
+ class << self
13
+ def apply(relation, representation_class, params)
14
+ new(relation, representation_class).apply(params)
15
+ end
16
+ end
17
+
18
+ def initialize(relation, representation_class)
19
+ @relation = relation
20
+ @representation_class = representation_class
21
+ end
22
+
23
+ def apply(params)
24
+ data = filter_data(params)
25
+ includes = IncludesResolver.resolve(representation_class, params)
26
+ { data:, includes: }
27
+ end
28
+
29
+ def build_where_conditions(filter, target_klass = representation_class.model_class)
30
+ filter.each_with_object([[], {}]) do |(key, value), (conditions, joins)|
31
+ key = key.to_sym
32
+
33
+ if (attribute = representation_class.attributes[key])&.filterable?
34
+ next unless filterable_for_context?(attribute)
35
+
36
+ if (condition = build_column_condition(key, value, target_klass))
37
+ conditions << condition
38
+ end
39
+
40
+ elsif (association = find_filterable_association(key))
41
+ association_conditions, association_joins = build_join_conditions(key, value, association)
42
+ conditions.concat(association_conditions)
43
+ joins.deep_merge!(association_joins)
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def filter_data(params)
51
+ return @relation if params.blank?
52
+
53
+ case params
54
+ when Hash
55
+ apply_hash_filter(params)
56
+ when Array
57
+ apply_array_filter(params)
58
+ end
59
+ end
60
+
61
+ def apply_hash_filter(params)
62
+ logical_operators, regular_attributes = separate_logical_operators(params)
63
+
64
+ scope = @relation
65
+
66
+ if regular_attributes.present?
67
+ conditions, joins = build_where_conditions(regular_attributes, representation_class.model_class)
68
+ scope = with_joins_and_distinct(scope, joins) { |scoped| scoped.where(conditions.reduce(:and)) } if conditions.any?
69
+ end
70
+
71
+ scope = apply_not(scope, logical_operators[Constants::NOT]) if logical_operators.key?(Constants::NOT)
72
+ scope = apply_or(scope, logical_operators[Constants::OR]) if logical_operators.key?(Constants::OR)
73
+ scope = apply_and(scope, logical_operators[Constants::AND]) if logical_operators.key?(Constants::AND)
74
+
75
+ scope
76
+ end
77
+
78
+ def apply_array_filter(params)
79
+ return @relation if params.empty?
80
+
81
+ individual_conditions = params.filter_map do |filter_hash|
82
+ conditions, _joins = build_where_conditions(filter_hash, representation_class.model_class)
83
+ conditions.compact.reduce(:and) if conditions.any?
84
+ end
85
+
86
+ all_joins = params
87
+ .map { |filter_params| build_where_conditions(filter_params, representation_class.model_class)[1] }
88
+ .each_with_object({}) { |joins, accumulated| accumulated.deep_merge!(joins) }
89
+
90
+ with_joins_and_distinct(@relation, all_joins) do |scope|
91
+ if individual_conditions.any?
92
+ scope.where(individual_conditions.reduce(:or))
93
+ else
94
+ scope
95
+ end
96
+ end
97
+ end
98
+
99
+ def apply_not(scope, filter_params)
100
+ condition, joins = build_conditions_recursive(filter_params)
101
+ return scope if condition.nil?
102
+
103
+ with_joins_and_distinct(scope, joins) { |scoped| scoped.where.not(condition) }
104
+ end
105
+
106
+ def apply_or(scope, conditions_array)
107
+ return scope if conditions_array.blank?
108
+
109
+ or_conditions = []
110
+ all_joins = {}
111
+
112
+ conditions_array.each do |filter_hash|
113
+ conditions, joins = build_conditions_recursive(filter_hash)
114
+ or_conditions << conditions if conditions
115
+ all_joins = all_joins.deep_merge(joins)
116
+ end
117
+
118
+ with_joins_and_distinct(scope, all_joins) do |scoped|
119
+ if or_conditions.any?
120
+ scoped.where(or_conditions.compact.reduce(:or))
121
+ else
122
+ scoped
123
+ end
124
+ end
125
+ end
126
+
127
+ def apply_and(scope, conditions_array)
128
+ return scope if conditions_array.blank?
129
+
130
+ conditions_array.reduce(scope) do |current_scope, filter_hash|
131
+ Filter.apply(current_scope, representation_class, filter_hash)[:data]
132
+ end
133
+ end
134
+
135
+ def build_conditions_recursive(filter_params)
136
+ return [nil, {}] if filter_params.blank?
137
+ return [nil, {}] unless filter_params.is_a?(Hash)
138
+
139
+ logical_operators, regular_attributes = separate_logical_operators(filter_params)
140
+
141
+ conditions = []
142
+ all_joins = {}
143
+
144
+ if regular_attributes.present?
145
+ attribute_conditions, joins = build_where_conditions(regular_attributes, representation_class.model_class)
146
+ conditions << attribute_conditions.reduce(:and) if attribute_conditions.any?
147
+ all_joins = all_joins.deep_merge(joins)
148
+ end
149
+
150
+ if logical_operators.key?(Constants::AND)
151
+ condition, joins = process_logical_operator(logical_operators[Constants::AND], :and)
152
+ conditions << condition if condition
153
+ all_joins = all_joins.deep_merge(joins)
154
+ end
155
+
156
+ if logical_operators.key?(Constants::OR)
157
+ condition, joins = process_logical_operator(logical_operators[Constants::OR], :or)
158
+ conditions << condition if condition
159
+ all_joins = all_joins.deep_merge(joins)
160
+ end
161
+
162
+ if logical_operators.key?(Constants::NOT)
163
+ not_condition, joins = build_conditions_recursive(logical_operators[Constants::NOT])
164
+ conditions << not_condition.not if not_condition
165
+ all_joins = all_joins.deep_merge(joins)
166
+ end
167
+
168
+ [conditions.compact.reduce(:and), all_joins]
169
+ end
170
+
171
+ def process_logical_operator(filters, combinator)
172
+ collected_conditions = []
173
+ all_joins = {}
174
+
175
+ filters.each do |filter_hash|
176
+ condition, joins = build_conditions_recursive(filter_hash)
177
+ collected_conditions << condition if condition
178
+ all_joins = all_joins.deep_merge(joins)
179
+ end
180
+
181
+ [collected_conditions.any? ? collected_conditions.reduce(combinator) : nil, all_joins]
182
+ end
183
+
184
+ def filterable_for_context?(attribute)
185
+ return true unless attribute.filterable?.is_a?(Proc)
186
+
187
+ representation_class.new(nil, {}).instance_eval(&attribute.filterable?)
188
+ end
189
+
190
+ def build_column_condition(key, value, target_klass)
191
+ association = representation_class.polymorphic_association_for_type_column(key)
192
+ value = transform_polymorphic_filter_value(value, association) if association
193
+
194
+ inheritance = representation_class.inheritance_for_column(key)
195
+ value = transform_sti_filter_value(value, inheritance) if inheritance
196
+
197
+ column_type = target_klass.type_for_attribute(key).type
198
+ return nil if column_type.nil?
199
+
200
+ case column_type
201
+ when :uuid
202
+ build_uuid_where_clause(key, value, target_klass)
203
+ when :string, :text, :binary
204
+ build_string_where_clause(key, value, target_klass)
205
+ when :date, :datetime
206
+ build_date_where_clause(key, value, target_klass)
207
+ when :time
208
+ build_time_where_clause(key, value, target_klass)
209
+ when :decimal, :integer, :float
210
+ build_numeric_where_clause(key, value, target_klass)
211
+ when :boolean
212
+ build_boolean_where_clause(key, value, target_klass)
213
+ end
214
+ end
215
+
216
+ def transform_polymorphic_filter_value(value, association)
217
+ mapping = build_polymorphic_type_mapping(association)
218
+
219
+ case value
220
+ when String
221
+ mapping[value] || value
222
+ when Hash
223
+ value.transform_values { |item| transform_polymorphic_filter_value(item, association) }
224
+ when Array
225
+ value.map { |item| transform_polymorphic_filter_value(item, association) }
226
+ else
227
+ value
228
+ end
229
+ end
230
+
231
+ def build_polymorphic_type_mapping(association)
232
+ association.polymorphic.each_with_object({}) do |representation_class, mapping|
233
+ mapping[representation_class.polymorphic_name] = representation_class.model_class.polymorphic_name
234
+ end
235
+ end
236
+
237
+ def transform_sti_filter_value(value, inheritance)
238
+ mapping = inheritance.mapping
239
+
240
+ case value
241
+ when String
242
+ mapping[value] || value
243
+ when Hash
244
+ value.transform_values { |item| transform_sti_filter_value(item, inheritance) }
245
+ when Array
246
+ value.map { |item| transform_sti_filter_value(item, inheritance) }
247
+ else
248
+ value
249
+ end
250
+ end
251
+
252
+ def find_filterable_association(key)
253
+ association = representation_class.associations[key]
254
+ return unless association
255
+ return unless association.filterable?
256
+
257
+ association
258
+ end
259
+
260
+ def build_join_conditions(key, value, association)
261
+ reflection = representation_class.model_class.reflect_on_association(key)
262
+ return [[], {}] unless reflection
263
+ return [[], {}] unless association.representation_class
264
+
265
+ nested_query = Filter.new(reflection.klass.all, association.representation_class)
266
+ nested_conditions, nested_joins = nested_query.build_where_conditions(value, reflection.klass)
267
+
268
+ [nested_conditions, { key => (nested_joins.any? ? nested_joins : {}) }]
269
+ end
270
+
271
+ def build_uuid_where_clause(key, value, target_klass)
272
+ column = target_klass.arel_table[key]
273
+
274
+ normalizer = lambda do |value|
275
+ case value
276
+ when String
277
+ value.include?(',') ? { in: value.split(',') } : { eq: value }
278
+ when Array
279
+ { in: value }
280
+ else
281
+ value
282
+ end
283
+ end
284
+
285
+ builder = Builder.new(column, key, allowed_types: [Hash])
286
+
287
+ builder.build(value, normalizer:, valid_operators: Constants::NULLABLE_UUID_OPERATORS) do |operator, compare|
288
+ case operator
289
+ when :eq then column.eq(compare)
290
+ when :in then column.in(compare)
291
+ when :null then handle_null_operator(column, compare)
292
+ end
293
+ end
294
+ end
295
+
296
+ def build_string_where_clause(key, value, target_klass)
297
+ column = target_klass.arel_table[key]
298
+
299
+ normalizer = ->(value) { value.is_a?(String) || value.nil? ? { eq: value } : value }
300
+
301
+ builder = Builder.new(column, key, allowed_types: [Hash])
302
+
303
+ builder.build(
304
+ value,
305
+ normalizer:,
306
+ valid_operators: Constants::NULLABLE_STRING_OPERATORS,
307
+ ) do |operator, compare|
308
+ case operator
309
+ when :eq then column.eq(compare)
310
+ when :contains then case_sensitive_pattern_match(column, "%#{compare}%")
311
+ when :starts_with then case_sensitive_pattern_match(column, "#{compare}%")
312
+ when :ends_with then case_sensitive_pattern_match(column, "%#{compare}")
313
+ when :in then column.in(compare)
314
+ when :null then handle_null_operator(column, compare)
315
+ end
316
+ end
317
+ end
318
+
319
+ def build_date_where_clause(key, value, target_klass)
320
+ column = target_klass.arel_table[key]
321
+
322
+ return handle_nil_value(column) if value.nil?
323
+ return column.eq(value) unless value.is_a?(Hash)
324
+
325
+ normalizer = ->(value) { value }
326
+
327
+ builder = Builder.new(column, key, allowed_types: [Hash])
328
+
329
+ builder.build(value, normalizer:, valid_operators: Constants::NULLABLE_DATE_OPERATORS) do |operator, compare|
330
+ case operator
331
+ when :null then handle_null_operator(column, compare)
332
+ when :between
333
+ from_date = compare[:from]
334
+ to_date = compare[:to]
335
+ next unless from_date && to_date
336
+
337
+ column.gteq(from_date.beginning_of_day).and(column.lteq(to_date.end_of_day))
338
+ when :eq then column.eq(compare)
339
+ when :gt then column.gt(compare)
340
+ when :gte then column.gteq(compare)
341
+ when :lt then column.lt(compare)
342
+ when :lte then column.lteq(compare)
343
+ when :in then column.in(Array(compare))
344
+ end
345
+ end
346
+ end
347
+
348
+ def handle_nil_value(column)
349
+ column.eq(nil)
350
+ end
351
+
352
+ def build_time_where_clause(key, value, target_klass)
353
+ column = target_klass.arel_table[key]
354
+
355
+ return handle_nil_value(column) if value.nil?
356
+ return column.eq(value) unless value.is_a?(Hash)
357
+
358
+ normalizer = ->(value) { value }
359
+
360
+ builder = Builder.new(column, key, allowed_types: [Hash])
361
+
362
+ builder.build(value, normalizer:, valid_operators: Constants::NULLABLE_DATE_OPERATORS) do |operator, compare|
363
+ case operator
364
+ when :null then handle_null_operator(column, compare)
365
+ when :between
366
+ from_time = compare[:from]
367
+ to_time = compare[:to]
368
+ next unless from_time && to_time
369
+
370
+ column.gteq(from_time).and(column.lteq(to_time))
371
+ when :eq then column.eq(compare)
372
+ when :gt then column.gt(compare)
373
+ when :gte then column.gteq(compare)
374
+ when :lt then column.lt(compare)
375
+ when :lte then column.lteq(compare)
376
+ when :in then column.in(Array(compare))
377
+ end
378
+ end
379
+ end
380
+
381
+ def build_numeric_where_clause(key, value, target_klass)
382
+ column = target_klass.arel_table[key]
383
+
384
+ normalizer = ->(value) { value.is_a?(Numeric) || value.nil? ? { eq: value } : value }
385
+
386
+ builder = Builder.new(column, key, allowed_types: [Hash])
387
+
388
+ builder.build(
389
+ value,
390
+ normalizer:,
391
+ valid_operators: Constants::NULLABLE_NUMERIC_OPERATORS,
392
+ ) do |operator, compare|
393
+ case operator
394
+ when :eq then column.eq(compare)
395
+ when :gt then column.gt(compare)
396
+ when :gte then column.gteq(compare)
397
+ when :lt then column.lt(compare)
398
+ when :lte then column.lteq(compare)
399
+ when :between
400
+ from_number = compare[:from]
401
+ to_number = compare[:to]
402
+ next unless from_number && to_number
403
+
404
+ column.between(from_number..to_number)
405
+ when :in
406
+ column.in(Array(compare))
407
+ when :null
408
+ handle_null_operator(column, compare)
409
+ end
410
+ end
411
+ end
412
+
413
+ def build_boolean_where_clause(key, value, target_klass)
414
+ column = target_klass.arel_table[key]
415
+
416
+ normalizer = ->(value) { [true, false, nil].include?(value) ? { eq: value } : value }
417
+
418
+ builder = Builder.new(column, key, allowed_types: [Hash])
419
+
420
+ builder.build(
421
+ value,
422
+ normalizer:,
423
+ valid_operators: Constants::NULLABLE_BOOLEAN_OPERATORS,
424
+ ) do |operator, compare|
425
+ case operator
426
+ when :eq then column.eq(compare)
427
+ when :null then handle_null_operator(column, compare)
428
+ end
429
+ end
430
+ end
431
+
432
+ def handle_null_operator(column, compare)
433
+ compare ? column.eq(nil) : column.not_eq(nil)
434
+ end
435
+
436
+ def sqlite_adapter?
437
+ @sqlite_adapter ||= representation_class.model_class.connection.adapter_name == 'SQLite'
438
+ end
439
+
440
+ def case_sensitive_pattern_match(column, pattern)
441
+ if sqlite_adapter?
442
+ Arel::Nodes::InfixOperation.new('GLOB', column, Arel::Nodes.build_quoted(pattern.tr('%', '*')))
443
+ else
444
+ column.matches(pattern)
445
+ end
446
+ end
447
+
448
+ def separate_logical_operators(params)
449
+ [params.slice(*Constants::LOGICAL_OPERATORS), params.except(*Constants::LOGICAL_OPERATORS)]
450
+ end
451
+
452
+ def with_joins_and_distinct(scope, joins)
453
+ result = yield(joins.present? ? scope.joins(joins) : scope)
454
+ joins.present? ? result.distinct : result
455
+ end
456
+ end
457
+ end
458
+ end
459
+ end
460
+ end
461
+ end
462
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Filtering
8
+ class Operation < Adapter::Capability::Operation::Base
9
+ target :collection
10
+
11
+ def apply
12
+ params = request.query[:filter]
13
+ return if params.blank?
14
+
15
+ result(**Filter.apply(data, representation_class, params))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Filtering < Adapter::Capability::Base
8
+ class RequestTransformer < Adapter::Capability::Transformer::Request::Base
9
+ NUMERIC_KEY_PATTERN = /^\d+$/
10
+
11
+ phase :before
12
+
13
+ def transform
14
+ request.transform(&method(:transform_value))
15
+ end
16
+
17
+ private
18
+
19
+ def transform_value(value)
20
+ case value
21
+ when Hash then apply(value)
22
+ when Array then value.map(&method(:transform_value))
23
+ else value
24
+ end
25
+ end
26
+
27
+ def apply(hash)
28
+ return to_array(hash) if indexed_hash?(hash)
29
+
30
+ hash.transform_values(&method(:transform_value))
31
+ end
32
+
33
+ def to_array(hash)
34
+ hash.keys.sort_by { |key| key.to_s.to_i }.map { |key| transform_value(hash[key]) }
35
+ end
36
+
37
+ def indexed_hash?(hash)
38
+ return false if hash.empty?
39
+
40
+ hash.keys.all? { |key| NUMERIC_KEY_PATTERN.match?(key.to_s) }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Filtering < Adapter::Capability::Base
8
+ capability_name :filtering
9
+
10
+ request_transformer RequestTransformer
11
+ api_builder APIBuilder
12
+ contract_builder ContractBuilder
13
+ operation Operation
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end