apiwork 0.0.0.pre → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +2 -2
- data/README.md +117 -1
- data/Rakefile +5 -3
- data/app/controllers/apiwork/errors_controller.rb +13 -0
- data/app/controllers/apiwork/exports_controller.rb +22 -0
- data/lib/apiwork/abstractable.rb +26 -0
- data/lib/apiwork/adapter/base.rb +369 -0
- data/lib/apiwork/adapter/builder/api/base.rb +66 -0
- data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
- data/lib/apiwork/adapter/capability/api/base.rb +51 -0
- data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
- data/lib/apiwork/adapter/capability/base.rb +291 -0
- data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
- data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
- data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
- data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
- data/lib/apiwork/adapter/capability/result.rb +21 -0
- data/lib/apiwork/adapter/capability/runner.rb +56 -0
- data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
- data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
- data/lib/apiwork/adapter/registry.rb +16 -0
- data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
- data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
- data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
- data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
- data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
- data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
- data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
- data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
- data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
- data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
- data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
- data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
- data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
- data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
- data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
- data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
- data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
- data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
- data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
- data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
- data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
- data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
- data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
- data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
- data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
- data/lib/apiwork/adapter/standard.rb +22 -0
- data/lib/apiwork/adapter/wrapper/base.rb +70 -0
- data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
- data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
- data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
- data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
- data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
- data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
- data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
- data/lib/apiwork/adapter.rb +50 -0
- data/lib/apiwork/api/base.rb +802 -0
- data/lib/apiwork/api/element.rb +110 -0
- data/lib/apiwork/api/enum_registry/definition.rb +51 -0
- data/lib/apiwork/api/enum_registry.rb +98 -0
- data/lib/apiwork/api/info/contact.rb +67 -0
- data/lib/apiwork/api/info/license.rb +50 -0
- data/lib/apiwork/api/info/server.rb +50 -0
- data/lib/apiwork/api/info.rb +221 -0
- data/lib/apiwork/api/object.rb +235 -0
- data/lib/apiwork/api/registry.rb +33 -0
- data/lib/apiwork/api/representation_registry.rb +76 -0
- data/lib/apiwork/api/resource/action.rb +41 -0
- data/lib/apiwork/api/resource.rb +648 -0
- data/lib/apiwork/api/router.rb +104 -0
- data/lib/apiwork/api/type_registry/definition.rb +117 -0
- data/lib/apiwork/api/type_registry.rb +99 -0
- data/lib/apiwork/api/union.rb +49 -0
- data/lib/apiwork/api.rb +85 -0
- data/lib/apiwork/configurable.rb +71 -0
- data/lib/apiwork/configuration/option.rb +125 -0
- data/lib/apiwork/configuration/validatable.rb +25 -0
- data/lib/apiwork/configuration.rb +95 -0
- data/lib/apiwork/configuration_error.rb +6 -0
- data/lib/apiwork/constraint_error.rb +20 -0
- data/lib/apiwork/contract/action/request.rb +79 -0
- data/lib/apiwork/contract/action/response.rb +87 -0
- data/lib/apiwork/contract/action.rb +258 -0
- data/lib/apiwork/contract/base.rb +714 -0
- data/lib/apiwork/contract/element.rb +130 -0
- data/lib/apiwork/contract/object/coercer.rb +194 -0
- data/lib/apiwork/contract/object/deserializer.rb +101 -0
- data/lib/apiwork/contract/object/transformer.rb +95 -0
- data/lib/apiwork/contract/object/validator/result.rb +27 -0
- data/lib/apiwork/contract/object/validator.rb +734 -0
- data/lib/apiwork/contract/object.rb +566 -0
- data/lib/apiwork/contract/request_parser/result.rb +25 -0
- data/lib/apiwork/contract/request_parser.rb +72 -0
- data/lib/apiwork/contract/response_parser/result.rb +25 -0
- data/lib/apiwork/contract/response_parser.rb +35 -0
- data/lib/apiwork/contract/union.rb +56 -0
- data/lib/apiwork/contract_error.rb +9 -0
- data/lib/apiwork/controller.rb +300 -0
- data/lib/apiwork/domain_error.rb +13 -0
- data/lib/apiwork/element.rb +386 -0
- data/lib/apiwork/engine.rb +20 -0
- data/lib/apiwork/error.rb +6 -0
- data/lib/apiwork/error_code/definition.rb +63 -0
- data/lib/apiwork/error_code/registry.rb +18 -0
- data/lib/apiwork/error_code.rb +132 -0
- data/lib/apiwork/export/base.rb +291 -0
- data/lib/apiwork/export/open_api.rb +600 -0
- data/lib/apiwork/export/pipeline/writer.rb +66 -0
- data/lib/apiwork/export/pipeline.rb +84 -0
- data/lib/apiwork/export/registry.rb +16 -0
- data/lib/apiwork/export/surface_resolver.rb +189 -0
- data/lib/apiwork/export/type_analysis.rb +170 -0
- data/lib/apiwork/export/type_script.rb +23 -0
- data/lib/apiwork/export/type_script_mapper.rb +349 -0
- data/lib/apiwork/export/zod.rb +39 -0
- data/lib/apiwork/export/zod_mapper.rb +421 -0
- data/lib/apiwork/export.rb +80 -0
- data/lib/apiwork/http_error.rb +16 -0
- data/lib/apiwork/introspection/action/request.rb +66 -0
- data/lib/apiwork/introspection/action/response.rb +57 -0
- data/lib/apiwork/introspection/action.rb +124 -0
- data/lib/apiwork/introspection/api/info/contact.rb +59 -0
- data/lib/apiwork/introspection/api/info/license.rb +49 -0
- data/lib/apiwork/introspection/api/info/server.rb +50 -0
- data/lib/apiwork/introspection/api/info.rb +107 -0
- data/lib/apiwork/introspection/api/resource.rb +83 -0
- data/lib/apiwork/introspection/api.rb +92 -0
- data/lib/apiwork/introspection/contract.rb +63 -0
- data/lib/apiwork/introspection/dump/action.rb +101 -0
- data/lib/apiwork/introspection/dump/api.rb +119 -0
- data/lib/apiwork/introspection/dump/contract.rb +129 -0
- data/lib/apiwork/introspection/dump/param.rb +486 -0
- data/lib/apiwork/introspection/dump/resource.rb +112 -0
- data/lib/apiwork/introspection/dump/type.rb +339 -0
- data/lib/apiwork/introspection/dump.rb +17 -0
- data/lib/apiwork/introspection/enum.rb +63 -0
- data/lib/apiwork/introspection/error_code.rb +44 -0
- data/lib/apiwork/introspection/param/array.rb +88 -0
- data/lib/apiwork/introspection/param/base.rb +285 -0
- data/lib/apiwork/introspection/param/binary.rb +73 -0
- data/lib/apiwork/introspection/param/boolean.rb +73 -0
- data/lib/apiwork/introspection/param/date.rb +73 -0
- data/lib/apiwork/introspection/param/date_time.rb +73 -0
- data/lib/apiwork/introspection/param/decimal.rb +121 -0
- data/lib/apiwork/introspection/param/integer.rb +131 -0
- data/lib/apiwork/introspection/param/literal.rb +45 -0
- data/lib/apiwork/introspection/param/number.rb +121 -0
- data/lib/apiwork/introspection/param/object.rb +59 -0
- data/lib/apiwork/introspection/param/reference.rb +45 -0
- data/lib/apiwork/introspection/param/string.rb +122 -0
- data/lib/apiwork/introspection/param/time.rb +73 -0
- data/lib/apiwork/introspection/param/union.rb +57 -0
- data/lib/apiwork/introspection/param/unknown.rb +26 -0
- data/lib/apiwork/introspection/param/uuid.rb +73 -0
- data/lib/apiwork/introspection/param.rb +31 -0
- data/lib/apiwork/introspection/type.rb +129 -0
- data/lib/apiwork/introspection.rb +28 -0
- data/lib/apiwork/issue.rb +80 -0
- data/lib/apiwork/json_pointer.rb +21 -0
- data/lib/apiwork/object.rb +1618 -0
- data/lib/apiwork/reference_generator.rb +638 -0
- data/lib/apiwork/registry.rb +56 -0
- data/lib/apiwork/representation/association.rb +391 -0
- data/lib/apiwork/representation/attribute.rb +335 -0
- data/lib/apiwork/representation/base.rb +819 -0
- data/lib/apiwork/representation/deserializer.rb +95 -0
- data/lib/apiwork/representation/element.rb +128 -0
- data/lib/apiwork/representation/inheritance.rb +78 -0
- data/lib/apiwork/representation/model_detector.rb +75 -0
- data/lib/apiwork/representation/root_key.rb +35 -0
- data/lib/apiwork/representation/serializer.rb +127 -0
- data/lib/apiwork/request.rb +79 -0
- data/lib/apiwork/response.rb +56 -0
- data/lib/apiwork/union.rb +102 -0
- data/lib/apiwork/version.rb +2 -2
- data/lib/apiwork.rb +61 -3
- data/lib/generators/apiwork/api_generator.rb +38 -0
- data/lib/generators/apiwork/contract_generator.rb +25 -0
- data/lib/generators/apiwork/install_generator.rb +27 -0
- data/lib/generators/apiwork/representation_generator.rb +25 -0
- data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
- data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
- data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
- data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
- data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
- data/lib/tasks/apiwork.rake +102 -0
- metadata +319 -19
- data/.rubocop.yml +0 -8
- 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
|