apiwork 0.0.0.pre → 0.1.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 (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 +622 -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,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ module Serializer
6
+ module Resource
7
+ # @api public
8
+ # Base class for resource serializers.
9
+ #
10
+ # Resource serializers handle serialization of records and collections
11
+ # and define resource types at the contract level.
12
+ #
13
+ # @example
14
+ # class MyResourceSerializer < Serializer::Resource::Base
15
+ # contract_builder Builder::Contract
16
+ #
17
+ # def serialize(resource, context:, serialize_options:)
18
+ # representation_class.serialize(resource, context:)
19
+ # end
20
+ # end
21
+ class Base
22
+ class << self
23
+ def serialize(representation_class, resource, context:, serialize_options:)
24
+ new(representation_class).serialize(resource, context:, serialize_options:)
25
+ end
26
+
27
+ # @api public
28
+ # The data type for this serializer.
29
+ #
30
+ # @param block [Proc, nil] (nil)
31
+ # Block that receives representation_class and returns type name.
32
+ # @return [Proc, nil]
33
+ def data_type(&block)
34
+ @data_type = block if block
35
+ @data_type
36
+ end
37
+
38
+ # @api public
39
+ # The contract builder for this serializer.
40
+ #
41
+ # @param klass [Class<Builder::Contract::Base>, nil] (nil)
42
+ # The builder class.
43
+ # @return [Class<Builder::Contract::Base>, nil]
44
+ def contract_builder(klass = nil)
45
+ @contract_builder = klass if klass
46
+ @contract_builder
47
+ end
48
+ end
49
+
50
+ # @api public
51
+ # The representation class for this serializer.
52
+ #
53
+ # @return [Class<Representation::Base>]
54
+ attr_reader :representation_class
55
+
56
+ def initialize(representation_class)
57
+ @representation_class = representation_class
58
+ end
59
+
60
+ def contract_types(contract_class)
61
+ builder_class = self.class.contract_builder
62
+ return unless builder_class
63
+
64
+ builder_class.new(contract_class, representation_class).build
65
+ end
66
+
67
+ # @api public
68
+ # Serializes a resource.
69
+ #
70
+ # @param resource [Object]
71
+ # The resource to serialize.
72
+ # @param context [Hash]
73
+ # The serialization context.
74
+ # @param serialize_options [Hash]
75
+ # The options (e.g., include).
76
+ # @return [Hash]
77
+ def serialize(resource, context:, serialize_options:)
78
+ raise NotImplementedError
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ module Serializer
6
+ module Resource
7
+ class Default < Base
8
+ class ContractBuilder < Adapter::Builder::Contract::Base
9
+ def build
10
+ build_enums
11
+ resource_type_name
12
+ end
13
+
14
+ def resource_type_name
15
+ if sti_base_representation?
16
+ build_sti_response_union_type
17
+ else
18
+ register_type(representation_class.root_key.singular.to_sym) unless type?(scoped_type_name(nil))
19
+
20
+ scoped_type_name(nil)
21
+ end
22
+ end
23
+
24
+ def import_association_contract(association_representation, visited)
25
+ return nil if visited.include?(association_representation)
26
+
27
+ association_contract = contract_for(association_representation)
28
+ return nil unless association_contract
29
+
30
+ alias_name = association_representation.root_key.singular.to_sym
31
+ import(association_contract, as: alias_name)
32
+ alias_name
33
+ end
34
+
35
+ private
36
+
37
+ def build_enums
38
+ representation_class.attributes.each do |name, attribute|
39
+ next unless attribute.enum&.any?
40
+
41
+ enum(name, values: attribute.enum)
42
+ end
43
+ end
44
+
45
+ def register_type(type_name)
46
+ association_type_map = {}
47
+ representation_class.associations.each do |name, association|
48
+ association_type_map[name] = build_association_type(association)
49
+ end
50
+
51
+ object(
52
+ type_name,
53
+ description: representation_class.description,
54
+ example: representation_class.example,
55
+ ) do |object|
56
+ if representation_class.subclass?
57
+ discriminator_name = representation_class.superclass.inheritance.column
58
+ object.literal(discriminator_name, value: representation_class.sti_name)
59
+ end
60
+
61
+ representation_class.attributes.each do |name, attribute|
62
+ param_options = {
63
+ deprecated: attribute.deprecated?,
64
+ description: attribute.description,
65
+ example: attribute.example,
66
+ format: attribute.format,
67
+ nullable: attribute.nullable?,
68
+ type: attribute.type,
69
+ **(attribute.enum ? { enum: name } : {}),
70
+ **(attribute.of ? { of: attribute.of } : {}),
71
+ }
72
+
73
+ element = attribute.element
74
+ if element
75
+ if element.type == :array
76
+ param_options[:of] = element.inner
77
+ else
78
+ param_options[:shape] = element.shape
79
+ param_options[:discriminator] = element.discriminator if element.discriminator
80
+ end
81
+ end
82
+
83
+ object.param(name, **param_options)
84
+ end
85
+
86
+ representation_class.associations.each do |name, association|
87
+ association_type = association_type_map[name]
88
+
89
+ base_options = {
90
+ deprecated: association.deprecated?,
91
+ description: association.description,
92
+ example: association.example,
93
+ nullable: association.nullable?,
94
+ optional: association.include != :always,
95
+ }
96
+
97
+ if association.singular?
98
+ object.param(name, type: association_type || :object, **base_options)
99
+ elsif association.collection?
100
+ if association_type
101
+ object.param(name, type: :array, **base_options) do |param|
102
+ param.of(association_type)
103
+ end
104
+ else
105
+ object.param(name, type: :array, **base_options)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ def sti_base_representation?
113
+ inheritance = representation_class.inheritance
114
+ inheritance&.subclasses&.any? && inheritance.base_class == representation_class
115
+ end
116
+
117
+ def build_sti_union(union_type_name:, visited: Set.new)
118
+ representation_inheritance = representation_class.inheritance
119
+ return nil unless representation_inheritance.subclasses.any?
120
+
121
+ discriminator_name = representation_inheritance.column
122
+
123
+ variant_types = representation_inheritance.subclasses.filter_map do |subclass|
124
+ variant_type = yield(subclass)
125
+ { tag: subclass.sti_name, type: variant_type } if variant_type
126
+ end
127
+
128
+ union(union_type_name, discriminator: discriminator_name) do |union|
129
+ variant_types.each do |variant_type|
130
+ union.variant(tag: variant_type[:tag]) do |variant|
131
+ variant.reference(variant_type[:type])
132
+ end
133
+ end
134
+ end
135
+
136
+ union_type_name
137
+ end
138
+
139
+ def build_sti_response_union_type(visited: Set.new)
140
+ union_type_name = representation_class.root_key.singular.to_sym
141
+
142
+ build_sti_union(union_type_name:, visited:) do |variant_representation_class|
143
+ variant_contract = contract_for(variant_representation_class)
144
+ next nil unless variant_contract
145
+
146
+ alias_name = variant_representation_class.root_key.singular.to_sym
147
+ import(variant_contract, as: alias_name)
148
+
149
+ alias_name
150
+ end
151
+ end
152
+
153
+ def build_association_type(association, visited: Set.new)
154
+ return build_polymorphic_association_type(association, visited:) if association.polymorphic?
155
+
156
+ association_info = resolve_association(association)
157
+ return nil unless association_info
158
+
159
+ representation_class = association_info[:representation_class]
160
+
161
+ return import_association_contract(representation_class, visited) if association_info[:sti]
162
+ return nil if visited.include?(representation_class)
163
+
164
+ association_contract = contract_for(representation_class)
165
+ return nil unless association_contract
166
+
167
+ type_name = representation_class.root_key.singular.to_sym
168
+ import(association_contract, as: type_name)
169
+ type_name
170
+ end
171
+
172
+ def build_polymorphic_association_type(association, visited: Set.new)
173
+ polymorphic = association.polymorphic
174
+ return nil unless polymorphic.any?
175
+
176
+ union_type_name = association.name
177
+
178
+ existing_type = type?(union_type_name)
179
+ return union_type_name if existing_type
180
+
181
+ union(union_type_name, discriminator: association.discriminator) do |union|
182
+ polymorphic.each do |polymorphic_representation_class|
183
+ tag = polymorphic_representation_class.polymorphic_name
184
+ alias_name = import_association_contract(polymorphic_representation_class, visited)
185
+ next unless alias_name
186
+
187
+ union.variant(tag:) do |variant|
188
+ variant.reference(alias_name)
189
+ end
190
+ end
191
+ end
192
+
193
+ union_type_name
194
+ end
195
+
196
+ def resolve_association(association)
197
+ return nil if association.polymorphic?
198
+
199
+ representation_class = association.representation_class
200
+ return nil unless representation_class
201
+
202
+ { representation_class:, sti: representation_class.inheritance&.subclasses&.any? }
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ module Serializer
6
+ module Resource
7
+ # @api public
8
+ # Default resource serializer.
9
+ #
10
+ # Delegates serialization to the representation class using its root key as data type.
11
+ #
12
+ # @example Configuration
13
+ # class MyAdapter < Adapter::Base
14
+ # serializer Serializer::Resource::Default
15
+ # end
16
+ #
17
+ # @example Output
18
+ # {
19
+ # "id": 1,
20
+ # "number": "INV-001",
21
+ # "customer": { "id": 5, "name": "Acme" }
22
+ # }
23
+ class Default < Base
24
+ data_type { |representation_class| representation_class.root_key.singular.to_sym }
25
+
26
+ contract_builder ContractBuilder
27
+
28
+ def serialize(resource, context:, serialize_options:)
29
+ representation_class.serialize(
30
+ resource,
31
+ context:,
32
+ include: serialize_options[:include],
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Filtering
8
+ class APIBuilder < Adapter::Capability::API::Base
9
+ def build
10
+ return unless scope.filterable?
11
+
12
+ scope.filter_types.each { |type| register_filter(type, nullable: false) }
13
+ scope.nullable_filter_types.each { |type| register_filter(type, nullable: true) }
14
+ end
15
+
16
+ private
17
+
18
+ def register_filter(type, nullable:)
19
+ type_name = type_name(type, nullable:)
20
+ return if type?(type_name)
21
+
22
+ operators = operators_for(type)
23
+ register_between(type) if operators.include?(:between)
24
+
25
+ object(type_name) do |object|
26
+ operators.each { |operator| add_operator(object, operator, type) }
27
+ object.boolean?(:null) if nullable
28
+ end
29
+ end
30
+
31
+ def add_operator(object, operator, type)
32
+ case operator
33
+ when :in
34
+ object.array?(:in) { |array| array.of(type) }
35
+ when :between
36
+ object.reference?(:between, to: between_type_name(type))
37
+ else
38
+ object.param(operator, type:, optional: true)
39
+ end
40
+ end
41
+
42
+ def register_between(type)
43
+ type_name = between_type_name(type)
44
+ return if type?(type_name)
45
+
46
+ object(type_name) do |object|
47
+ object.param(:from, type:, optional: true)
48
+ object.param(:to, type:, optional: true)
49
+ end
50
+ end
51
+
52
+ def operators_for(type)
53
+ case type
54
+ when :string, :binary then Constants::STRING_OPERATORS
55
+ when :date, :datetime, :time then Constants::DATE_OPERATORS
56
+ when :integer, :decimal, :number then Constants::NUMERIC_OPERATORS
57
+ when :uuid then Constants::UUID_OPERATORS
58
+ when :boolean then Constants::BOOLEAN_OPERATORS
59
+ else Constants::STRING_OPERATORS
60
+ end
61
+ end
62
+
63
+ def type_name(type, nullable:)
64
+ nullable ? [:nullable, type, :filter].join('_').to_sym : [type, :filter].join('_').to_sym
65
+ end
66
+
67
+ def between_type_name(type)
68
+ [type, :filter, :between].join('_').to_sym
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,37 @@
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
+ module Constants
9
+ AND = :AND
10
+ OR = :OR
11
+ NOT = :NOT
12
+ LOGICAL_OPERATORS = [AND, OR, NOT].freeze
13
+
14
+ EQUALITY_OPERATORS = %i[eq].freeze
15
+ COMPARISON_OPERATORS = %i[gt gte lt lte].freeze
16
+ RANGE_OPERATORS = %i[between].freeze
17
+ COLLECTION_OPERATORS = %i[in].freeze
18
+ STRING_SPECIFIC_OPERATORS = %i[contains starts_with ends_with].freeze
19
+ NULL_OPERATORS = %i[null].freeze
20
+
21
+ STRING_OPERATORS = (EQUALITY_OPERATORS + COLLECTION_OPERATORS + STRING_SPECIFIC_OPERATORS).freeze
22
+ DATE_OPERATORS = (EQUALITY_OPERATORS + COMPARISON_OPERATORS + RANGE_OPERATORS + COLLECTION_OPERATORS).freeze
23
+ NUMERIC_OPERATORS = (EQUALITY_OPERATORS + COMPARISON_OPERATORS + RANGE_OPERATORS + COLLECTION_OPERATORS).freeze
24
+ UUID_OPERATORS = (EQUALITY_OPERATORS + COLLECTION_OPERATORS).freeze
25
+ BOOLEAN_OPERATORS = EQUALITY_OPERATORS.freeze
26
+
27
+ NULLABLE_STRING_OPERATORS = (STRING_OPERATORS + NULL_OPERATORS).freeze
28
+ NULLABLE_DATE_OPERATORS = (DATE_OPERATORS + NULL_OPERATORS).freeze
29
+ NULLABLE_NUMERIC_OPERATORS = (NUMERIC_OPERATORS + NULL_OPERATORS).freeze
30
+ NULLABLE_UUID_OPERATORS = (UUID_OPERATORS + NULL_OPERATORS).freeze
31
+ NULLABLE_BOOLEAN_OPERATORS = (BOOLEAN_OPERATORS + NULL_OPERATORS).freeze
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Adapter
5
+ class Standard
6
+ module Capability
7
+ class Filtering
8
+ class ContractBuilder < Adapter::Capability::Contract::Base
9
+ TYPE_NAME = :filter
10
+ UNFILTERABLE_TYPES = %i[unknown array object union].freeze
11
+
12
+ def build
13
+ return unless filterable?
14
+
15
+ representation_class.attributes.each do |name, attribute|
16
+ next unless attribute.filterable? && attribute.enum
17
+
18
+ type_name = [name, TYPE_NAME].join('_').to_sym
19
+ next if type?(type_name)
20
+
21
+ build_enum_filter_union(type_name, scoped_enum_name(name))
22
+ end
23
+
24
+ build_polymorphic_type_filters
25
+ build_sti_type_filters
26
+
27
+ attributes = representation_class.attributes.filter_map do |name, attribute|
28
+ next unless attribute.filterable? && UNFILTERABLE_TYPES.exclude?(attribute.type)
29
+
30
+ filter_type = filter_type_for(attribute)
31
+ has_custom_filter = attribute.enum || polymorphic_type_column?(attribute) || sti_type_column?(attribute)
32
+ shorthand = !has_custom_filter && !%i[object array union].include?(attribute.type)
33
+ [name, attribute.type, filter_type, shorthand]
34
+ end
35
+
36
+ associations = representation_class.associations.filter_map do |name, association|
37
+ next unless association.filterable?
38
+ next if association.polymorphic?
39
+
40
+ representation = association.representation_class
41
+ next unless representation
42
+
43
+ contract = contract_for(representation)
44
+ next unless contract
45
+
46
+ alias_name = representation.root_key.singular.to_sym
47
+ import(contract, as: alias_name)
48
+
49
+ filter_type = [alias_name, TYPE_NAME].join('_').to_sym
50
+ next unless type?(filter_type)
51
+
52
+ [name, filter_type]
53
+ end
54
+
55
+ object(TYPE_NAME) do |object|
56
+ object.array?(Constants::AND) do |element|
57
+ element.reference(TYPE_NAME)
58
+ end
59
+ object.array?(Constants::OR) do |element|
60
+ element.reference(TYPE_NAME)
61
+ end
62
+ object.reference?(Constants::NOT, to: TYPE_NAME)
63
+
64
+ attributes.each do |name, type, filter_type, shorthand|
65
+ if shorthand
66
+ object.union?(name) do |union|
67
+ union.variant do |element|
68
+ element.of(type)
69
+ end
70
+ union.variant do |element|
71
+ element.reference(filter_type)
72
+ end
73
+ end
74
+ else
75
+ object.reference?(name, to: filter_type)
76
+ end
77
+ end
78
+
79
+ associations.each do |name, filter_type|
80
+ object.reference?(name, to: filter_type)
81
+ end
82
+ end
83
+
84
+ return unless type?(TYPE_NAME)
85
+
86
+ action(:index) do |act|
87
+ act.request do |request|
88
+ request.query do |query|
89
+ query.union?(TYPE_NAME) do |union|
90
+ union.variant do |element|
91
+ element.reference(TYPE_NAME)
92
+ end
93
+ union.variant do |element|
94
+ element.array do |array|
95
+ array.reference(TYPE_NAME)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def build_polymorphic_type_filters
107
+ representation_class.attributes.each do |name, attribute|
108
+ next unless attribute.filterable?
109
+
110
+ association = representation_class.polymorphic_association_for_type_column(name)
111
+ next unless association
112
+
113
+ type_name = [name, TYPE_NAME].join('_').to_sym
114
+ next if type?(type_name)
115
+
116
+ enum name, values: association.polymorphic.map(&:polymorphic_name)
117
+ build_enum_filter_union(type_name, scoped_enum_name(name))
118
+ end
119
+ end
120
+
121
+ def build_sti_type_filters
122
+ return if representation_class.subclass?
123
+
124
+ representation_class.attributes.each do |name, attribute|
125
+ next unless attribute.filterable?
126
+
127
+ inheritance = representation_class.inheritance_for_column(name)
128
+ next unless inheritance
129
+
130
+ type_name = [name, TYPE_NAME].join('_').to_sym
131
+ next if type?(type_name)
132
+
133
+ enum(name, values: inheritance.subclasses.map(&:sti_name))
134
+ build_enum_filter_union(type_name, scoped_enum_name(name))
135
+ end
136
+ end
137
+
138
+ def build_enum_filter_union(type_name, enum_name)
139
+ union(type_name) do |union|
140
+ union.variant do |element|
141
+ element.reference(enum_name)
142
+ end
143
+ union.variant(partial: true) do |element|
144
+ element.object do |object|
145
+ object.reference(:eq, to: enum_name)
146
+ object.array(:in) do |array|
147
+ array.reference(enum_name)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ def polymorphic_type_column?(attribute)
155
+ representation_class.polymorphic_association_for_type_column(attribute.name).present?
156
+ end
157
+
158
+ def sti_type_column?(attribute)
159
+ representation_class.inheritance_for_column(attribute.name).present?
160
+ end
161
+
162
+ def filter_type_for(attribute)
163
+ custom_type = [attribute.name, TYPE_NAME].join('_').to_sym
164
+ return custom_type if attribute.enum
165
+ return custom_type if polymorphic_type_column?(attribute)
166
+ return custom_type if sti_type_column?(attribute)
167
+
168
+ type = case attribute.type
169
+ when :string, :binary then :string_filter
170
+ when :date then :date_filter
171
+ when :datetime then :datetime_filter
172
+ when :time then :time_filter
173
+ when :integer then :integer_filter
174
+ when :decimal then :decimal_filter
175
+ when :number then :number_filter
176
+ when :uuid then :uuid_filter
177
+ when :boolean then :boolean_filter
178
+ else :string_filter
179
+ end
180
+
181
+ attribute.nullable? ? [:nullable, type].join('_').to_sym : type
182
+ end
183
+
184
+ def filterable?
185
+ representation_class.attributes.values.any? { |attribute| attribute.filterable? && UNFILTERABLE_TYPES.exclude?(attribute.type) } ||
186
+ representation_class.associations.values.any?(&:filterable?)
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ 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
8
+ class Operation
9
+ class Filter
10
+ class Builder
11
+ attr_reader :allowed_types,
12
+ :column,
13
+ :field_name
14
+
15
+ def initialize(column, field_name, allowed_types:)
16
+ @column = column
17
+ @field_name = field_name
18
+ @allowed_types = Array(allowed_types)
19
+ end
20
+
21
+ def build(value, normalizer: nil, valid_operators:, &block)
22
+ value = normalize_value(value, normalizer) if normalizer
23
+ return nil unless valid_value_type?(value)
24
+
25
+ builder = OperatorBuilder.new(column, field_name, valid_operators:)
26
+ builder.build(value, &block)
27
+ end
28
+
29
+ private
30
+
31
+ def normalize_value(value, normalizer)
32
+ normalizer.call(value)
33
+ end
34
+
35
+ def valid_value_type?(value)
36
+ return true if allowed_types.empty?
37
+
38
+ allowed_types.any? { |type| value.is_a?(type) }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end