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,802 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module API
|
|
5
|
+
# @api public
|
|
6
|
+
# Base class for API definitions.
|
|
7
|
+
#
|
|
8
|
+
# Created via {API.define}. Configure resources, types, enums,
|
|
9
|
+
# adapters, and exports. Each API is mounted at a unique path.
|
|
10
|
+
#
|
|
11
|
+
# @example Define an API
|
|
12
|
+
# Apiwork::API.define '/api/v1' do
|
|
13
|
+
# key_format :camel
|
|
14
|
+
#
|
|
15
|
+
# resources :invoices do
|
|
16
|
+
# resources :items
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
class Base
|
|
20
|
+
VALID_FORMATS = %i[keep camel pascal kebab underscore].freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# @!attribute [r] base_path
|
|
24
|
+
# @api public
|
|
25
|
+
# The base path for this API.
|
|
26
|
+
#
|
|
27
|
+
# @return [String]
|
|
28
|
+
attr_reader :base_path,
|
|
29
|
+
:enum_registry,
|
|
30
|
+
:export_configs,
|
|
31
|
+
:representation_registry,
|
|
32
|
+
:root_resource,
|
|
33
|
+
:type_registry
|
|
34
|
+
|
|
35
|
+
# @api public
|
|
36
|
+
# Configures key transformation for this API.
|
|
37
|
+
#
|
|
38
|
+
# Transforms JSON keys in request bodies, response bodies, and query parameters. Incoming requests are
|
|
39
|
+
# normalized to underscore internally, so controllers always receive `params[:user_name]` regardless of
|
|
40
|
+
# format.
|
|
41
|
+
#
|
|
42
|
+
# With `:camel`, `user_name` becomes `userName`. With `:pascal`, `user_name` becomes `UserName`.
|
|
43
|
+
# With `:kebab`, `user_name` becomes `user-name`.
|
|
44
|
+
#
|
|
45
|
+
# @param format [Symbol, nil] (nil) [:camel, :kebab, :keep, :pascal, :underscore]
|
|
46
|
+
# The key format. Default is `:keep`.
|
|
47
|
+
# @return [Symbol, nil]
|
|
48
|
+
# @raise [ConfigurationError] if format is invalid
|
|
49
|
+
#
|
|
50
|
+
# @example camelCase keys
|
|
51
|
+
# key_format :camel
|
|
52
|
+
#
|
|
53
|
+
# # Client sends: { "userName": "alice" }
|
|
54
|
+
# # Controller receives: params[:user_name]
|
|
55
|
+
# # Response: { "userName": "alice", "createdAt": "2024-01-01" }
|
|
56
|
+
def key_format(format = nil)
|
|
57
|
+
return @key_format if format.nil?
|
|
58
|
+
|
|
59
|
+
raise ConfigurationError, "key_format must be one of #{VALID_FORMATS}" unless VALID_FORMATS.include?(format)
|
|
60
|
+
|
|
61
|
+
@key_format = format
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @api public
|
|
65
|
+
# Configures URL path transformation for this API.
|
|
66
|
+
#
|
|
67
|
+
# Transforms URL path segments: base path, resource paths, action paths, and explicit `path:` options.
|
|
68
|
+
# Path parameters like `:id` and `:user_id` are not transformed. Controllers and params remain underscore
|
|
69
|
+
# internally.
|
|
70
|
+
#
|
|
71
|
+
# With `:kebab`, `/api/user_profiles/:id` becomes `/api/user-profiles/:id`.
|
|
72
|
+
# With `:pascal`, `/api/user_profiles/:id` becomes `/api/UserProfiles/:id`.
|
|
73
|
+
#
|
|
74
|
+
# @param format [Symbol, nil] (nil) [:camel, :kebab, :keep, :pascal, :underscore]
|
|
75
|
+
# The path format. Default is `:keep`.
|
|
76
|
+
# @return [Symbol, nil]
|
|
77
|
+
# @raise [ConfigurationError] if format is invalid
|
|
78
|
+
#
|
|
79
|
+
# @example kebab-case paths
|
|
80
|
+
# path_format :kebab
|
|
81
|
+
#
|
|
82
|
+
# resources :user_profiles
|
|
83
|
+
# # URL: /user-profiles/:id
|
|
84
|
+
# # Controller: UserProfilesController
|
|
85
|
+
# # Params: params[:user_profile]
|
|
86
|
+
def path_format(format = nil)
|
|
87
|
+
return @path_format if format.nil?
|
|
88
|
+
|
|
89
|
+
raise ConfigurationError, "path_format must be one of #{VALID_FORMATS}" unless VALID_FORMATS.include?(format)
|
|
90
|
+
|
|
91
|
+
@path_format = format
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @api public
|
|
95
|
+
# Enables an export for this API.
|
|
96
|
+
#
|
|
97
|
+
# @param name [Symbol]
|
|
98
|
+
# The registered export name. Built-in: :openapi, :typescript, :zod.
|
|
99
|
+
# @yield Block evaluated in export context.
|
|
100
|
+
# @yieldparam export [Configuration]
|
|
101
|
+
# @return [void]
|
|
102
|
+
#
|
|
103
|
+
# @example
|
|
104
|
+
# export :openapi
|
|
105
|
+
# export :typescript do
|
|
106
|
+
# endpoint do
|
|
107
|
+
# mode :always
|
|
108
|
+
# end
|
|
109
|
+
# end
|
|
110
|
+
def export(name, &block)
|
|
111
|
+
unless Export.exists?(name)
|
|
112
|
+
available = Export.keys.join(', ')
|
|
113
|
+
raise ConfigurationError,
|
|
114
|
+
"Unknown export: :#{name}. " \
|
|
115
|
+
"Available: #{available}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
unless @export_configs[name]
|
|
119
|
+
export_class = Export.find!(name)
|
|
120
|
+
|
|
121
|
+
options = Configurable.define(extends: export_class) do
|
|
122
|
+
option :endpoint, type: :hash do
|
|
123
|
+
option :mode, default: :auto, enum: %i[auto always never], type: :symbol
|
|
124
|
+
option :path, type: :string
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
@export_configs[name] = Configuration.new(options)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return unless block
|
|
132
|
+
|
|
133
|
+
block.arity.positive? ? yield(@export_configs[name]) : @export_configs[name].instance_eval(&block)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @api public
|
|
137
|
+
# Sets or configures the adapter for this API.
|
|
138
|
+
#
|
|
139
|
+
# Without arguments, returns the adapter instance. With a block, configures the current adapter.
|
|
140
|
+
# Without a name, the built-in `:standard` adapter is used.
|
|
141
|
+
#
|
|
142
|
+
# Custom adapters must be registered via {Adapter.register} and referenced by their `adapter_name`.
|
|
143
|
+
#
|
|
144
|
+
# @param name [Symbol, nil] (nil)
|
|
145
|
+
# A registered adapter name matching `adapter_name` in the adapter class.
|
|
146
|
+
# @yield Block evaluated in adapter configuration context.
|
|
147
|
+
# @yieldparam config [Configuration]
|
|
148
|
+
# @return [Adapter::Base, void] the adapter instance when called without block
|
|
149
|
+
#
|
|
150
|
+
# @example Configure the default :standard adapter
|
|
151
|
+
# adapter do
|
|
152
|
+
# pagination do
|
|
153
|
+
# default_size 25
|
|
154
|
+
# max_size 100
|
|
155
|
+
# end
|
|
156
|
+
# end
|
|
157
|
+
#
|
|
158
|
+
# @example Use a registered custom adapter
|
|
159
|
+
# adapter :jsonapi
|
|
160
|
+
#
|
|
161
|
+
# @example Use and configure a custom adapter
|
|
162
|
+
# adapter :jsonapi do
|
|
163
|
+
# pagination do
|
|
164
|
+
# strategy :cursor
|
|
165
|
+
# end
|
|
166
|
+
# end
|
|
167
|
+
#
|
|
168
|
+
# @see Adapter::Standard The built-in adapter
|
|
169
|
+
# @see Adapter.register How to register custom adapters
|
|
170
|
+
def adapter(name = nil, &block)
|
|
171
|
+
@adapter_name = name if name.is_a?(Symbol)
|
|
172
|
+
|
|
173
|
+
if block
|
|
174
|
+
block.arity.positive? ? yield(adapter_config) : adapter_config.instance_eval(&block)
|
|
175
|
+
return
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
@adapter ||= adapter_class.new
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @api public
|
|
182
|
+
# Defines a reusable object type.
|
|
183
|
+
#
|
|
184
|
+
# @param name [Symbol]
|
|
185
|
+
# The object name.
|
|
186
|
+
# @param deprecated [Boolean] (false)
|
|
187
|
+
# Whether deprecated. Metadata included in exports.
|
|
188
|
+
# @param description [String, nil] (nil)
|
|
189
|
+
# The description. Metadata included in exports.
|
|
190
|
+
# @param example [Object, nil] (nil)
|
|
191
|
+
# The example. Metadata included in exports.
|
|
192
|
+
# @yieldparam object [API::Object]
|
|
193
|
+
# @return [void]
|
|
194
|
+
#
|
|
195
|
+
# @example
|
|
196
|
+
# object :item do
|
|
197
|
+
# string :description
|
|
198
|
+
# decimal :amount
|
|
199
|
+
# end
|
|
200
|
+
#
|
|
201
|
+
def object(name, deprecated: false, description: nil, example: nil, &block)
|
|
202
|
+
register_object(name, deprecated:, description:, example:, &block)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# @api public
|
|
206
|
+
# Defines a fragment type for composition.
|
|
207
|
+
#
|
|
208
|
+
# Fragments are only available for merging into other types and never appear as standalone types. Use
|
|
209
|
+
# fragments to define reusable field groups.
|
|
210
|
+
#
|
|
211
|
+
# @param name [Symbol]
|
|
212
|
+
# The fragment name.
|
|
213
|
+
# @yieldparam object [API::Object]
|
|
214
|
+
# @return [void]
|
|
215
|
+
#
|
|
216
|
+
# @example Reusable timestamps
|
|
217
|
+
# fragment :timestamps do
|
|
218
|
+
# datetime :created_at
|
|
219
|
+
# datetime :updated_at
|
|
220
|
+
# end
|
|
221
|
+
#
|
|
222
|
+
# object :invoice do
|
|
223
|
+
# merge :timestamps
|
|
224
|
+
# string :number
|
|
225
|
+
# end
|
|
226
|
+
def fragment(name, &block)
|
|
227
|
+
register_fragment(name, &block)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# @api public
|
|
231
|
+
# Defines a reusable enumeration type.
|
|
232
|
+
#
|
|
233
|
+
# @param name [Symbol]
|
|
234
|
+
# The enum name.
|
|
235
|
+
# @param values [Array<String>, nil] (nil)
|
|
236
|
+
# The allowed values.
|
|
237
|
+
# @param description [String, nil] (nil)
|
|
238
|
+
# The description. Metadata included in exports.
|
|
239
|
+
# @param example [String, nil] (nil)
|
|
240
|
+
# The example. Metadata included in exports.
|
|
241
|
+
# @param deprecated [Boolean] (false)
|
|
242
|
+
# Whether deprecated. Metadata included in exports.
|
|
243
|
+
# @return [void]
|
|
244
|
+
#
|
|
245
|
+
# @example
|
|
246
|
+
# enum :status, values: %w[draft sent paid]
|
|
247
|
+
def enum(name, deprecated: false, description: nil, example: nil, values: nil)
|
|
248
|
+
register_enum(name, deprecated:, description:, example:, values:)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# @api public
|
|
252
|
+
# Defines a discriminated union type.
|
|
253
|
+
#
|
|
254
|
+
# @param name [Symbol]
|
|
255
|
+
# The union name.
|
|
256
|
+
# @param deprecated [Boolean] (false)
|
|
257
|
+
# Whether deprecated. Metadata included in exports.
|
|
258
|
+
# @param description [String, nil] (nil)
|
|
259
|
+
# The description. Metadata included in exports.
|
|
260
|
+
# @param discriminator [Symbol, nil] (nil)
|
|
261
|
+
# The discriminator field name.
|
|
262
|
+
# @param example [Object, nil] (nil)
|
|
263
|
+
# The example. Metadata included in exports.
|
|
264
|
+
# @yieldparam union [API::Union]
|
|
265
|
+
# @return [void]
|
|
266
|
+
#
|
|
267
|
+
# @example
|
|
268
|
+
# union :payment_method, discriminator: :type do
|
|
269
|
+
# variant tag: 'card' do
|
|
270
|
+
# object do
|
|
271
|
+
# string :last_four
|
|
272
|
+
# end
|
|
273
|
+
# end
|
|
274
|
+
# end
|
|
275
|
+
def union(name, deprecated: false, description: nil, discriminator: nil, example: nil, &block)
|
|
276
|
+
register_union(name, deprecated:, description:, discriminator:, example:, &block)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# @api public
|
|
280
|
+
# API-wide error codes.
|
|
281
|
+
#
|
|
282
|
+
# Included in generated specs (OpenAPI, etc.) as possible error responses.
|
|
283
|
+
#
|
|
284
|
+
# @param error_code_keys [Array<Symbol>]
|
|
285
|
+
# The registered error code keys.
|
|
286
|
+
# @return [Array<Symbol>]
|
|
287
|
+
# @raise [ConfigurationError] if error code is not registered
|
|
288
|
+
#
|
|
289
|
+
# @example
|
|
290
|
+
# raises :unauthorized, :forbidden, :not_found
|
|
291
|
+
# api_class.raises # => [:unauthorized, :forbidden, :not_found]
|
|
292
|
+
def raises(*error_code_keys)
|
|
293
|
+
return @raises if error_code_keys.empty?
|
|
294
|
+
|
|
295
|
+
error_code_keys = error_code_keys.flatten.uniq
|
|
296
|
+
error_code_keys.each do |error_code_key|
|
|
297
|
+
unless error_code_key.is_a?(Symbol)
|
|
298
|
+
hint = error_code_key.is_a?(Integer) ? " Use :#{ErrorCode.key_for_status(error_code_key)} instead." : ''
|
|
299
|
+
raise ConfigurationError, "raises must be symbols, got #{error_code_key.class}: #{error_code_key}.#{hint}"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
next if ErrorCode.exists?(error_code_key)
|
|
303
|
+
|
|
304
|
+
raise ConfigurationError,
|
|
305
|
+
"Unknown error code :#{error_code_key}. Register it with: " \
|
|
306
|
+
"Apiwork::ErrorCode.register :#{error_code_key}, status: <status>"
|
|
307
|
+
end
|
|
308
|
+
@raises = error_code_keys
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# @api public
|
|
312
|
+
# The info for this API.
|
|
313
|
+
#
|
|
314
|
+
# @yield Block for defining API info.
|
|
315
|
+
# @yieldparam info [Info]
|
|
316
|
+
# @return [Info, nil]
|
|
317
|
+
#
|
|
318
|
+
# @example instance_eval style
|
|
319
|
+
# info do
|
|
320
|
+
# title 'My API'
|
|
321
|
+
# version '1.0.0'
|
|
322
|
+
# end
|
|
323
|
+
#
|
|
324
|
+
# @example yield style
|
|
325
|
+
# info do |info|
|
|
326
|
+
# info.title 'My API'
|
|
327
|
+
# info.version '1.0.0'
|
|
328
|
+
# end
|
|
329
|
+
def info(&block)
|
|
330
|
+
return @info unless block
|
|
331
|
+
|
|
332
|
+
@info = Info.new
|
|
333
|
+
block.arity.positive? ? yield(@info) : @info.instance_eval(&block)
|
|
334
|
+
@info
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# @api public
|
|
338
|
+
# Defines a RESTful resource with standard CRUD actions.
|
|
339
|
+
#
|
|
340
|
+
# This is the main method for declaring API endpoints. Creates
|
|
341
|
+
# routes for index, show, create, update, destroy actions.
|
|
342
|
+
# Nested resources and custom actions can be defined in the block.
|
|
343
|
+
#
|
|
344
|
+
# @param name [Symbol]
|
|
345
|
+
# The resource name (plural).
|
|
346
|
+
# @param concerns [Array<Symbol>, nil] (nil)
|
|
347
|
+
# The concerns to include.
|
|
348
|
+
# @param constraints [Hash, Proc, nil] (nil)
|
|
349
|
+
# The route constraints (regex, lambdas).
|
|
350
|
+
# @param contract [String, nil] (nil)
|
|
351
|
+
# The custom contract path.
|
|
352
|
+
# @param controller [String, nil] (nil)
|
|
353
|
+
# The custom controller path.
|
|
354
|
+
# @param defaults [Hash, nil] (nil)
|
|
355
|
+
# The default parameters for routes.
|
|
356
|
+
# @param except [Array<Symbol>, nil] (nil)
|
|
357
|
+
# The CRUD actions to exclude.
|
|
358
|
+
# @param only [Array<Symbol>, nil] (nil)
|
|
359
|
+
# The CRUD actions to include.
|
|
360
|
+
# @param param [Symbol, nil] (nil)
|
|
361
|
+
# The custom parameter name for ID.
|
|
362
|
+
# @param path [String, nil] (nil)
|
|
363
|
+
# The custom URL path segment.
|
|
364
|
+
# @yield Block for nested resources and custom actions.
|
|
365
|
+
# @yieldparam resource [Resource]
|
|
366
|
+
# @return [void]
|
|
367
|
+
#
|
|
368
|
+
# @example instance_eval style
|
|
369
|
+
# resources :invoices do
|
|
370
|
+
# member { post :archive }
|
|
371
|
+
# resources :items
|
|
372
|
+
# end
|
|
373
|
+
#
|
|
374
|
+
# @example yield style
|
|
375
|
+
# resources :invoices do |resource|
|
|
376
|
+
# resource.member { |member| member.post :archive }
|
|
377
|
+
# resource.resources :items
|
|
378
|
+
# end
|
|
379
|
+
def resources(
|
|
380
|
+
name,
|
|
381
|
+
concerns: nil,
|
|
382
|
+
constraints: nil,
|
|
383
|
+
contract: nil,
|
|
384
|
+
controller: nil,
|
|
385
|
+
defaults: nil,
|
|
386
|
+
except: nil,
|
|
387
|
+
only: nil,
|
|
388
|
+
param: nil,
|
|
389
|
+
path: nil,
|
|
390
|
+
&block
|
|
391
|
+
)
|
|
392
|
+
@root_resource.resources(
|
|
393
|
+
name,
|
|
394
|
+
concerns:,
|
|
395
|
+
constraints:,
|
|
396
|
+
contract:,
|
|
397
|
+
controller:,
|
|
398
|
+
defaults:,
|
|
399
|
+
except:,
|
|
400
|
+
only:,
|
|
401
|
+
param:,
|
|
402
|
+
path:,
|
|
403
|
+
&block
|
|
404
|
+
)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# @api public
|
|
408
|
+
# Defines a singular resource (no index action, no :id in URL).
|
|
409
|
+
#
|
|
410
|
+
# Useful for resources where only one instance exists,
|
|
411
|
+
# like user profile or application settings.
|
|
412
|
+
#
|
|
413
|
+
# @param name [Symbol]
|
|
414
|
+
# The resource name (singular).
|
|
415
|
+
# @param concerns [Array<Symbol>, nil] (nil)
|
|
416
|
+
# The concerns to include.
|
|
417
|
+
# @param constraints [Hash, Proc, nil] (nil)
|
|
418
|
+
# The route constraints (regex, lambdas).
|
|
419
|
+
# @param contract [String, nil] (nil)
|
|
420
|
+
# The custom contract path.
|
|
421
|
+
# @param controller [String, nil] (nil)
|
|
422
|
+
# The custom controller path.
|
|
423
|
+
# @param defaults [Hash, nil] (nil)
|
|
424
|
+
# The default parameters for routes.
|
|
425
|
+
# @param except [Array<Symbol>, nil] (nil)
|
|
426
|
+
# The CRUD actions to exclude.
|
|
427
|
+
# @param only [Array<Symbol>, nil] (nil)
|
|
428
|
+
# The CRUD actions to include.
|
|
429
|
+
# @param param [Symbol, nil] (nil)
|
|
430
|
+
# The custom parameter name for ID.
|
|
431
|
+
# @param path [String, nil] (nil)
|
|
432
|
+
# The custom URL path segment.
|
|
433
|
+
# @yield Block for nested resources and custom actions.
|
|
434
|
+
# @yieldparam resource [Resource]
|
|
435
|
+
# @return [void]
|
|
436
|
+
#
|
|
437
|
+
# @example instance_eval style
|
|
438
|
+
# resource :profile do
|
|
439
|
+
# resources :settings
|
|
440
|
+
# end
|
|
441
|
+
#
|
|
442
|
+
# @example yield style
|
|
443
|
+
# resource :profile do |resource|
|
|
444
|
+
# resource.resources :settings
|
|
445
|
+
# end
|
|
446
|
+
def resource(
|
|
447
|
+
name,
|
|
448
|
+
concerns: nil,
|
|
449
|
+
constraints: nil,
|
|
450
|
+
contract: nil,
|
|
451
|
+
controller: nil,
|
|
452
|
+
defaults: nil,
|
|
453
|
+
except: nil,
|
|
454
|
+
only: nil,
|
|
455
|
+
param: nil,
|
|
456
|
+
path: nil,
|
|
457
|
+
&block
|
|
458
|
+
)
|
|
459
|
+
@root_resource.resource(
|
|
460
|
+
name,
|
|
461
|
+
concerns:,
|
|
462
|
+
constraints:,
|
|
463
|
+
contract:,
|
|
464
|
+
controller:,
|
|
465
|
+
defaults:,
|
|
466
|
+
except:,
|
|
467
|
+
only:,
|
|
468
|
+
param:,
|
|
469
|
+
path:,
|
|
470
|
+
&block
|
|
471
|
+
)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# @api public
|
|
475
|
+
# Defines a reusable concern for resources.
|
|
476
|
+
#
|
|
477
|
+
# Concerns are reusable blocks of resource configuration that can
|
|
478
|
+
# be included in multiple resources via the `concerns` option.
|
|
479
|
+
#
|
|
480
|
+
# @param name [Symbol]
|
|
481
|
+
# The concern name.
|
|
482
|
+
# @yield Block defining shared actions and configuration.
|
|
483
|
+
# @yieldparam resource [Resource]
|
|
484
|
+
# @return [void]
|
|
485
|
+
#
|
|
486
|
+
# @example instance_eval style
|
|
487
|
+
# concern :archivable do
|
|
488
|
+
# member do
|
|
489
|
+
# post :archive
|
|
490
|
+
# post :unarchive
|
|
491
|
+
# end
|
|
492
|
+
# end
|
|
493
|
+
#
|
|
494
|
+
# resources :posts, concerns: [:archivable]
|
|
495
|
+
#
|
|
496
|
+
# @example yield style
|
|
497
|
+
# concern :archivable do |resource|
|
|
498
|
+
# resource.member do |member|
|
|
499
|
+
# member.post :archive
|
|
500
|
+
# member.post :unarchive
|
|
501
|
+
# end
|
|
502
|
+
# end
|
|
503
|
+
#
|
|
504
|
+
# resources :posts, concerns: [:archivable]
|
|
505
|
+
def concern(name, &block)
|
|
506
|
+
@root_resource.concern(name, &block)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# @api public
|
|
510
|
+
# Applies options to all nested resource definitions.
|
|
511
|
+
#
|
|
512
|
+
# Useful for applying common configuration to a group of resources.
|
|
513
|
+
# Accepts the same options as {#resources}: only, except, defaults,
|
|
514
|
+
# constraints, controller, param, path.
|
|
515
|
+
#
|
|
516
|
+
# @param options [Hash] ({})
|
|
517
|
+
# The options to apply to nested resources.
|
|
518
|
+
# @yield Block containing resource definitions.
|
|
519
|
+
# @yieldparam resource [Resource]
|
|
520
|
+
# @return [void]
|
|
521
|
+
#
|
|
522
|
+
# @example instance_eval style
|
|
523
|
+
# with_options only: [:index, :show] do
|
|
524
|
+
# resources :reports
|
|
525
|
+
# resources :analytics
|
|
526
|
+
# end
|
|
527
|
+
#
|
|
528
|
+
# @example yield style
|
|
529
|
+
# with_options only: [:index, :show] do |resource|
|
|
530
|
+
# resource.resources :reports
|
|
531
|
+
# resource.resources :analytics
|
|
532
|
+
# end
|
|
533
|
+
def with_options(options = {}, &block)
|
|
534
|
+
@root_resource.with_options(options, &block)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def register_object(name, deprecated: false, description: nil, example: nil, scope: nil, &block)
|
|
538
|
+
type_registry.register(
|
|
539
|
+
name,
|
|
540
|
+
deprecated:,
|
|
541
|
+
description:,
|
|
542
|
+
example:,
|
|
543
|
+
scope:,
|
|
544
|
+
kind: :object,
|
|
545
|
+
&block
|
|
546
|
+
)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def register_fragment(name, scope: nil, &block)
|
|
550
|
+
type_registry.register(
|
|
551
|
+
name,
|
|
552
|
+
scope:,
|
|
553
|
+
fragment: true,
|
|
554
|
+
kind: :object,
|
|
555
|
+
&block
|
|
556
|
+
)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def register_enum(name, deprecated: false, description: nil, example: nil, scope: nil, values: nil)
|
|
560
|
+
raise ConfigurationError, 'Values must be an array' if values && !values.is_a?(Array)
|
|
561
|
+
|
|
562
|
+
enum_registry.register(
|
|
563
|
+
name,
|
|
564
|
+
values,
|
|
565
|
+
deprecated:,
|
|
566
|
+
description:,
|
|
567
|
+
example:,
|
|
568
|
+
scope:,
|
|
569
|
+
)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def register_union(name, deprecated: false, description: nil, discriminator: nil, example: nil, scope: nil, &block)
|
|
573
|
+
raise ConfigurationError, 'Union requires a block' unless block_given?
|
|
574
|
+
|
|
575
|
+
type_registry.register(
|
|
576
|
+
name,
|
|
577
|
+
deprecated:,
|
|
578
|
+
description:,
|
|
579
|
+
discriminator:,
|
|
580
|
+
example:,
|
|
581
|
+
scope:,
|
|
582
|
+
kind: :union,
|
|
583
|
+
&block
|
|
584
|
+
)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def adapter_class
|
|
588
|
+
Adapter.find!(@adapter_name || :standard)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def adapter_config
|
|
592
|
+
@adapter_config ||= Configuration.new(adapter_class)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def locale_key
|
|
596
|
+
@locale_key ||= base_path.delete_prefix('/')
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def namespaces
|
|
600
|
+
@namespaces ||= extract_namespaces(base_path)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def mount(base_path)
|
|
604
|
+
@base_path = base_path
|
|
605
|
+
@locale_key = nil
|
|
606
|
+
@namespaces = nil
|
|
607
|
+
@info = nil
|
|
608
|
+
@raises = []
|
|
609
|
+
@export_configs = {}
|
|
610
|
+
@adapter_config = nil
|
|
611
|
+
@root_resource = Resource.new(self)
|
|
612
|
+
@type_registry = TypeRegistry.new
|
|
613
|
+
@enum_registry = EnumRegistry.new
|
|
614
|
+
@representation_registry = RepresentationRegistry.new
|
|
615
|
+
@built_contracts = Set.new
|
|
616
|
+
@key_format = :keep
|
|
617
|
+
@path_format = :keep
|
|
618
|
+
@introspect_cache = {}
|
|
619
|
+
@introspect_contract_cache = {}
|
|
620
|
+
|
|
621
|
+
Registry.register(self)
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def translate(*segments, default: nil)
|
|
625
|
+
key = :"apiwork.apis.#{locale_key}.#{segments.join('.')}"
|
|
626
|
+
I18n.translate(key, default:)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def transform_path(path)
|
|
630
|
+
path_string = path.to_s
|
|
631
|
+
return path_string if @path_format == :keep
|
|
632
|
+
return path_string if path_string == '/'
|
|
633
|
+
|
|
634
|
+
path_string.split('/').map do |segment|
|
|
635
|
+
next segment if segment.empty?
|
|
636
|
+
|
|
637
|
+
case @path_format
|
|
638
|
+
when :camel then segment.camelize(:lower)
|
|
639
|
+
when :pascal then segment.camelize
|
|
640
|
+
when :kebab then segment.dasherize
|
|
641
|
+
when :underscore then segment.underscore
|
|
642
|
+
else segment
|
|
643
|
+
end
|
|
644
|
+
end.join('/')
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def normalize_request(request)
|
|
648
|
+
return request if %i[camel pascal kebab].exclude?(key_format)
|
|
649
|
+
|
|
650
|
+
request.transform do |hash|
|
|
651
|
+
hash.deep_transform_keys do |key|
|
|
652
|
+
key_string = key.to_s
|
|
653
|
+
next key if key_string.match?(/\A[A-Z]+\z/)
|
|
654
|
+
|
|
655
|
+
key_string.underscore.to_sym
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def prepare_request(request)
|
|
661
|
+
request
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def prepare_response(response)
|
|
665
|
+
result = adapter.apply_response_transformers(response)
|
|
666
|
+
case key_format
|
|
667
|
+
when :camel
|
|
668
|
+
result.transform { |hash| hash.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym } }
|
|
669
|
+
when :pascal
|
|
670
|
+
result.transform { |hash| hash.deep_transform_keys { |key| key.to_s.camelize.to_sym } }
|
|
671
|
+
when :kebab
|
|
672
|
+
result.transform { |hash| hash.deep_transform_keys { |key| key.to_s.dasherize.to_sym } }
|
|
673
|
+
else
|
|
674
|
+
result
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def type?(name, scope: nil)
|
|
679
|
+
type_registry.exists?(name, scope:)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def type_definition(name, scope: nil)
|
|
683
|
+
type_registry.find(name, scope:)
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def enum?(name, scope: nil)
|
|
687
|
+
enum_registry.exists?(name, scope:)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def enum_values(name, scope: nil)
|
|
691
|
+
enum_registry.values(name, scope:)
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def scoped_type_name(scope, name)
|
|
695
|
+
type_registry.scoped_name(scope, name)
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def scoped_enum_name(scope, name)
|
|
699
|
+
enum_registry.scoped_name(scope, name)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def introspect(locale: nil)
|
|
703
|
+
ensure_all_contracts_built!
|
|
704
|
+
@introspect_cache[locale] ||= Introspection.api(self, locale:)
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def introspect_contract(contract_class, expand:, locale:)
|
|
708
|
+
ensure_all_contracts_built!
|
|
709
|
+
cache_key = [contract_class, locale, expand]
|
|
710
|
+
@introspect_contract_cache[cache_key] ||= Introspection.contract(contract_class, expand:, locale:)
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def reset_contracts!
|
|
714
|
+
@built_contracts = Set.new
|
|
715
|
+
@introspect_cache = {}
|
|
716
|
+
@introspect_contract_cache = {}
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def ensure_contract_built!(contract_class)
|
|
720
|
+
return if @built_contracts.include?(contract_class)
|
|
721
|
+
|
|
722
|
+
ensure_pre_pass_complete!
|
|
723
|
+
|
|
724
|
+
representation_class = contract_class.representation_class
|
|
725
|
+
return unless representation_class
|
|
726
|
+
|
|
727
|
+
@built_contracts.add(contract_class)
|
|
728
|
+
|
|
729
|
+
resource = @root_resource.find_resource { |resource| resource.resolve_contract_class == contract_class }
|
|
730
|
+
actions = resource ? resource.actions : {}
|
|
731
|
+
|
|
732
|
+
adapter.register_contract(contract_class, representation_class, actions)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def ensure_pre_pass_complete!
|
|
736
|
+
return if @pre_pass_complete
|
|
737
|
+
|
|
738
|
+
mark_nested_writable_representations!
|
|
739
|
+
adapter.register_api(self)
|
|
740
|
+
@pre_pass_complete = true
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def ensure_all_contracts_built!
|
|
744
|
+
ensure_pre_pass_complete!
|
|
745
|
+
|
|
746
|
+
@root_resource.each_resource do |resource|
|
|
747
|
+
build_contracts_for_resource(resource)
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
private
|
|
752
|
+
|
|
753
|
+
def extract_namespaces(mount_path)
|
|
754
|
+
return [] if mount_path.nil? || mount_path == '/'
|
|
755
|
+
|
|
756
|
+
mount_path.split('/').reject(&:empty?).map { |segment| segment.tr('-', '_').to_sym }
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def mark_nested_writable_representations!
|
|
760
|
+
visited = Set.new
|
|
761
|
+
@root_resource.each_resource do |resource|
|
|
762
|
+
representation_class = resource.resolve_contract_class&.representation_class
|
|
763
|
+
next unless representation_class
|
|
764
|
+
|
|
765
|
+
representation_registry.register(representation_class)
|
|
766
|
+
mark_writable_associations(representation_class, visited)
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def mark_writable_associations(representation_class, visited)
|
|
771
|
+
return if visited.include?(representation_class)
|
|
772
|
+
|
|
773
|
+
visited.add(representation_class)
|
|
774
|
+
|
|
775
|
+
representation_class.associations.each_value do |association|
|
|
776
|
+
next unless association.writable?
|
|
777
|
+
|
|
778
|
+
target_representation = association.representation_class
|
|
779
|
+
next unless target_representation
|
|
780
|
+
|
|
781
|
+
representation_registry.register(target_representation)
|
|
782
|
+
representation_registry.mark(target_representation, :nested_writable)
|
|
783
|
+
mark_writable_associations(target_representation, visited)
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def build_contracts_for_resource(resource)
|
|
788
|
+
contract_class = resource.resolve_contract_class
|
|
789
|
+
return unless contract_class
|
|
790
|
+
return if @built_contracts.include?(contract_class)
|
|
791
|
+
|
|
792
|
+
representation_class = contract_class.representation_class
|
|
793
|
+
return unless representation_class
|
|
794
|
+
|
|
795
|
+
@built_contracts.add(contract_class)
|
|
796
|
+
|
|
797
|
+
adapter.register_contract(contract_class, representation_class, resource.actions)
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
end
|