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,566 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ # @api public
6
+ # Block context for defining request/response structure.
7
+ #
8
+ # Accessed via `body do` and `query do` inside contract actions,
9
+ # or `object :name do` at contract level to define reusable types.
10
+ #
11
+ # @see API::Object Block context for reusable types
12
+ #
13
+ # @example instance_eval style
14
+ # body do
15
+ # string :title
16
+ # decimal :amount
17
+ # end
18
+ #
19
+ # @example yield style
20
+ # body do |body|
21
+ # body.string :title
22
+ # body.decimal :amount
23
+ # end
24
+ class Object < Apiwork::Object
25
+ attr_reader :action_name,
26
+ :contract_class
27
+
28
+ def params
29
+ return @params if @merged.empty?
30
+
31
+ expanded = @params.dup
32
+ @merged.each do |type_name|
33
+ merged_type = @contract_class.api_class&.type_registry&.[](type_name)
34
+ next unless merged_type&.params
35
+
36
+ merged_type.params.each do |name, param_options|
37
+ expanded[name] ||= param_options
38
+ end
39
+ end
40
+ expanded
41
+ end
42
+
43
+ def initialize(contract_class, action_name: nil, visited_types: nil, wrapped: false)
44
+ super()
45
+ @contract_class = contract_class
46
+ @action_name = action_name
47
+ @wrapped = wrapped
48
+ @visited_types = visited_types
49
+ end
50
+
51
+ # @api public
52
+ # Defines a param with explicit type.
53
+ #
54
+ # This is the verbose form. Prefer sugar methods (string, integer, etc.)
55
+ # for static definitions.
56
+ #
57
+ # @param name [Symbol]
58
+ # The param name.
59
+ # @param type [Symbol, nil] (nil) [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
60
+ # The param type.
61
+ # @param as [Symbol, nil] (nil)
62
+ # The target attribute name.
63
+ # @param default [Object, nil] (nil)
64
+ # The default value.
65
+ # @param deprecated [Boolean] (false)
66
+ # Whether deprecated. Metadata included in exports.
67
+ # @param description [String, nil] (nil)
68
+ # The description. Metadata included in exports.
69
+ # @param discriminator [Symbol, nil] (nil)
70
+ # The discriminator param name. Unions only.
71
+ # @param enum [Array, Symbol, nil] (nil)
72
+ # The allowed values or enum reference.
73
+ # @param example [Object, nil] (nil)
74
+ # The example value. Metadata included in exports.
75
+ # @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :url, :uuid]
76
+ # Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
77
+ # Valid formats by type: `:decimal`/`:number` (`:double`, `:float`), `:integer` (`:int32`, `:int64`),
78
+ # `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:url`, `:uuid`).
79
+ # @param max [Integer, nil] (nil)
80
+ # The maximum. For `:array`: size. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
81
+ # @param min [Integer, nil] (nil)
82
+ # The minimum. For `:array`: size. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
83
+ # @param nullable [Boolean] (false)
84
+ # Whether the value can be `null`.
85
+ # @param of [Symbol, Hash, nil] (nil)
86
+ # The element type. Arrays only.
87
+ # @param optional [Boolean] (false)
88
+ # Whether the param is optional.
89
+ # @param required [Boolean] (false)
90
+ # Whether the param is required.
91
+ # @param shape [Contract::Object, Contract::Union, nil] (nil)
92
+ # The pre-built shape.
93
+ # @param value [Object, nil] (nil)
94
+ # The literal value.
95
+ # @yield block for nested structure
96
+ # @yieldparam shape [Contract::Object, Contract::Union, Contract::Element]
97
+ # @return [void]
98
+ #
99
+ # @example Dynamic param generation
100
+ # param_type = :string
101
+ # param :title, type: param_type
102
+ #
103
+ # @example Object with block
104
+ # param :address, type: :object do
105
+ # string :street
106
+ # string :city
107
+ # end
108
+ def param(
109
+ name,
110
+ type: nil,
111
+ as: nil,
112
+ default: nil,
113
+ deprecated: false,
114
+ description: nil,
115
+ discriminator: nil,
116
+ enum: nil,
117
+ example: nil,
118
+ format: nil,
119
+ max: nil,
120
+ min: nil,
121
+ nullable: false,
122
+ of: nil,
123
+ optional: false,
124
+ required: false,
125
+ shape: nil,
126
+ value: nil,
127
+ &block
128
+ )
129
+ options = {
130
+ deprecated:,
131
+ description:,
132
+ example:,
133
+ format:,
134
+ max:,
135
+ min:,
136
+ nullable:,
137
+ required:,
138
+ }
139
+
140
+ raise ConfigurationError, 'discriminator can only be used with type: :union' if discriminator && type != :union
141
+
142
+ visited_types ||= @visited_types
143
+ visited_types ||= Set.new
144
+
145
+ resolved_enum = resolve_enum(enum)
146
+
147
+ case type
148
+ when :literal
149
+ define_literal_param(name, as:, default:, deprecated:, description:, optional:, value:)
150
+ when :union
151
+ define_union_param(
152
+ name,
153
+ as:,
154
+ default:,
155
+ discriminator:,
156
+ optional:,
157
+ options:,
158
+ resolved_enum:,
159
+ &block
160
+ )
161
+ else
162
+ define_regular_param(
163
+ name,
164
+ as:,
165
+ default:,
166
+ of:,
167
+ optional:,
168
+ options:,
169
+ resolved_enum:,
170
+ shape:,
171
+ type:,
172
+ visited_types:,
173
+ &block
174
+ )
175
+ end
176
+ end
177
+
178
+ # @api public
179
+ # Defines an array param with element type.
180
+ #
181
+ # @param name [Symbol]
182
+ # The param name.
183
+ # @param as [Symbol, nil] (nil)
184
+ # The target attribute name.
185
+ # @param default [Object, nil] (nil)
186
+ # The default value.
187
+ # @param deprecated [Boolean] (false)
188
+ # Whether deprecated. Metadata included in exports.
189
+ # @param description [String, nil] (nil)
190
+ # The description. Metadata included in exports.
191
+ # @param nullable [Boolean] (false)
192
+ # Whether the value can be `null`.
193
+ # @param optional [Boolean] (false)
194
+ # Whether the param is optional.
195
+ # @param required [Boolean] (false)
196
+ # Whether the param is required.
197
+ # @yield block for defining element type
198
+ # @yieldparam element [Contract::Element]
199
+ # @return [void]
200
+ #
201
+ # @example instance_eval style
202
+ # array :tags do
203
+ # string
204
+ # end
205
+ #
206
+ # @example yield style
207
+ # array :tags do |element|
208
+ # element.string
209
+ # end
210
+ def array(
211
+ name,
212
+ as: nil,
213
+ default: nil,
214
+ deprecated: false,
215
+ description: nil,
216
+ nullable: false,
217
+ optional: false,
218
+ required: false,
219
+ &block
220
+ )
221
+ raise ConfigurationError, 'array requires a block' unless block
222
+
223
+ element = Element.new(@contract_class)
224
+ block.arity.positive? ? yield(element) : element.instance_eval(&block)
225
+ element.validate!
226
+
227
+ param(
228
+ name,
229
+ as:,
230
+ default:,
231
+ deprecated:,
232
+ description:,
233
+ nullable:,
234
+ optional:,
235
+ required:,
236
+ of: element,
237
+ type: :array,
238
+ )
239
+ end
240
+
241
+ def wrapped?
242
+ @wrapped
243
+ end
244
+
245
+ def validate(data, current_depth: 0, max_depth: 10, path: [])
246
+ Validator.validate(self, data, current_depth:, max_depth:, path:)
247
+ end
248
+
249
+ def coerce(data)
250
+ Coercer.coerce(self, data)
251
+ end
252
+
253
+ def deserialize(data)
254
+ Deserializer.deserialize(self, data)
255
+ end
256
+
257
+ def transform(data)
258
+ Transformer.transform(self, data)
259
+ end
260
+
261
+ def copy_type_definition_params(type_definition, target_param)
262
+ return unless type_definition.object?
263
+
264
+ type_definition.params.each do |param_name, param_options|
265
+ nested_shape = param_options[:shape]
266
+
267
+ if param_options[:type] == :array && nested_shape.is_a?(Apiwork::API::Object)
268
+ target_param.param(param_name, **param_options.except(:name))
269
+ elsif nested_shape.is_a?(API::Object)
270
+ copy_nested_object_param(target_param, param_name, param_options, nested_shape)
271
+ elsif nested_shape.is_a?(API::Union)
272
+ copy_nested_union_param(target_param, param_name, param_options, nested_shape)
273
+ else
274
+ target_param.param(param_name, **param_options.except(:name, :shape))
275
+ end
276
+ end
277
+ end
278
+
279
+ private
280
+
281
+ def copy_nested_object_param(target_param, param_name, param_options, nested_shape)
282
+ target_param.param(
283
+ param_name,
284
+ type: param_options[:type],
285
+ **param_options.except(:name, :type, :shape),
286
+ ) do
287
+ nested_shape.params.each do |nested_name, nested_param_options|
288
+ param(nested_name, **nested_param_options.except(:name, :shape))
289
+ end
290
+ end
291
+ end
292
+
293
+ def copy_nested_union_param(target_param, param_name, param_options, nested_shape)
294
+ target_param.param(
295
+ param_name,
296
+ type: param_options[:type],
297
+ **param_options.except(:name, :type, :shape),
298
+ ) do
299
+ nested_shape.variants.each do |variant|
300
+ if variant[:shape].is_a?(API::Object)
301
+ variant tag: variant[:tag] do
302
+ object do
303
+ variant[:shape].params.each do |name, param_options|
304
+ param(name, **param_options.except(:name, :shape))
305
+ end
306
+ end
307
+ end
308
+ elsif variant[:custom_type]
309
+ variant tag: variant[:tag] do
310
+ reference variant[:custom_type]
311
+ end
312
+ else
313
+ variant tag: variant[:tag] do
314
+ send(variant[:type])
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+ def define_literal_param(name, as:, default:, deprecated:, description:, optional:, value:)
322
+ raise ConfigurationError, 'Literal type requires a value parameter' if value.nil?
323
+
324
+ @params[name] = (@params[name] || {}).merge(
325
+ {
326
+ as:,
327
+ default:,
328
+ deprecated:,
329
+ description:,
330
+ name:,
331
+ optional:,
332
+ value:,
333
+ type: :literal,
334
+ }.compact,
335
+ )
336
+ end
337
+
338
+ def define_union_param(name, as:, default:, discriminator:, optional:, options:, resolved_enum:, &block)
339
+ raise ConfigurationError, 'Union type requires a block with variant definitions' unless block_given?
340
+
341
+ union = Union.new(@contract_class, discriminator:)
342
+ block.arity.positive? ? yield(union) : union.instance_eval(&block)
343
+
344
+ @params[name] = (@params[name] || {}).merge(
345
+ {
346
+ as:,
347
+ default:,
348
+ discriminator:,
349
+ enum: resolved_enum,
350
+ name:,
351
+ optional:,
352
+ type: :union,
353
+ union:,
354
+ **options,
355
+ }.compact,
356
+ )
357
+ end
358
+
359
+ def define_regular_param(name, as:, default:, of:, optional:, options:, resolved_enum:, shape:, type:, visited_types:, &block)
360
+ type_definition = @contract_class.resolve_custom_type(type)
361
+
362
+ if type_definition
363
+ expansion_key = [@contract_class.object_id, type]
364
+
365
+ type_definition = nil if visited_types.include?(expansion_key)
366
+ end
367
+
368
+ if type_definition&.object?
369
+ define_custom_type_param(
370
+ name,
371
+ as:,
372
+ default:,
373
+ of:,
374
+ optional:,
375
+ options:,
376
+ resolved_enum:,
377
+ type:,
378
+ type_definition:,
379
+ visited_types:,
380
+ &block
381
+ )
382
+ elsif type_definition&.union?
383
+ define_custom_union_type_param(
384
+ name,
385
+ as:,
386
+ default:,
387
+ optional:,
388
+ options:,
389
+ resolved_enum:,
390
+ type:,
391
+ type_definition:,
392
+ )
393
+ else
394
+ define_standard_param(
395
+ name,
396
+ as:,
397
+ default:,
398
+ of:,
399
+ optional:,
400
+ options:,
401
+ resolved_enum:,
402
+ shape:,
403
+ type:,
404
+ &block
405
+ )
406
+ end
407
+ end
408
+
409
+ def define_custom_union_type_param(
410
+ name,
411
+ type:,
412
+ type_definition:,
413
+ resolved_enum:,
414
+ optional:,
415
+ default:,
416
+ as:,
417
+ options:
418
+ )
419
+ union = Union.new(@contract_class, discriminator: type_definition.discriminator)
420
+
421
+ type_definition.variants.each do |variant|
422
+ if variant[:shape].is_a?(API::Object)
423
+ union.variant tag: variant[:tag] do
424
+ object do
425
+ variant[:shape].params.each do |name, param_options|
426
+ param(name, **param_options.except(:name, :shape))
427
+ end
428
+ end
429
+ end
430
+ elsif variant[:custom_type]
431
+ union.variant tag: variant[:tag] do
432
+ reference variant[:custom_type]
433
+ end
434
+ else
435
+ union.variant tag: variant[:tag] do
436
+ send(variant[:type])
437
+ end
438
+ end
439
+ end
440
+
441
+ @params[name] = (@params[name] || {}).merge(
442
+ {
443
+ as:,
444
+ custom_type: type,
445
+ default:,
446
+ discriminator: type_definition.discriminator,
447
+ enum: resolved_enum,
448
+ name:,
449
+ optional:,
450
+ type: :union,
451
+ union:,
452
+ **options,
453
+ }.compact,
454
+ )
455
+ end
456
+
457
+ def define_custom_type_param(
458
+ name,
459
+ type:,
460
+ type_definition:,
461
+ resolved_enum:,
462
+ optional:,
463
+ default:,
464
+ of:,
465
+ as:,
466
+ visited_types:,
467
+ options:,
468
+ &block
469
+ )
470
+ shape = Object.new(
471
+ @contract_class,
472
+ action_name: @action_name,
473
+ visited_types: visited_types.dup.add([@contract_class.object_id, type]),
474
+ )
475
+
476
+ copy_type_definition_params(type_definition, shape)
477
+
478
+ if block_given?
479
+ block.arity.positive? ? yield(shape) : shape.instance_eval(&block)
480
+ end
481
+
482
+ @params[name] = (@params[name] || {}).merge(
483
+ {
484
+ as:,
485
+ custom_type: type,
486
+ default:,
487
+ enum: resolved_enum,
488
+ name:,
489
+ of:,
490
+ optional:,
491
+ shape:,
492
+ type: :object,
493
+ **options,
494
+ }.compact,
495
+ )
496
+ end
497
+
498
+ def define_standard_param(name, as:, default:, of:, optional:, options:, resolved_enum:, shape:, type:, &block)
499
+ resolved_of = resolve_of(of, type, &block)
500
+ resolved_shape = resolve_shape(shape, type, &block)
501
+
502
+ @params[name] = (@params[name] || {}).merge(
503
+ {
504
+ as:,
505
+ default:,
506
+ enum: resolved_enum,
507
+ name:,
508
+ of: resolved_of,
509
+ optional:,
510
+ type:,
511
+ **options,
512
+ }.compact,
513
+ )
514
+
515
+ @params[name][:shape] = resolved_shape if resolved_shape
516
+ end
517
+
518
+ def resolve_of(of, type, &block)
519
+ return nil unless type == :array
520
+
521
+ if block_given?
522
+ element = Element.new(@contract_class)
523
+ block.arity.positive? ? yield(element) : element.instance_eval(&block)
524
+ element.validate!
525
+ element
526
+ elsif of.is_a?(Symbol)
527
+ wrap_symbol_in_element(of)
528
+ else
529
+ of
530
+ end
531
+ end
532
+
533
+ def resolve_shape(shape, type, &block)
534
+ return nil if type == :array
535
+
536
+ if shape
537
+ shape
538
+ elsif block_given?
539
+ nested_shape = Object.new(@contract_class, action_name: @action_name)
540
+ block.arity.positive? ? yield(nested_shape) : nested_shape.instance_eval(&block)
541
+ nested_shape
542
+ end
543
+ end
544
+
545
+ def wrap_symbol_in_element(type_symbol)
546
+ element = Element.new(@contract_class)
547
+ element.of(type_symbol)
548
+ element
549
+ end
550
+
551
+ def resolve_enum(enum)
552
+ return nil if enum.nil?
553
+ return enum if enum.is_a?(Array)
554
+
555
+ raise ConfigurationError, "enum must be a Symbol (reference) or Array (inline values), got #{enum.class}" unless enum.is_a?(Symbol)
556
+
557
+ unless @contract_class.enum?(enum)
558
+ raise ConfigurationError,
559
+ "Enum :#{enum} not found. Define it using `enum :#{enum}, %w[...]` in definition scope."
560
+ end
561
+
562
+ enum
563
+ end
564
+ end
565
+ end
566
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ class RequestParser
6
+ class Result
7
+ attr_reader :issues,
8
+ :request
9
+
10
+ def initialize(issues: [], request:)
11
+ @request = request
12
+ @issues = issues
13
+ end
14
+
15
+ def valid?
16
+ issues.empty?
17
+ end
18
+
19
+ def invalid?
20
+ issues.any?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ class RequestParser
6
+ class << self
7
+ def parse(contract_class, action_name, request, coerce: false)
8
+ new(contract_class, action_name).parse(request, coerce:)
9
+ end
10
+ end
11
+
12
+ def initialize(contract_class, action_name)
13
+ @contract_class = contract_class
14
+ @action_name = action_name.to_sym
15
+ end
16
+
17
+ def parse(request, coerce: false)
18
+ request = coerce_request(request) if coerce
19
+
20
+ parsed_query, query_issues = parse_part(request.query, :query)
21
+ parsed_body, body_issues = parse_part(request.body, :body)
22
+
23
+ Result.new(
24
+ issues: query_issues + body_issues,
25
+ request: Request.new(body: parsed_body, query: parsed_query),
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ def coerce_request(request)
32
+ request
33
+ .transform_query { |query| coerce(query, shape_for(:query)) }
34
+ .transform_body { |body| coerce(body, shape_for(:body)) }
35
+ end
36
+
37
+ def parse_part(data, part_type)
38
+ shape = shape_for(part_type)
39
+ return [{}, []] if shape.nil? && data.blank?
40
+ return [data, []] unless shape
41
+
42
+ validated = shape.validate(data)
43
+
44
+ return [{}, validated.issues] if validated.invalid?
45
+
46
+ [shape.transform(shape.deserialize(validated.params)), []]
47
+ end
48
+
49
+ def action
50
+ @action ||= @contract_class.action_for(@action_name)
51
+ end
52
+
53
+ def shape_for(part_type)
54
+ return unless action
55
+
56
+ case part_type
57
+ when :query
58
+ action.request.query
59
+ when :body
60
+ action.request.body
61
+ end
62
+ end
63
+
64
+ def coerce(data, shape)
65
+ return data unless shape
66
+ return data unless data.is_a?(Hash)
67
+
68
+ shape.coerce(data)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ class ResponseParser
6
+ class Result
7
+ attr_reader :issues,
8
+ :response
9
+
10
+ def initialize(issues: [], response:)
11
+ @response = response
12
+ @issues = issues
13
+ end
14
+
15
+ def valid?
16
+ issues.empty?
17
+ end
18
+
19
+ def invalid?
20
+ issues.any?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end