Skip to content

Commit acde604

Browse files
committed
feat!: use plural resource names for per-action types and add request/response wrappers
Per-action type names now use the plural resource identifier instead of the singular contract scope prefix. This aligns type names with endpoint naming (invoices/show, not invoice/show). Before: invoice_show_response_body, invoice_create_request_body After: invoices_show_response_body, invoices_create_request_body New wrapper types register *_request and *_response types in the type registry, referencing their leaf types (request_query, request_body, response_body). Surface resolver exposes action_types for export consumers. Sorbus now uses types from the type system directly instead of generating its own action schemas.
1 parent 045b03c commit acde604

9 files changed

Lines changed: 134 additions & 113 deletions

File tree

lib/apiwork/adapter/base.rb

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,16 @@ def register_api(api_class)
214214
build_error_response_body(api_class, error_serializer_class)
215215
end
216216

217-
def register_contract(contract_class, representation_class, actions)
217+
def register_contract(contract_class, representation_class, resource: nil)
218+
resource_actions = resource ? resource.actions : {}
219+
218220
capabilities.each do |capability|
219-
capability.contract_types(contract_class, representation_class, actions)
221+
capability.contract_types(contract_class, representation_class, resource_actions)
220222
end
221223

222224
self.class.resource_serializer.new(representation_class).contract_types(contract_class)
223225

224-
build_action_responses(contract_class, representation_class, actions)
226+
build_action_responses(contract_class, representation_class, resource_actions, resource.name) if resource
225227
end
226228

227229
def apply_request_transformers(request, phase:)
@@ -250,33 +252,35 @@ def apply_capabilities(data, representation_class, request, wrapper_type:)
250252
)
251253
end
252254

253-
def build_action_responses(contract_class, representation_class, actions)
255+
def build_action_responses(contract_class, representation_class, actions, resource_name)
254256
actions.each_value do |action|
255-
build_action_response(contract_class, representation_class, action)
257+
build_action_response(contract_class, representation_class, action, resource_name)
256258
end
257259
end
258260

259-
def build_action_response(contract_class, representation_class, action)
261+
def build_action_response(contract_class, representation_class, action, resource_name)
260262
contract_action = contract_class.action(action.name)
261263
return if contract_action.resets_response?
262264

263265
case action.name
264266
when :index
265-
build_collection_action_response(contract_class, representation_class, action, contract_action)
267+
build_collection_action_response(contract_class, representation_class, action, contract_action, resource_name)
266268
when :show, :create, :update
267-
build_member_action_response(contract_class, representation_class, action, contract_action)
269+
build_member_action_response(contract_class, representation_class, action, contract_action, resource_name)
268270
when :destroy
269271
contract_action.response { no_content! }
270272
else
271-
build_custom_action_response(contract_class, representation_class, action, contract_action)
273+
build_custom_action_response(contract_class, representation_class, action, contract_action, resource_name)
272274
end
273275

274-
build_request_query_type(contract_class, action.name, contract_action)
275-
build_request_body_type(contract_class, action.name, contract_action)
276+
build_request_query_type(contract_class, action.name, contract_action, resource_name)
277+
build_request_body_type(contract_class, action.name, contract_action, resource_name)
278+
build_request_type(contract_class, action.name, contract_action, resource_name)
279+
build_response_type(contract_class, action.name, contract_action, resource_name)
276280
end
277281

278-
def build_member_action_response(contract_class, representation_class, action, contract_action)
279-
build_response_body_type(contract_class, representation_class, action.name, :member)
282+
def build_member_action_response(contract_class, representation_class, action, contract_action, resource_name)
283+
build_response_body_type(contract_class, representation_class, action.name, :member, resource_name)
280284
member_shape_class = self.class.member_wrapper.shape_class
281285
data_type = resolve_resource_data_type(representation_class)
282286

@@ -287,8 +291,8 @@ def build_member_action_response(contract_class, representation_class, action, c
287291
end
288292
end
289293

290-
def build_collection_action_response(contract_class, representation_class, action, contract_action)
291-
build_response_body_type(contract_class, representation_class, action.name, :collection)
294+
def build_collection_action_response(contract_class, representation_class, action, contract_action, resource_name)
295+
build_response_body_type(contract_class, representation_class, action.name, :collection, resource_name)
292296
collection_shape_class = self.class.collection_wrapper.shape_class
293297
data_type = resolve_resource_data_type(representation_class)
294298

@@ -299,19 +303,20 @@ def build_collection_action_response(contract_class, representation_class, actio
299303
end
300304
end
301305

302-
def build_custom_action_response(contract_class, representation_class, action, contract_action)
306+
def build_custom_action_response(contract_class, representation_class, action, contract_action, resource_name)
303307
if action.method == :delete
304308
contract_action.response { no_content! }
305309
elsif action.collection?
306-
build_collection_action_response(contract_class, representation_class, action, contract_action)
310+
build_collection_action_response(contract_class, representation_class, action, contract_action, resource_name)
307311
elsif action.member?
308-
build_member_action_response(contract_class, representation_class, action, contract_action)
312+
build_member_action_response(contract_class, representation_class, action, contract_action, resource_name)
309313
end
310314
end
311315

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)
316+
def build_response_body_type(contract_class, representation_class, action_name, response_type, resource_name)
317+
type_name = build_action_type_name(resource_name, action_name, 'response_body')
318+
api_class = contract_class.api_class
319+
return if api_class.type_registry.key?(type_name)
315320

316321
shape_class = if response_type == :collection
317322
self.class.collection_wrapper.shape_class
@@ -320,35 +325,74 @@ def build_response_body_type(contract_class, representation_class, action_name,
320325
end
321326
data_type = resolve_resource_data_type(representation_class)
322327

323-
contract_class.object(type_name) do |object|
328+
api_class.register_object(type_name) do |object|
324329
shape_class.apply(object, representation_class.root_key, capabilities, representation_class, response_type, data_type:)
325330
end
326331
end
327332

328-
def build_request_query_type(contract_class, action_name, contract_action)
333+
def build_request_query_type(contract_class, action_name, contract_action, resource_name)
329334
request = contract_action.request
330335
return unless request.query.params.any?
331336

332-
type_name = [action_name, 'request_query'].join('_').to_sym
333-
return if contract_class.type?(type_name)
337+
type_name = build_action_type_name(resource_name, action_name, 'request_query')
338+
api_class = contract_class.api_class
339+
return if api_class.type_registry.key?(type_name)
334340

335-
contract_class.object(type_name) do |object|
341+
api_class.register_object(type_name) do |object|
336342
request.query.params.each { |name, param| object.param(name, **normalize_request_param(param)) }
337343
end
338344
end
339345

340-
def build_request_body_type(contract_class, action_name, contract_action)
346+
def build_request_body_type(contract_class, action_name, contract_action, resource_name)
341347
request = contract_action.request
342348
return unless request.body.params.any?
343349

344-
type_name = [action_name, 'request_body'].join('_').to_sym
345-
return if contract_class.type?(type_name)
350+
type_name = build_action_type_name(resource_name, action_name, 'request_body')
351+
api_class = contract_class.api_class
352+
return if api_class.type_registry.key?(type_name)
346353

347-
contract_class.object(type_name) do |object|
354+
api_class.register_object(type_name) do |object|
348355
request.body.params.each { |name, param| object.param(name, **normalize_request_param(param)) }
349356
end
350357
end
351358

359+
def build_request_type(contract_class, action_name, contract_action, resource_name)
360+
request = contract_action.request
361+
return unless request.query.params.any? || request.body.params.any?
362+
363+
type_name = build_action_type_name(resource_name, action_name, 'request')
364+
api_class = contract_class.api_class
365+
return if api_class.type_registry.key?(type_name)
366+
367+
query_type_name = build_action_type_name(resource_name, action_name, 'request_query')
368+
body_type_name = build_action_type_name(resource_name, action_name, 'request_body')
369+
370+
api_class.register_object(type_name) do |object|
371+
object.param(:query, type: query_type_name) if request.query.params.any?
372+
object.param(:body, type: body_type_name) if request.body.params.any?
373+
end
374+
end
375+
376+
def build_response_type(contract_class, action_name, contract_action, resource_name)
377+
type_name = build_action_type_name(resource_name, action_name, 'response')
378+
api_class = contract_class.api_class
379+
return if api_class.type_registry.key?(type_name)
380+
381+
body_type_name = build_action_type_name(resource_name, action_name, 'response_body')
382+
383+
if contract_action.response.no_content?
384+
api_class.register_object(type_name) { |_object| }
385+
else
386+
api_class.register_object(type_name) do |object|
387+
object.param(:body, type: body_type_name)
388+
end
389+
end
390+
end
391+
392+
def build_action_type_name(resource_name, action_name, suffix)
393+
[resource_name, action_name, suffix].join('_').to_sym
394+
end
395+
352396
def normalize_request_param(param)
353397
options = param.except(:name, :custom_type, :union, :partial)
354398
options[:type] = param[:custom_type] if param[:custom_type]

lib/apiwork/api/base.rb

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -727,9 +727,8 @@ def ensure_contract_built!(contract_class)
727727
@built_contracts.add(contract_class)
728728

729729
resource = @root_resource.find_resource { |resource| resource.resolve_contract_class == contract_class }
730-
actions = resource ? resource.actions : {}
731730

732-
adapter.register_contract(contract_class, representation_class, actions)
731+
adapter.register_contract(contract_class, representation_class, resource:)
733732
end
734733

735734
def ensure_pre_pass_complete!
@@ -788,13 +787,11 @@ def build_contracts_for_resource(resource)
788787
contract_class = resource.resolve_contract_class
789788
return unless contract_class
790789
return if @built_contracts.include?(contract_class)
791-
792-
representation_class = contract_class.representation_class
793-
return unless representation_class
790+
return unless contract_class.representation_class
794791

795792
@built_contracts.add(contract_class)
796793

797-
adapter.register_contract(contract_class, representation_class, resource.actions)
794+
adapter.register_contract(contract_class, contract_class.representation_class, resource:)
798795
end
799796
end
800797
end

lib/apiwork/export/surface_resolver.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,36 @@ def enums
2121
@enums ||= compute_reachable_enums
2222
end
2323

24+
def action_types
25+
@action_types ||= compute_action_types
26+
end
27+
2428
private
2529

30+
def compute_action_types
31+
type_names = Set.new
32+
@api.resources.each_value do |resource|
33+
collect_action_type_names_from_resource(resource, type_names)
34+
end
35+
@api.types.select { |name, _| type_names.include?(name) }
36+
end
37+
38+
def collect_action_type_names_from_resource(resource, type_names, parent_identifiers: [])
39+
identifier = resource.identifier
40+
prefix = (parent_identifiers + [identifier]).join('_')
41+
42+
resource.actions.each_key do |action_name|
43+
%w[request_query request_body request response_body response].each do |suffix|
44+
type_name = [prefix, action_name, suffix].join('_').to_sym
45+
type_names << type_name if @api.types.key?(type_name)
46+
end
47+
end
48+
49+
resource.resources.each_value do |nested|
50+
collect_action_type_names_from_resource(nested, type_names, parent_identifiers: parent_identifiers + [identifier])
51+
end
52+
end
53+
2654
def compute_reachable_types
2755
type_names = collect_type_names_from_actions
2856
expand_transitive_dependencies(type_names)

lib/apiwork/export/type_script_mapper.rb

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ def initialize(export)
1414
end
1515

1616
def map(surface)
17-
types = build_enum_types(surface) +
18-
build_type_definitions(surface) +
17+
types = build_enum_types(surface.enums) +
18+
build_type_definitions(surface.types) +
1919
build_action_types +
2020
build_action_response_types
2121

@@ -298,47 +298,22 @@ def build_action_response_types
298298
types
299299
end
300300

301-
def build_action_response_envelope_types
302-
types = []
303-
304-
traverse_resources do |resource|
305-
resource_name = resource.identifier.to_sym
306-
parent_identifiers = resource.parent_identifiers
307-
308-
resource.actions.each do |action_name, action|
309-
type_name = action_type_name(resource_name, action_name, 'Response', parent_identifiers:)
310-
response = action.response
311-
312-
code = if response.no_content?
313-
"export interface #{type_name} {}"
314-
else
315-
body_ref = action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)
316-
"export interface #{type_name} {\n body: #{body_ref};\n}"
317-
end
318-
319-
types << { code:, name: type_name }
320-
end
321-
end
322-
323-
types
324-
end
325-
326301
def traverse_resources(resources: @export.api.resources, &block)
327302
resources.each_value do |resource|
328303
yield(resource)
329304
traverse_resources(resources: resource.resources, &block) if resource.resources.any?
330305
end
331306
end
332307

333-
def build_enum_types(surface)
334-
surface.enums.map do |name, enum|
308+
def build_enum_types(enums)
309+
enums.map do |name, enum|
335310
{ code: build_enum_type(name, enum), name: pascal_case(name) }
336311
end
337312
end
338313

339-
def build_type_definitions(surface)
340-
TypeAnalysis.topological_sort_types(surface.types.transform_values(&:to_h)).map(&:first).map do |type_name|
341-
type = surface.types[type_name]
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]
342317
code = type.union? ? build_union_type(type_name, type) : build_interface(type_name, type)
343318
{ code:, name: pascal_case(type_name) }
344319
end

lib/apiwork/export/zod_mapper.rb

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ def initialize(export)
3030
def map(surface)
3131
parts = []
3232

33-
enum_schemas = build_enum_schemas(surface)
33+
enum_schemas = build_enum_schemas(surface.enums)
3434
parts << enum_schemas if enum_schemas.present?
3535

36-
type_schemas = build_type_schemas(surface)
36+
type_schemas = build_type_schemas(surface.types)
3737
parts << type_schemas if type_schemas.present?
3838

3939
action_schemas = build_action_schemas
@@ -317,20 +317,20 @@ def pascal_case(name)
317317
name.to_s.camelize(:upper)
318318
end
319319

320-
def build_enum_schemas(surface)
321-
return '' if surface.enums.empty?
320+
def build_enum_schemas(enums)
321+
return '' if enums.empty?
322322

323-
surface.enums.map do |name, enum|
323+
enums.map do |name, enum|
324324
"export const #{pascal_case(name)}Schema = z.enum([#{enum.values.sort.map { |value| "'#{value}'" }.join(', ')}]);"
325325
end.join("\n\n")
326326
end
327327

328-
def build_type_schemas(surface)
329-
types_hash = surface.types.transform_values(&:to_h)
328+
def build_type_schemas(types)
329+
types_hash = types.transform_values(&:to_h)
330330
lazy_types = TypeAnalysis.cycle_breaking_types(types_hash)
331331

332332
TypeAnalysis.topological_sort_types(types_hash).map(&:first).map do |type_name|
333-
type = surface.types[type_name]
333+
type = types[type_name]
334334
recursive = lazy_types.include?(type_name)
335335

336336
if type.union?
@@ -398,29 +398,6 @@ def build_action_response_schemas
398398
schemas.join("\n\n")
399399
end
400400

401-
def build_action_response_envelope_schemas
402-
schemas = []
403-
404-
traverse_resources do |resource|
405-
resource_name = resource.identifier.to_sym
406-
parent_identifiers = resource.parent_identifiers
407-
408-
resource.actions.each do |action_name, action|
409-
schema_name = "#{action_type_name(resource_name, action_name, 'Response', parent_identifiers:)}Schema"
410-
response = action.response
411-
412-
schemas << if response.no_content?
413-
"export const #{schema_name} = z.object({});"
414-
else
415-
body_ref = "#{action_type_name(resource_name, action_name, 'ResponseBody', parent_identifiers:)}Schema"
416-
"export const #{schema_name} = z.object({ body: #{body_ref} });"
417-
end
418-
end
419-
end
420-
421-
schemas.join("\n\n")
422-
end
423-
424401
def traverse_resources(resources: @export.api.resources, &block)
425402
resources.each_value do |resource|
426403
yield(resource)

0 commit comments

Comments
 (0)