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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef169b6db5b913f078945dbf9b29939139cd80d5d77e07c0339e9df070dfb259
4
- data.tar.gz: b979dc9b7b721f5949952a22572fa8ea99593f59f1c5161a91fc8757b28205d5
3
+ metadata.gz: eb5f681a0f08e4bebe2d0f5ebf4edac733f3787e4144f019bc4babfa7a7ef84a
4
+ data.tar.gz: e5d1d87115d2902295d66567c324d73ef15d01b0ac106c1fa9739b56a276aeb3
5
5
  SHA512:
6
- metadata.gz: 46548e8a016ade99fecc507ac9cb7d575b21278d4981e2ea5ebed9ddbdb81b2ff270669c4acca0f0cd6ce5e7437c9b14f62d51eff650492160e136f950a040fe
7
- data.tar.gz: 8aabaa2bd3d750d425429ba4f47d034a07c1562c3ca5f90405d2c1d17e1f159d499e64bd906667bf9f6fe7dd3ee0640863dcb797a264ba985fcee6753f74925b
6
+ metadata.gz: bb539d87fcd3b065d643711616f7f9f5589d574cbf12bcc06c69aafe7000f6fafa4e19d7fd34cd165d2734076d3b192472a6f51459074241fabbf5dd3faece42
7
+ data.tar.gz: 692745d73d86c3f5c377ae2a7962b38efe351b1fc893df6af45bd8de025622f61257307dedcd1b6aac15df8f3112874e92c54d3b3e316ef9aaa59ef6fcca0338
@@ -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, actions)
215
+ def register_contract(contract_class, representation_class, resource_actions: {})
218
216
  capabilities.each do |capability|
219
- capability.contract_types(contract_class, representation_class, actions)
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, actions)
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, actions)
254
- actions.each_value do |action|
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
@@ -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
- actions = resource ? resource.actions : {}
730
+ resource_actions = resource ? resource.actions : {}
731
731
 
732
- adapter.register_contract(contract_class, representation_class, actions)
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
- operation[:requestBody] = build_request_body(request.body) if request.body?
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: 'No content' }
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: 'Successful response',
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: 'Successful response',
237
+ description: response_description,
232
238
  }
233
239
  else
234
- responses[:'204'] = { description: 'No content' }
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(:error_response_body)} }" }
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
- private
262
+ def build_action_types
263
+ types = []
262
264
 
263
- def resolve_error_statuses(raises)
264
- raises.map { |code| @export.api.error_codes[code].status }.uniq.sort
265
- end
265
+ traverse_resources do |resource|
266
+ resource_name = resource.identifier.to_sym
267
+ parent_identifiers = resource.parent_identifiers
266
268
 
267
- def build_enum_types(surface)
268
- surface.enums.map do |name, enum|
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
- def build_type_definitions(surface)
274
- TypeAnalysis.topological_sort_types(surface.types.transform_values(&:to_h)).map(&:first).map do |type_name|
275
- type = surface.types[type_name]
276
- code = type.union? ? build_union_type(type_name, type) : build_interface(type_name, type)
277
- { code:, name: pascal_case(type_name) }
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 build_action_types
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
- types.concat(build_request_types(resource_name, action_name, action.request, parent_identifiers:))
290
- types.concat(build_response_types(resource_name, action_name, action, parent_identifiers:))
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(:error_response_body)}Schema })"
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
- private
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
- @surface.enums.map do |name, enum|
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 = @surface.types.transform_values(&:to_h)
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 = @surface.types[type_name]
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
- schemas << build_action_response_schema(resource_name, action_name, response, parent_identifiers:, raises: action.raises)
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
@@ -74,6 +74,7 @@ module Apiwork
74
74
  register(OpenAPI)
75
75
  register(TypeScript)
76
76
  register(Zod)
77
+ register(Sorbus)
77
78
  end
78
79
  end
79
80
  end
@@ -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(field)
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
- field,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Apiwork
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
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.2.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-26 00:00:00.000000000 Z
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