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,714 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module Contract
5
+ # @api public
6
+ # Base class for contracts.
7
+ #
8
+ # Validates requests and defines response shapes. Drives type generation and
9
+ # request parsing. Types are defined manually per action or auto-generated
10
+ # from a linked representation.
11
+ #
12
+ # @example Manual contract
13
+ # class InvoiceContract < Apiwork::Contract::Base
14
+ # action :create do
15
+ # request do
16
+ # body do
17
+ # string :title
18
+ # decimal :amount, min: 0
19
+ # end
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # @example With representation
25
+ # class InvoiceContract < Apiwork::Contract::Base
26
+ # representation InvoiceRepresentation
27
+ # end
28
+ #
29
+ # @!scope class
30
+ # @!method abstract!
31
+ # @api public
32
+ # Marks this contract as abstract.
33
+ #
34
+ # Abstract contracts serve as base classes for other contracts.
35
+ # Use this when creating application-wide base contracts that define
36
+ # shared imports or configuration. Subclasses automatically become non-abstract.
37
+ # @return [void]
38
+ # @example Application base contract
39
+ # class ApplicationContract < Apiwork::Contract::Base
40
+ # abstract!
41
+ # end
42
+ #
43
+ # @!method abstract?
44
+ # @api public
45
+ # Whether this contract is abstract.
46
+ # @return [Boolean]
47
+ class Base
48
+ include Abstractable
49
+
50
+ # @!attribute [r] issues
51
+ # @api public
52
+ # The issues for this contract.
53
+ # @return [Array<Issue>]
54
+ attr_reader :action_name,
55
+ :issues,
56
+ :request
57
+
58
+ # @!method body
59
+ # @api public
60
+ # The body for this contract.
61
+ #
62
+ # Use this in controller actions to access validated request data.
63
+ # Contains type-coerced values matching your contract definition.
64
+ # Invalid requests are rejected before the action runs.
65
+ #
66
+ # @return [Hash]
67
+ #
68
+ # @example
69
+ # def create
70
+ # Invoice.create!(contract.body[:invoice])
71
+ # end
72
+ #
73
+ # @!method query
74
+ # @api public
75
+ # The query for this contract.
76
+ #
77
+ # Use this in controller actions to access validated request data.
78
+ # Contains type-coerced values matching your contract definition.
79
+ # Invalid requests are rejected before the action runs.
80
+ #
81
+ # @return [Hash]
82
+ #
83
+ # @example
84
+ # def index
85
+ # Invoice.where(status: contract.query[:status])
86
+ # end
87
+ delegate :body,
88
+ :query,
89
+ to: :request
90
+
91
+ class << self
92
+ # @api public
93
+ # The representation class for this contract.
94
+ #
95
+ # @return [Class<Representation::Base>, nil]
96
+ # @see .representation
97
+ attr_reader :representation_class
98
+
99
+ # @api public
100
+ # Prefixes types, enums, and unions in introspection output.
101
+ #
102
+ # Must be unique within the API. Derived from the contract class
103
+ # name when not set (e.g., `RecurringInvoiceContract` becomes
104
+ # `recurring_invoice`).
105
+ #
106
+ # @param value [Symbol, String, nil] (nil)
107
+ # The identifier prefix.
108
+ # @return [String, nil]
109
+ #
110
+ # @example
111
+ # class InvoiceContract < Apiwork::Contract::Base
112
+ # identifier :billing
113
+ #
114
+ # object :address do
115
+ # string :street
116
+ # end
117
+ # # In introspection: :address becomes :billing_address
118
+ # end
119
+ def identifier(value = nil)
120
+ return @identifier if value.nil?
121
+
122
+ @identifier = value.to_s
123
+ end
124
+
125
+ # @api public
126
+ # Configures the representation class for this contract.
127
+ #
128
+ # Adapters use the representation to auto-generate request/response
129
+ # types. Use {.representation_class} to retrieve.
130
+ #
131
+ # @param klass [Class<Representation::Base>]
132
+ # The representation class.
133
+ # @return [void]
134
+ # @raise [ArgumentError] if klass is not a Representation subclass
135
+ #
136
+ # @example
137
+ # class InvoiceContract < Apiwork::Contract::Base
138
+ # representation InvoiceRepresentation
139
+ # end
140
+ def representation(klass)
141
+ unless klass.is_a?(Class)
142
+ raise ConfigurationError,
143
+ "representation must be a Representation class, got #{klass.class}. " \
144
+ "Use: representation InvoiceRepresentation (not 'InvoiceRepresentation' or :invoice)"
145
+ end
146
+ unless klass < Representation::Base
147
+ raise ConfigurationError,
148
+ 'representation must be a Representation class (subclass of Apiwork::Representation::Base), ' \
149
+ "got #{klass}"
150
+ end
151
+
152
+ @representation_class = klass
153
+ end
154
+
155
+ # @api public
156
+ # Defines or extends an object type for this contract.
157
+ #
158
+ # Subclasses inherit parent types. In introspection, types are prefixed with the
159
+ # contract's {.identifier} (e.g., `:item` in `InvoiceContract` becomes `:invoice_item`).
160
+ #
161
+ # Multiple calls with the same name merge fields (declaration merging).
162
+ #
163
+ # @param name [Symbol]
164
+ # The object name.
165
+ # @param deprecated [Boolean] (false)
166
+ # Whether deprecated. Metadata included in exports.
167
+ # @param description [String, nil] (nil)
168
+ # The description. Metadata included in exports.
169
+ # @param example [Object, nil] (nil)
170
+ # The example. Metadata included in exports.
171
+ # @yieldparam object [API::Object]
172
+ # @return [void]
173
+ #
174
+ # @example Define and reference
175
+ # object :item do
176
+ # string :description
177
+ # decimal :amount
178
+ # end
179
+ #
180
+ # action :create do
181
+ # request do
182
+ # body do
183
+ # array :items do
184
+ # reference :item
185
+ # end
186
+ # end
187
+ # end
188
+ # end
189
+ #
190
+ # @example Different shapes for request and response
191
+ # object :invoice do
192
+ # uuid :id
193
+ # string :number
194
+ # string :status
195
+ # end
196
+ #
197
+ # object :invoice_payload do
198
+ # string :number
199
+ # string :status
200
+ # end
201
+ #
202
+ # action :create do
203
+ # request do
204
+ # body do
205
+ # reference :invoice, to: :invoice_payload
206
+ # end
207
+ # end
208
+ # response do
209
+ # body do
210
+ # reference :invoice
211
+ # end
212
+ # end
213
+ # end
214
+ def object(
215
+ name,
216
+ deprecated: false,
217
+ description: nil,
218
+ example: nil,
219
+ &block
220
+ )
221
+ api_class.register_object(
222
+ name,
223
+ deprecated:,
224
+ description:,
225
+ example:,
226
+ scope: self,
227
+ &block
228
+ )
229
+ end
230
+
231
+ # @api public
232
+ # Defines a fragment type for this contract.
233
+ #
234
+ # Fragments are only available for merging into other types and never appear as standalone types. Use
235
+ # fragments to define reusable field groups.
236
+ #
237
+ # @param name [Symbol]
238
+ # The fragment name.
239
+ # @yieldparam object [API::Object]
240
+ # @return [void]
241
+ #
242
+ # @example Reusable timestamps
243
+ # fragment :timestamps do
244
+ # datetime :created_at
245
+ # datetime :updated_at
246
+ # end
247
+ #
248
+ # object :invoice do
249
+ # merge :timestamps
250
+ # string :number
251
+ # end
252
+ def fragment(name, &block)
253
+ api_class.register_fragment(name, scope: self, &block)
254
+ end
255
+
256
+ # @api public
257
+ # Defines or extends an enum for this contract.
258
+ #
259
+ # Subclasses inherit parent enums. In introspection, enums are prefixed with the
260
+ # contract's {.identifier} (e.g., `:status` in `InvoiceContract` becomes `:invoice_status`).
261
+ #
262
+ # Multiple calls with the same name merge values (declaration merging).
263
+ #
264
+ # @param name [Symbol]
265
+ # The enum name.
266
+ # @param deprecated [Boolean] (false)
267
+ # Whether deprecated. Metadata included in exports.
268
+ # @param description [String, nil] (nil)
269
+ # The description. Metadata included in exports.
270
+ # @param example [String, nil] (nil)
271
+ # The example. Metadata included in exports.
272
+ # @param values [Array<String>, nil] (nil)
273
+ # The allowed values.
274
+ # @return [void]
275
+ #
276
+ # @example Define and reference
277
+ # enum :status, values: %w[draft sent paid]
278
+ #
279
+ # action :update do
280
+ # request do
281
+ # body do
282
+ # string :status, enum: :status
283
+ # end
284
+ # end
285
+ # end
286
+ #
287
+ # @example Inline values (without separate definition)
288
+ # action :index do
289
+ # request do
290
+ # query do
291
+ # string? :priority, enum: %w[low medium high]
292
+ # end
293
+ # end
294
+ # end
295
+ def enum(
296
+ name,
297
+ deprecated: false,
298
+ description: nil,
299
+ example: nil,
300
+ values: nil
301
+ )
302
+ api_class.register_enum(
303
+ name,
304
+ deprecated:,
305
+ description:,
306
+ example:,
307
+ values:,
308
+ scope: self,
309
+ )
310
+ end
311
+
312
+ # @api public
313
+ # Defines or extends a discriminated union for this contract.
314
+ #
315
+ # Subclasses inherit parent unions. In introspection, unions are prefixed with the
316
+ # contract's {.identifier} (e.g., `:payment_method` in `InvoiceContract` becomes `:invoice_payment_method`).
317
+ #
318
+ # Multiple calls with the same name merge variants (declaration merging).
319
+ #
320
+ # @param name [Symbol]
321
+ # The union name.
322
+ # @param deprecated [Boolean] (false)
323
+ # Whether deprecated. Metadata included in exports.
324
+ # @param description [String, nil] (nil)
325
+ # The description. Metadata included in exports.
326
+ # @param discriminator [Symbol, nil] (nil)
327
+ # The discriminator field name.
328
+ # @param example [Object, nil] (nil)
329
+ # The example. Metadata included in exports.
330
+ # @yieldparam union [API::Union]
331
+ # @return [void]
332
+ #
333
+ # @example Define and reference
334
+ # union :payment_method, discriminator: :type do
335
+ # variant tag: 'card' do
336
+ # object do
337
+ # string :last_four
338
+ # end
339
+ # end
340
+ # variant tag: 'bank_transfer' do
341
+ # object do
342
+ # string :bank_name
343
+ # string :account_number
344
+ # end
345
+ # end
346
+ # end
347
+ #
348
+ # action :create do
349
+ # request do
350
+ # body do
351
+ # reference :payment_method
352
+ # end
353
+ # end
354
+ # end
355
+ def union(
356
+ name,
357
+ deprecated: false,
358
+ description: nil,
359
+ discriminator: nil,
360
+ example: nil,
361
+ &block
362
+ )
363
+ api_class.register_union(
364
+ name,
365
+ deprecated:,
366
+ description:,
367
+ discriminator:,
368
+ example:,
369
+ scope: self,
370
+ &block
371
+ )
372
+ end
373
+
374
+ # @api public
375
+ # Imports types from another contract for reuse.
376
+ #
377
+ # Imported types are accessed with a prefix matching the alias.
378
+ #
379
+ # @param klass [Class<Contract::Base>]
380
+ # The contract class to import types from.
381
+ # @param as [Symbol]
382
+ # The alias prefix.
383
+ # @return [void]
384
+ # @raise [ArgumentError] if klass is not a Contract subclass
385
+ # @raise [ArgumentError] if as is not a Symbol
386
+ #
387
+ # @example
388
+ # import UserContract, as: :user
389
+ # # UserContract's :address becomes :user_address
390
+ def import(klass, as:)
391
+ unless klass.is_a?(Class)
392
+ raise ConfigurationError,
393
+ "import must be a Class constant, got #{klass.class}. " \
394
+ "Use: import UserContract, as: :user (not 'UserContract' or :user_contract)"
395
+ end
396
+
397
+ unless klass < Contract::Base
398
+ raise ConfigurationError,
399
+ 'import must be a Contract class (subclass of Apiwork::Contract::Base), ' \
400
+ "got #{klass}"
401
+ end
402
+
403
+ unless as.is_a?(Symbol)
404
+ raise ConfigurationError,
405
+ "import alias must be a Symbol, got #{as.class}. " \
406
+ 'Use: import UserContract, as: :user'
407
+ end
408
+
409
+ imports[as] = klass
410
+
411
+ return if klass.building?
412
+ return unless klass.representation? && klass.api_class
413
+
414
+ klass.building = true
415
+ begin
416
+ klass.api_class.ensure_contract_built!(klass)
417
+ ensure
418
+ klass.building = false
419
+ end
420
+ end
421
+
422
+ # @api public
423
+ # Defines or extends an action on this contract.
424
+ #
425
+ # Multiple calls with the same name merge definitions (declaration merging).
426
+ #
427
+ # @param name [Symbol]
428
+ # The action name. Matches your controller action.
429
+ # @param replace [Boolean] (false)
430
+ # Whether to discard any existing definition and start fresh. Use when overriding
431
+ # auto-generated actions from representation.
432
+ # @yieldparam action [Contract::Action]
433
+ # @return [Contract::Action]
434
+ #
435
+ # @example Query parameters
436
+ # action :index do
437
+ # request do
438
+ # query do
439
+ # string? :search
440
+ # integer? :page
441
+ # end
442
+ # end
443
+ # end
444
+ #
445
+ # @example Request body with custom type
446
+ # action :create do
447
+ # request do
448
+ # body do
449
+ # reference :invoice, to: :invoice_payload
450
+ # end
451
+ # end
452
+ # response do
453
+ # body do
454
+ # reference :invoice
455
+ # end
456
+ # end
457
+ # end
458
+ #
459
+ # @example Override auto-generated action
460
+ # action :destroy, replace: true do
461
+ # response do
462
+ # body do
463
+ # reference :invoice
464
+ # end
465
+ # end
466
+ # end
467
+ #
468
+ # @example No content response
469
+ # action :destroy do
470
+ # response { no_content! }
471
+ # end
472
+ def action(name, replace: false, &block)
473
+ name = name.to_sym
474
+
475
+ action = if replace
476
+ Action.new(self, name, replace: true)
477
+ else
478
+ actions[name] ||= Action.new(self, name)
479
+ end
480
+
481
+ if block_given?
482
+ block.arity.positive? ? yield(action) : action.instance_eval(&block)
483
+ end
484
+ actions[name] = action
485
+ end
486
+
487
+ # @api public
488
+ # Returns introspection data for this contract.
489
+ #
490
+ # @param expand [Boolean] (false)
491
+ # Whether to expand all types inline.
492
+ # @param locale [Symbol, nil] (nil)
493
+ # The locale for translations.
494
+ # @return [Introspection::Contract]
495
+ #
496
+ # @example
497
+ # InvoiceContract.introspect
498
+ def introspect(expand: false, locale: nil)
499
+ api_class.introspect_contract(self, expand:, locale:)
500
+ end
501
+
502
+ attr_writer :building
503
+
504
+ def actions
505
+ @actions ||= {}
506
+ end
507
+
508
+ def imports
509
+ @imports ||= {}
510
+ end
511
+
512
+ def building?
513
+ @building
514
+ end
515
+
516
+ def synthetic_contracts
517
+ @synthetic_contracts ||= {}
518
+ end
519
+
520
+ def synthetic?
521
+ @synthetic == true
522
+ end
523
+
524
+ def contract_for(representation_class)
525
+ return nil unless representation_class&.name
526
+
527
+ contract_name = representation_class.name.sub(/Representation\z/, 'Contract')
528
+ contract_class = contract_name.safe_constantize
529
+
530
+ return contract_class if contract_class.is_a?(Class) && contract_class < Contract::Base
531
+
532
+ synthetic_contracts[representation_class] ||= build_synthetic_contract(representation_class, api_class)
533
+ end
534
+
535
+ def build_synthetic_contract(representation_class, api_class)
536
+ Class.new(Contract::Base) do
537
+ @synthetic = true
538
+ @representation_class = representation_class
539
+ @api_class = api_class
540
+ end
541
+ end
542
+
543
+ def representation?
544
+ representation_class.present?
545
+ end
546
+
547
+ def scope_prefix
548
+ return @identifier if @identifier
549
+
550
+ if name
551
+ name
552
+ .demodulize
553
+ .delete_suffix('Contract')
554
+ .underscore
555
+ elsif representation_class
556
+ representation_class.name
557
+ .demodulize
558
+ .delete_suffix('Representation')
559
+ .underscore
560
+ end
561
+ end
562
+
563
+ def resolve_custom_type(type_name, visited: Set.new)
564
+ raise ConfigurationError, "Circular import detected while resolving :#{type_name}" if visited.include?(self)
565
+
566
+ if api_class
567
+ result = api_class.type_definition(type_name, scope: self)
568
+ return result if result
569
+ end
570
+
571
+ result = resolve_imported_type(type_name, visited: visited.dup.add(self))
572
+ return result if result
573
+
574
+ resolve_parent_type(type_name, visited: visited.dup.add(self))
575
+ end
576
+
577
+ def action_for(action_name)
578
+ api_class.ensure_contract_built!(self)
579
+
580
+ action_name = action_name.to_sym
581
+ actions[action_name]
582
+ end
583
+
584
+ def api_class
585
+ return @api_class if @api_class
586
+ return nil unless name
587
+
588
+ namespace = name.deconstantize
589
+ return nil if namespace.blank?
590
+
591
+ API.find("/#{namespace.underscore.tr('::', '/')}")
592
+ end
593
+
594
+ def parse_response(response, action)
595
+ ResponseParser.parse(self, action, response)
596
+ end
597
+
598
+ def type?(name)
599
+ resolve_custom_type(name).present?
600
+ end
601
+
602
+ def enum?(name)
603
+ enum_values(name).present?
604
+ end
605
+
606
+ def enum_values(enum_name, visited: Set.new)
607
+ return nil if visited.include?(self)
608
+
609
+ if api_class
610
+ result = api_class.enum_values(enum_name, scope: self)
611
+ return result if result
612
+ end
613
+
614
+ result = resolve_imported_enum_values(enum_name, visited: visited.dup.add(self))
615
+ return result if result
616
+
617
+ resolve_parent_enum_values(enum_name, visited: visited.dup.add(self))
618
+ end
619
+
620
+ def scoped_type_name(name)
621
+ api_class.scoped_type_name(self, name)
622
+ end
623
+
624
+ def scoped_enum_name(name)
625
+ api_class.scoped_enum_name(self, name)
626
+ end
627
+
628
+ private
629
+
630
+ def resolve_imported_type(type_name, visited:)
631
+ type_string = type_name.to_s
632
+
633
+ imports.each do |import_alias, imported_contract|
634
+ prefix = "#{import_alias}_"
635
+ next unless type_string.start_with?(prefix)
636
+
637
+ unprefixed_name = type_string.delete_prefix(prefix).to_sym
638
+ result = imported_contract.resolve_custom_type(unprefixed_name, visited:)
639
+ return result if result
640
+ end
641
+
642
+ nil
643
+ end
644
+
645
+ def resolve_parent_type(type_name, visited:)
646
+ parent = superclass
647
+ return nil unless parent < Contract::Base
648
+
649
+ parent.resolve_custom_type(type_name, visited:)
650
+ end
651
+
652
+ def resolve_imported_enum_values(enum_name, visited:)
653
+ enum_string = enum_name.to_s
654
+
655
+ imports.each do |import_alias, imported_contract|
656
+ prefix = "#{import_alias}_"
657
+ next unless enum_string.start_with?(prefix)
658
+
659
+ unprefixed_name = enum_string.delete_prefix(prefix).to_sym
660
+ result = imported_contract.enum_values(unprefixed_name, visited:)
661
+ return result if result
662
+ end
663
+
664
+ nil
665
+ end
666
+
667
+ def resolve_parent_enum_values(enum_name, visited:)
668
+ parent = superclass
669
+ return nil unless parent < Contract::Base
670
+
671
+ parent.enum_values(enum_name, visited:)
672
+ end
673
+ end
674
+
675
+ def initialize(action_name, request, coerce: false)
676
+ request = normalize_request(request)
677
+ result = RequestParser.parse(self.class, action_name, request, coerce:)
678
+ @request = prepare_request(result.request)
679
+ @action_name = action_name.to_sym
680
+ @issues = result.issues
681
+ end
682
+
683
+ # @api public
684
+ # Whether this contract is valid.
685
+ #
686
+ # @return [Boolean]
687
+ def valid?
688
+ issues.empty?
689
+ end
690
+
691
+ # @api public
692
+ # Whether this contract is invalid.
693
+ #
694
+ # @return [Boolean]
695
+ def invalid?
696
+ issues.any?
697
+ end
698
+
699
+ private
700
+
701
+ def normalize_request(request)
702
+ api_class = self.class.api_class
703
+ result = api_class.normalize_request(request)
704
+ api_class.adapter.apply_request_transformers(result, phase: :before)
705
+ end
706
+
707
+ def prepare_request(request)
708
+ api_class = self.class.api_class
709
+ result = api_class.prepare_request(request)
710
+ api_class.adapter.apply_request_transformers(result, phase: :after)
711
+ end
712
+ end
713
+ end
714
+ end