apiwork 0.2.0 → 0.3.0
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/lib/apiwork/adapter/base.rb +5 -72
- data/lib/apiwork/api/base.rb +4 -6
- data/lib/apiwork/contract/action/request.rb +22 -0
- data/lib/apiwork/contract/action/response.rb +22 -0
- data/lib/apiwork/export/open_api.rb +11 -5
- data/lib/apiwork/export/sorbus.rb +21 -0
- data/lib/apiwork/export/sorbus_mapper.rb +159 -0
- data/lib/apiwork/export/surface_resolver.rb +1 -0
- data/lib/apiwork/export/type_script_mapper.rb +52 -45
- data/lib/apiwork/export/zod_mapper.rb +28 -13
- data/lib/apiwork/export.rb +1 -0
- data/lib/apiwork/introspection/action/request.rb +9 -0
- data/lib/apiwork/introspection/action/response.rb +9 -1
- data/lib/apiwork/introspection/dump/action.rb +9 -7
- data/lib/apiwork/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb5f681a0f08e4bebe2d0f5ebf4edac733f3787e4144f019bc4babfa7a7ef84a
|
|
4
|
+
data.tar.gz: e5d1d87115d2902295d66567c324d73ef15d01b0ac106c1fa9739b56a276aeb3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb539d87fcd3b065d643711616f7f9f5589d574cbf12bcc06c69aafe7000f6fafa4e19d7fd34cd165d2734076d3b192472a6f51459074241fabbf5dd3faece42
|
|
7
|
+
data.tar.gz: 692745d73d86c3f5c377ae2a7962b38efe351b1fc893df6af45bd8de025622f61257307dedcd1b6aac15df8f3112874e92c54d3b3e316ef9aaa59ef6fcca0338
|
data/lib/apiwork/adapter/base.rb
CHANGED
|
@@ -210,18 +210,16 @@ module Apiwork
|
|
|
210
210
|
|
|
211
211
|
error_serializer_class = self.class.error_serializer
|
|
212
212
|
error_serializer_class.new.api_types(api_class)
|
|
213
|
-
|
|
214
|
-
build_error_response_body(api_class, error_serializer_class)
|
|
215
213
|
end
|
|
216
214
|
|
|
217
|
-
def register_contract(contract_class, representation_class,
|
|
215
|
+
def register_contract(contract_class, representation_class, resource_actions: {})
|
|
218
216
|
capabilities.each do |capability|
|
|
219
|
-
capability.contract_types(contract_class, representation_class,
|
|
217
|
+
capability.contract_types(contract_class, representation_class, resource_actions)
|
|
220
218
|
end
|
|
221
219
|
|
|
222
220
|
self.class.resource_serializer.new(representation_class).contract_types(contract_class)
|
|
223
221
|
|
|
224
|
-
build_action_responses(contract_class, representation_class,
|
|
222
|
+
build_action_responses(contract_class, representation_class, resource_actions) if resource_actions.any?
|
|
225
223
|
end
|
|
226
224
|
|
|
227
225
|
def apply_request_transformers(request, phase:)
|
|
@@ -250,8 +248,8 @@ module Apiwork
|
|
|
250
248
|
)
|
|
251
249
|
end
|
|
252
250
|
|
|
253
|
-
def build_action_responses(contract_class, representation_class,
|
|
254
|
-
|
|
251
|
+
def build_action_responses(contract_class, representation_class, resource_actions)
|
|
252
|
+
resource_actions.each_value do |action|
|
|
255
253
|
build_action_response(contract_class, representation_class, action)
|
|
256
254
|
end
|
|
257
255
|
end
|
|
@@ -270,13 +268,9 @@ module Apiwork
|
|
|
270
268
|
else
|
|
271
269
|
build_custom_action_response(contract_class, representation_class, action, contract_action)
|
|
272
270
|
end
|
|
273
|
-
|
|
274
|
-
build_request_query_type(contract_class, action.name, contract_action)
|
|
275
|
-
build_request_body_type(contract_class, action.name, contract_action)
|
|
276
271
|
end
|
|
277
272
|
|
|
278
273
|
def build_member_action_response(contract_class, representation_class, action, contract_action)
|
|
279
|
-
build_response_body_type(contract_class, representation_class, action.name, :member)
|
|
280
274
|
member_shape_class = self.class.member_wrapper.shape_class
|
|
281
275
|
data_type = resolve_resource_data_type(representation_class)
|
|
282
276
|
|
|
@@ -288,7 +282,6 @@ module Apiwork
|
|
|
288
282
|
end
|
|
289
283
|
|
|
290
284
|
def build_collection_action_response(contract_class, representation_class, action, contract_action)
|
|
291
|
-
build_response_body_type(contract_class, representation_class, action.name, :collection)
|
|
292
285
|
collection_shape_class = self.class.collection_wrapper.shape_class
|
|
293
286
|
data_type = resolve_resource_data_type(representation_class)
|
|
294
287
|
|
|
@@ -309,70 +302,10 @@ module Apiwork
|
|
|
309
302
|
end
|
|
310
303
|
end
|
|
311
304
|
|
|
312
|
-
def build_response_body_type(contract_class, representation_class, action_name, response_type)
|
|
313
|
-
type_name = [action_name, 'response_body'].join('_').to_sym
|
|
314
|
-
return if contract_class.type?(type_name)
|
|
315
|
-
|
|
316
|
-
shape_class = if response_type == :collection
|
|
317
|
-
self.class.collection_wrapper.shape_class
|
|
318
|
-
else
|
|
319
|
-
self.class.member_wrapper.shape_class
|
|
320
|
-
end
|
|
321
|
-
data_type = resolve_resource_data_type(representation_class)
|
|
322
|
-
|
|
323
|
-
contract_class.object(type_name) do |object|
|
|
324
|
-
shape_class.apply(object, representation_class.root_key, capabilities, representation_class, response_type, data_type:)
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
def build_request_query_type(contract_class, action_name, contract_action)
|
|
329
|
-
request = contract_action.request
|
|
330
|
-
return unless request.query.params.any?
|
|
331
|
-
|
|
332
|
-
type_name = [action_name, 'request_query'].join('_').to_sym
|
|
333
|
-
return if contract_class.type?(type_name)
|
|
334
|
-
|
|
335
|
-
contract_class.object(type_name) do |object|
|
|
336
|
-
request.query.params.each { |name, param| object.param(name, **normalize_request_param(param)) }
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def build_request_body_type(contract_class, action_name, contract_action)
|
|
341
|
-
request = contract_action.request
|
|
342
|
-
return unless request.body.params.any?
|
|
343
|
-
|
|
344
|
-
type_name = [action_name, 'request_body'].join('_').to_sym
|
|
345
|
-
return if contract_class.type?(type_name)
|
|
346
|
-
|
|
347
|
-
contract_class.object(type_name) do |object|
|
|
348
|
-
request.body.params.each { |name, param| object.param(name, **normalize_request_param(param)) }
|
|
349
|
-
end
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
def normalize_request_param(param)
|
|
353
|
-
options = param.except(:name, :custom_type, :union, :partial)
|
|
354
|
-
options[:type] = param[:custom_type] if param[:custom_type]
|
|
355
|
-
options[:shape] = param[:union] if param[:union]
|
|
356
|
-
options
|
|
357
|
-
end
|
|
358
|
-
|
|
359
305
|
def resolve_resource_data_type(representation_class)
|
|
360
306
|
self.class.resource_serializer.data_type.call(representation_class)
|
|
361
307
|
end
|
|
362
308
|
|
|
363
|
-
def build_error_response_body(api_class, error_serializer_class)
|
|
364
|
-
return if api_class.type?(:error_response_body)
|
|
365
|
-
|
|
366
|
-
shape_class = self.class.error_wrapper.shape_class
|
|
367
|
-
return unless shape_class
|
|
368
|
-
|
|
369
|
-
data_type = error_serializer_class.data_type
|
|
370
|
-
|
|
371
|
-
api_class.object(:error_response_body) do |object|
|
|
372
|
-
shape_class.apply(object, nil, [], nil, :error, data_type:)
|
|
373
|
-
end
|
|
374
|
-
end
|
|
375
|
-
|
|
376
309
|
def run_capability_request_transformers(request, phase:)
|
|
377
310
|
transformers = capability_request_transformers.select { |transformer_class| transformer_class.phase == phase }
|
|
378
311
|
result = request
|
data/lib/apiwork/api/base.rb
CHANGED
|
@@ -727,9 +727,9 @@ module Apiwork
|
|
|
727
727
|
@built_contracts.add(contract_class)
|
|
728
728
|
|
|
729
729
|
resource = @root_resource.find_resource { |resource| resource.resolve_contract_class == contract_class }
|
|
730
|
-
|
|
730
|
+
resource_actions = resource ? resource.actions : {}
|
|
731
731
|
|
|
732
|
-
adapter.register_contract(contract_class, representation_class,
|
|
732
|
+
adapter.register_contract(contract_class, representation_class, resource_actions:)
|
|
733
733
|
end
|
|
734
734
|
|
|
735
735
|
def ensure_pre_pass_complete!
|
|
@@ -788,13 +788,11 @@ module Apiwork
|
|
|
788
788
|
contract_class = resource.resolve_contract_class
|
|
789
789
|
return unless contract_class
|
|
790
790
|
return if @built_contracts.include?(contract_class)
|
|
791
|
-
|
|
792
|
-
representation_class = contract_class.representation_class
|
|
793
|
-
return unless representation_class
|
|
791
|
+
return unless contract_class.representation_class
|
|
794
792
|
|
|
795
793
|
@built_contracts.add(contract_class)
|
|
796
794
|
|
|
797
|
-
adapter.register_contract(contract_class, representation_class, resource.actions)
|
|
795
|
+
adapter.register_contract(contract_class, contract_class.representation_class, resource_actions: resource.actions)
|
|
798
796
|
end
|
|
799
797
|
end
|
|
800
798
|
end
|
|
@@ -16,6 +16,28 @@ module Apiwork
|
|
|
16
16
|
@action_name = action_name
|
|
17
17
|
@query = nil
|
|
18
18
|
@body = nil
|
|
19
|
+
@description = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @api public
|
|
23
|
+
# The description for this request.
|
|
24
|
+
#
|
|
25
|
+
# Metadata included in exports.
|
|
26
|
+
#
|
|
27
|
+
# @param value [String, nil] (nil)
|
|
28
|
+
# The description.
|
|
29
|
+
# @return [String, nil]
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# action :create do
|
|
33
|
+
# request do
|
|
34
|
+
# description 'The invoice to create'
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
def description(value = nil)
|
|
38
|
+
return @description if value.nil?
|
|
39
|
+
|
|
40
|
+
@description = value
|
|
19
41
|
end
|
|
20
42
|
|
|
21
43
|
# @api public
|
|
@@ -15,9 +15,31 @@ module Apiwork
|
|
|
15
15
|
@contract_class = contract_class
|
|
16
16
|
@action_name = action_name
|
|
17
17
|
@body = nil
|
|
18
|
+
@description = nil
|
|
18
19
|
@no_content = false
|
|
19
20
|
end
|
|
20
21
|
|
|
22
|
+
# @api public
|
|
23
|
+
# The description for this response.
|
|
24
|
+
#
|
|
25
|
+
# Metadata included in exports.
|
|
26
|
+
#
|
|
27
|
+
# @param value [String, nil] (nil)
|
|
28
|
+
# The description.
|
|
29
|
+
# @return [String, nil]
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# action :show do
|
|
33
|
+
# response do
|
|
34
|
+
# description 'Returns the invoice'
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
def description(value = nil)
|
|
38
|
+
return @description if value.nil?
|
|
39
|
+
|
|
40
|
+
@description = value
|
|
41
|
+
end
|
|
42
|
+
|
|
21
43
|
# @api public
|
|
22
44
|
# Whether this response has no content.
|
|
23
45
|
#
|
|
@@ -106,7 +106,11 @@ module Apiwork
|
|
|
106
106
|
query_params = request.query? ? build_query_parameters(request.query) : []
|
|
107
107
|
all_params = path_params + query_params
|
|
108
108
|
operation[:parameters] = all_params if all_params.any?
|
|
109
|
-
|
|
109
|
+
if request.body?
|
|
110
|
+
request_body = build_request_body(request.body)
|
|
111
|
+
request_body[:description] = request.description if request.description
|
|
112
|
+
operation[:requestBody] = request_body
|
|
113
|
+
end
|
|
110
114
|
elsif path_params.any?
|
|
111
115
|
operation[:parameters] = path_params
|
|
112
116
|
end
|
|
@@ -200,8 +204,10 @@ module Apiwork
|
|
|
200
204
|
def build_responses(action_name, response, raises: [])
|
|
201
205
|
responses = {}
|
|
202
206
|
|
|
207
|
+
response_description = response.description || ''
|
|
208
|
+
|
|
203
209
|
if response.no_content?
|
|
204
|
-
responses[:'204'] = { description:
|
|
210
|
+
responses[:'204'] = { description: response_description }
|
|
205
211
|
elsif response.body
|
|
206
212
|
responses[:'200'] = {
|
|
207
213
|
content: {
|
|
@@ -209,7 +215,7 @@ module Apiwork
|
|
|
209
215
|
schema: map_param(response.body),
|
|
210
216
|
},
|
|
211
217
|
},
|
|
212
|
-
description:
|
|
218
|
+
description: response_description,
|
|
213
219
|
}
|
|
214
220
|
|
|
215
221
|
raises.each do |code|
|
|
@@ -228,10 +234,10 @@ module Apiwork
|
|
|
228
234
|
},
|
|
229
235
|
},
|
|
230
236
|
},
|
|
231
|
-
description:
|
|
237
|
+
description: response_description,
|
|
232
238
|
}
|
|
233
239
|
else
|
|
234
|
-
responses[:'204'] = { description: '
|
|
240
|
+
responses[:'204'] = { description: '' }
|
|
235
241
|
end
|
|
236
242
|
|
|
237
243
|
responses
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Export
|
|
5
|
+
class Sorbus < Base
|
|
6
|
+
export_name :sorbus
|
|
7
|
+
output :string
|
|
8
|
+
file_extension '.ts'
|
|
9
|
+
|
|
10
|
+
def generate
|
|
11
|
+
SorbusMapper.map(self, surface)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def surface
|
|
17
|
+
@surface ||= SurfaceResolver.resolve(api)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Export
|
|
5
|
+
class SorbusMapper
|
|
6
|
+
class << self
|
|
7
|
+
def map(export, surface)
|
|
8
|
+
new(export).map(surface)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(export)
|
|
13
|
+
@export = export
|
|
14
|
+
@zod_mapper = ZodMapper.new(export)
|
|
15
|
+
@type_script_mapper = TypeScriptMapper.new(export)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def map(surface)
|
|
19
|
+
@surface = surface
|
|
20
|
+
|
|
21
|
+
[
|
|
22
|
+
"import { z } from 'zod';",
|
|
23
|
+
@zod_mapper.build_enum_schemas(surface.enums).presence,
|
|
24
|
+
@zod_mapper.build_type_schemas(surface.types).presence,
|
|
25
|
+
build_typescript_types.presence,
|
|
26
|
+
build_contract,
|
|
27
|
+
].compact.join("\n\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_typescript_types
|
|
33
|
+
types = @type_script_mapper.build_enum_types(@surface.enums) +
|
|
34
|
+
@type_script_mapper.build_type_definitions(@surface.types)
|
|
35
|
+
|
|
36
|
+
types.sort_by { |entry| entry[:name] }.map { |entry| entry[:code] }.join("\n\n")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_contract
|
|
40
|
+
contract = {
|
|
41
|
+
endpoints: build_endpoint_tree(@export.api.resources),
|
|
42
|
+
error: build_error_schema,
|
|
43
|
+
}
|
|
44
|
+
"export const contract = #{format_object(contract, indent: 0)} as const;"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_endpoint_tree(resources, parent_identifiers: [])
|
|
48
|
+
resources.each_with_object({}) do |(name, resource), tree|
|
|
49
|
+
resource_key = @export.transform_key(name)
|
|
50
|
+
identifiers = parent_identifiers + [resource.identifier]
|
|
51
|
+
|
|
52
|
+
endpoints = resource.actions.each_with_object({}) do |(action_name, action), actions|
|
|
53
|
+
actions[@export.transform_key(action_name)] = build_endpoint(
|
|
54
|
+
resource.identifier.to_sym, action_name, action
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
endpoints.merge!(build_endpoint_tree(resource.resources, parent_identifiers: identifiers))
|
|
59
|
+
tree[resource_key] = endpoints
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_endpoint(resource_name, action_name, action)
|
|
64
|
+
path = transform_path(action.path)
|
|
65
|
+
endpoint = { path:, method: action.method.to_s.upcase }
|
|
66
|
+
|
|
67
|
+
path_params = extract_path_params(action.path)
|
|
68
|
+
endpoint[:pathParams] = build_path_params_schema(path_params) if path_params.any?
|
|
69
|
+
|
|
70
|
+
request = build_request(action.request)
|
|
71
|
+
endpoint[:request] = request if request
|
|
72
|
+
|
|
73
|
+
response = build_response(action.response)
|
|
74
|
+
endpoint[:response] = response if response
|
|
75
|
+
|
|
76
|
+
errors = resolve_errors(action)
|
|
77
|
+
endpoint[:errors] = errors if errors.any?
|
|
78
|
+
|
|
79
|
+
endpoint
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def transform_path(path)
|
|
83
|
+
path.gsub(%r{(/:?)(\w+)}) do
|
|
84
|
+
"#{::Regexp.last_match(1)}#{@export.transform_key(::Regexp.last_match(2))}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def extract_path_params(path)
|
|
89
|
+
path.scan(/:(\w+)/).flatten
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_path_params_schema(params)
|
|
93
|
+
properties = params.map { |param| "#{@export.transform_key(param)}: z.string()" }.join(', ')
|
|
94
|
+
"z.object({ #{properties} })"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_request(request)
|
|
98
|
+
return unless request.query? || request.body?
|
|
99
|
+
|
|
100
|
+
hash = {}
|
|
101
|
+
hash[:query] = build_params_schema(request.query) if request.query?
|
|
102
|
+
hash[:body] = build_params_schema(request.body) if request.body?
|
|
103
|
+
hash
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_response(response)
|
|
107
|
+
return unless response.body?
|
|
108
|
+
|
|
109
|
+
{ body: @zod_mapper.map_param(response.body) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_params_schema(params)
|
|
113
|
+
properties = params.sort_by { |name, _| name.to_s }.map do |name, param|
|
|
114
|
+
"#{@export.transform_key(name)}: #{@zod_mapper.map_field(param)}"
|
|
115
|
+
end.join(', ')
|
|
116
|
+
"z.object({ #{properties} })"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_error_schema
|
|
120
|
+
'ErrorSchema'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def resolve_errors(action)
|
|
124
|
+
action.raises.map { |code| @export.api.error_codes[code].status }.sort.uniq
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def format_object(hash, indent:)
|
|
128
|
+
return '{}' if hash.empty?
|
|
129
|
+
|
|
130
|
+
padding = ' ' * (indent + 1)
|
|
131
|
+
closing_padding = ' ' * indent
|
|
132
|
+
|
|
133
|
+
entries = hash.map do |key, value|
|
|
134
|
+
formatted_value = format_value(value, indent: indent + 1)
|
|
135
|
+
"#{padding}#{key}: #{formatted_value},"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
"{\n#{entries.join("\n")}\n#{closing_padding}}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def format_value(value, indent:)
|
|
142
|
+
case value
|
|
143
|
+
when Hash
|
|
144
|
+
format_object(value, indent:)
|
|
145
|
+
when String
|
|
146
|
+
if value.start_with?('z.') || value.end_with?('Schema')
|
|
147
|
+
value
|
|
148
|
+
else
|
|
149
|
+
"'#{value}'"
|
|
150
|
+
end
|
|
151
|
+
when Array
|
|
152
|
+
"[#{value.join(', ')}]"
|
|
153
|
+
else
|
|
154
|
+
value.to_s
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -56,6 +56,7 @@ module Apiwork
|
|
|
56
56
|
action.request.query.each_value { |param| collect_types_from_param(param, type_names) }
|
|
57
57
|
action.request.body.each_value { |param| collect_types_from_param(param, type_names) }
|
|
58
58
|
collect_types_from_param(action.response.body, type_names) if action.response.body
|
|
59
|
+
type_names << :error if action.raises.any? && @api.types.key?(:error)
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def collect_types_from_param(param, type_names)
|
|
@@ -14,9 +14,10 @@ module Apiwork
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def map(surface)
|
|
17
|
-
types = build_enum_types(surface) +
|
|
18
|
-
build_type_definitions(surface) +
|
|
19
|
-
build_action_types
|
|
17
|
+
types = build_enum_types(surface.enums) +
|
|
18
|
+
build_type_definitions(surface.types) +
|
|
19
|
+
build_action_types +
|
|
20
|
+
build_action_response_types
|
|
20
21
|
|
|
21
22
|
types.sort_by { |entry| entry[:name] }.map { |entry| entry[:code] }.join("\n\n")
|
|
22
23
|
end
|
|
@@ -131,7 +132,7 @@ module Apiwork
|
|
|
131
132
|
if error_statuses.empty?
|
|
132
133
|
"export type #{type_name} = #{success_variant};"
|
|
133
134
|
else
|
|
134
|
-
error_variants = error_statuses.map { |status| "{ status: #{status}; body: #{pascal_case(:
|
|
135
|
+
error_variants = error_statuses.map { |status| "{ status: #{status}; body: #{pascal_case(:error)} }" }
|
|
135
136
|
all_variants = ([success_variant] + error_variants).map { |variant| " | #{variant}" }.join("\n")
|
|
136
137
|
"export type #{type_name} =\n#{all_variants};"
|
|
137
138
|
end
|
|
@@ -258,27 +259,29 @@ module Apiwork
|
|
|
258
259
|
end
|
|
259
260
|
end
|
|
260
261
|
|
|
261
|
-
|
|
262
|
+
def build_action_types
|
|
263
|
+
types = []
|
|
262
264
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
traverse_resources do |resource|
|
|
266
|
+
resource_name = resource.identifier.to_sym
|
|
267
|
+
parent_identifiers = resource.parent_identifiers
|
|
266
268
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{ code: build_enum_type(name, enum), name: pascal_case(name) }
|
|
270
|
-
end
|
|
271
|
-
end
|
|
269
|
+
resource.actions.each do |action_name, action|
|
|
270
|
+
types.concat(build_request_types(resource_name, action_name, action.request, parent_identifiers:))
|
|
272
271
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
response = action.response
|
|
273
|
+
next unless response.body?
|
|
274
|
+
|
|
275
|
+
type_name = action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)
|
|
276
|
+
code = build_action_response_body_type(resource_name, action_name, response.body, parent_identifiers:)
|
|
277
|
+
types << { code:, name: type_name }
|
|
278
|
+
end
|
|
278
279
|
end
|
|
280
|
+
|
|
281
|
+
types
|
|
279
282
|
end
|
|
280
283
|
|
|
281
|
-
def
|
|
284
|
+
def build_action_response_types
|
|
282
285
|
types = []
|
|
283
286
|
|
|
284
287
|
traverse_resources do |resource|
|
|
@@ -286,14 +289,42 @@ module Apiwork
|
|
|
286
289
|
parent_identifiers = resource.parent_identifiers
|
|
287
290
|
|
|
288
291
|
resource.actions.each do |action_name, action|
|
|
289
|
-
|
|
290
|
-
|
|
292
|
+
type_name = action_type_name(resource_name, action_name, 'Response', parent_identifiers:)
|
|
293
|
+
code = build_action_response_type(resource_name, action_name, action.response, parent_identifiers:, raises: action.raises)
|
|
294
|
+
types << { code:, name: type_name }
|
|
291
295
|
end
|
|
292
296
|
end
|
|
293
297
|
|
|
294
298
|
types
|
|
295
299
|
end
|
|
296
300
|
|
|
301
|
+
def traverse_resources(resources: @export.api.resources, &block)
|
|
302
|
+
resources.each_value do |resource|
|
|
303
|
+
yield(resource)
|
|
304
|
+
traverse_resources(resources: resource.resources, &block) if resource.resources.any?
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def build_enum_types(enums)
|
|
309
|
+
enums.map do |name, enum|
|
|
310
|
+
{ code: build_enum_type(name, enum), name: pascal_case(name) }
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def build_type_definitions(types)
|
|
315
|
+
TypeAnalysis.topological_sort_types(types.transform_values(&:to_h)).map(&:first).map do |type_name|
|
|
316
|
+
type = types[type_name]
|
|
317
|
+
code = type.union? ? build_union_type(type_name, type) : build_interface(type_name, type)
|
|
318
|
+
{ code:, name: pascal_case(type_name) }
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
private
|
|
323
|
+
|
|
324
|
+
def resolve_error_statuses(raises)
|
|
325
|
+
raises.map { |code| @export.api.error_codes[code].status }.uniq.sort
|
|
326
|
+
end
|
|
327
|
+
|
|
297
328
|
def build_request_types(resource_name, action_name, request, parent_identifiers:)
|
|
298
329
|
types = []
|
|
299
330
|
return types unless request && (request.query? || request.body?)
|
|
@@ -317,30 +348,6 @@ module Apiwork
|
|
|
317
348
|
types
|
|
318
349
|
end
|
|
319
350
|
|
|
320
|
-
def build_response_types(resource_name, action_name, action, parent_identifiers:)
|
|
321
|
-
types = []
|
|
322
|
-
response = action.response
|
|
323
|
-
|
|
324
|
-
if response.body?
|
|
325
|
-
type_name = action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)
|
|
326
|
-
code = build_action_response_body_type(resource_name, action_name, response.body, parent_identifiers:)
|
|
327
|
-
types << { code:, name: type_name }
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
type_name = action_type_name(resource_name, action_name, 'Response', parent_identifiers:)
|
|
331
|
-
code = build_action_response_type(resource_name, action_name, response, parent_identifiers:, raises: action.raises)
|
|
332
|
-
types << { code:, name: type_name }
|
|
333
|
-
|
|
334
|
-
types
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def traverse_resources(resources: @export.api.resources, &block)
|
|
338
|
-
resources.each_value do |resource|
|
|
339
|
-
yield(resource)
|
|
340
|
-
traverse_resources(resources: resource.resources, &block) if resource.resources.any?
|
|
341
|
-
end
|
|
342
|
-
end
|
|
343
|
-
|
|
344
351
|
def type_or_enum_reference?(symbol)
|
|
345
352
|
@export.api.types.key?(symbol) || @export.api.enums.key?(symbol)
|
|
346
353
|
end
|
|
@@ -28,18 +28,20 @@ module Apiwork
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def map(surface)
|
|
31
|
-
@surface = surface
|
|
32
31
|
parts = []
|
|
33
32
|
|
|
34
|
-
enum_schemas = build_enum_schemas
|
|
33
|
+
enum_schemas = build_enum_schemas(surface.enums)
|
|
35
34
|
parts << enum_schemas if enum_schemas.present?
|
|
36
35
|
|
|
37
|
-
type_schemas = build_type_schemas
|
|
36
|
+
type_schemas = build_type_schemas(surface.types)
|
|
38
37
|
parts << type_schemas if type_schemas.present?
|
|
39
38
|
|
|
40
39
|
action_schemas = build_action_schemas
|
|
41
40
|
parts << action_schemas if action_schemas.present?
|
|
42
41
|
|
|
42
|
+
action_response_schemas = build_action_response_schemas
|
|
43
|
+
parts << action_response_schemas if action_response_schemas.present?
|
|
44
|
+
|
|
43
45
|
parts.join("\n\n")
|
|
44
46
|
end
|
|
45
47
|
|
|
@@ -169,7 +171,7 @@ module Apiwork
|
|
|
169
171
|
"export const #{schema_name}Schema = #{success_variant};"
|
|
170
172
|
else
|
|
171
173
|
error_variants = error_statuses.map do |status|
|
|
172
|
-
"z.object({ status: z.literal(#{status}), body: #{pascal_case(:
|
|
174
|
+
"z.object({ status: z.literal(#{status}), body: #{pascal_case(:error)}Schema })"
|
|
173
175
|
end
|
|
174
176
|
all_variants = ([success_variant] + error_variants).map { |variant| " #{variant}" }.join(",\n")
|
|
175
177
|
"export const #{schema_name}Schema = z.discriminatedUnion('status', [\n#{all_variants}\n]);"
|
|
@@ -315,22 +317,20 @@ module Apiwork
|
|
|
315
317
|
name.to_s.camelize(:upper)
|
|
316
318
|
end
|
|
317
319
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def build_enum_schemas
|
|
321
|
-
return '' if @surface.enums.empty?
|
|
320
|
+
def build_enum_schemas(enums)
|
|
321
|
+
return '' if enums.empty?
|
|
322
322
|
|
|
323
|
-
|
|
323
|
+
enums.map do |name, enum|
|
|
324
324
|
"export const #{pascal_case(name)}Schema = z.enum([#{enum.values.sort.map { |value| "'#{value}'" }.join(', ')}]);"
|
|
325
325
|
end.join("\n\n")
|
|
326
326
|
end
|
|
327
327
|
|
|
328
|
-
def build_type_schemas
|
|
329
|
-
types_hash =
|
|
328
|
+
def build_type_schemas(types)
|
|
329
|
+
types_hash = types.transform_values(&:to_h)
|
|
330
330
|
lazy_types = TypeAnalysis.cycle_breaking_types(types_hash)
|
|
331
331
|
|
|
332
332
|
TypeAnalysis.topological_sort_types(types_hash).map(&:first).map do |type_name|
|
|
333
|
-
type =
|
|
333
|
+
type = types[type_name]
|
|
334
334
|
recursive = lazy_types.include?(type_name)
|
|
335
335
|
|
|
336
336
|
if type.union?
|
|
@@ -377,8 +377,21 @@ module Apiwork
|
|
|
377
377
|
|
|
378
378
|
response = action.response
|
|
379
379
|
schemas << build_action_response_body_schema(resource_name, action_name, response.body, parent_identifiers:) if response.body?
|
|
380
|
+
end
|
|
381
|
+
end
|
|
380
382
|
|
|
381
|
-
|
|
383
|
+
schemas.join("\n\n")
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def build_action_response_schemas
|
|
387
|
+
schemas = []
|
|
388
|
+
|
|
389
|
+
traverse_resources do |resource|
|
|
390
|
+
resource_name = resource.identifier.to_sym
|
|
391
|
+
parent_identifiers = resource.parent_identifiers
|
|
392
|
+
|
|
393
|
+
resource.actions.each do |action_name, action|
|
|
394
|
+
schemas << build_action_response_schema(resource_name, action_name, action.response, parent_identifiers:, raises: action.raises)
|
|
382
395
|
end
|
|
383
396
|
end
|
|
384
397
|
|
|
@@ -392,6 +405,8 @@ module Apiwork
|
|
|
392
405
|
end
|
|
393
406
|
end
|
|
394
407
|
|
|
408
|
+
private
|
|
409
|
+
|
|
395
410
|
def type_or_enum_reference?(symbol)
|
|
396
411
|
@export.api.types.key?(symbol) || @export.api.enums.key?(symbol)
|
|
397
412
|
end
|
data/lib/apiwork/export.rb
CHANGED
|
@@ -18,6 +18,14 @@ module Apiwork
|
|
|
18
18
|
@dump = dump
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# @api public
|
|
22
|
+
# The description for this request.
|
|
23
|
+
#
|
|
24
|
+
# @return [String, nil]
|
|
25
|
+
def description
|
|
26
|
+
@dump[:description]
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
# @api public
|
|
22
30
|
# The query for this request.
|
|
23
31
|
#
|
|
@@ -56,6 +64,7 @@ module Apiwork
|
|
|
56
64
|
# @return [Hash]
|
|
57
65
|
def to_h
|
|
58
66
|
{
|
|
67
|
+
description:,
|
|
59
68
|
body: body.transform_values(&:to_h),
|
|
60
69
|
query: query.transform_values(&:to_h),
|
|
61
70
|
}
|
|
@@ -28,6 +28,14 @@ module Apiwork
|
|
|
28
28
|
@body ||= @dump[:body] ? Param.build(@dump[:body]) : nil
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# @api public
|
|
32
|
+
# The description for this response.
|
|
33
|
+
#
|
|
34
|
+
# @return [String, nil]
|
|
35
|
+
def description
|
|
36
|
+
@dump[:description]
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
# @api public
|
|
32
40
|
# Whether this response has no content.
|
|
33
41
|
#
|
|
@@ -49,7 +57,7 @@ module Apiwork
|
|
|
49
57
|
#
|
|
50
58
|
# @return [Hash]
|
|
51
59
|
def to_h
|
|
52
|
-
{ body: body&.to_h, no_content: no_content? }
|
|
60
|
+
{ description:, body: body&.to_h, no_content: no_content? }
|
|
53
61
|
end
|
|
54
62
|
end
|
|
55
63
|
end
|
|
@@ -23,7 +23,7 @@ module Apiwork
|
|
|
23
23
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
|
-
def i18n_lookup(
|
|
26
|
+
def i18n_lookup(*segments)
|
|
27
27
|
contract_class = @contract_action.contract_class
|
|
28
28
|
return nil unless contract_class.name
|
|
29
29
|
|
|
@@ -32,15 +32,16 @@ module Apiwork
|
|
|
32
32
|
contract_class.name.demodulize.delete_suffix('Contract').underscore,
|
|
33
33
|
:actions,
|
|
34
34
|
@contract_action.name,
|
|
35
|
-
|
|
35
|
+
*segments,
|
|
36
36
|
)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def build_request(request)
|
|
40
|
-
return { body: {}, query: {} } unless request
|
|
40
|
+
return { body: {}, description: i18n_lookup(:request, :description), query: {} } unless request
|
|
41
41
|
|
|
42
42
|
{
|
|
43
43
|
body: build_param(request.body),
|
|
44
|
+
description: request.description || i18n_lookup(:request, :description),
|
|
44
45
|
query: build_param(request.query),
|
|
45
46
|
}
|
|
46
47
|
end
|
|
@@ -50,13 +51,14 @@ module Apiwork
|
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def build_response(response)
|
|
53
|
-
return { body: {}, no_content: false } unless response
|
|
54
|
-
return { body: {}, no_content: true } if response.no_content?
|
|
54
|
+
return { body: {}, description: i18n_lookup(:response, :description), no_content: false } unless response
|
|
55
|
+
return { body: {}, description: response.description || i18n_lookup(:response, :description), no_content: true } if response.no_content?
|
|
55
56
|
|
|
57
|
+
description = response.description || i18n_lookup(:response, :description)
|
|
56
58
|
body_shape = response.body
|
|
57
|
-
return { body: {}, no_content: false } unless body_shape
|
|
59
|
+
return { description:, body: {}, no_content: false } unless body_shape
|
|
58
60
|
|
|
59
|
-
{ body: Param.new(body_shape).to_h, no_content: false }
|
|
61
|
+
{ description:, body: Param.new(body_shape).to_h, no_content: false }
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def raises
|
data/lib/apiwork/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: apiwork
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- skiftle
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -252,6 +252,8 @@ files:
|
|
|
252
252
|
- lib/apiwork/export/pipeline.rb
|
|
253
253
|
- lib/apiwork/export/pipeline/writer.rb
|
|
254
254
|
- lib/apiwork/export/registry.rb
|
|
255
|
+
- lib/apiwork/export/sorbus.rb
|
|
256
|
+
- lib/apiwork/export/sorbus_mapper.rb
|
|
255
257
|
- lib/apiwork/export/surface_resolver.rb
|
|
256
258
|
- lib/apiwork/export/type_analysis.rb
|
|
257
259
|
- lib/apiwork/export/type_script.rb
|