apiwork 0.4.0 → 0.5.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/serializer/resource/base.rb +15 -0
- data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +3 -2
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +2 -2
- data/lib/apiwork/api/base.rb +67 -17
- data/lib/apiwork/api/element.rb +33 -2
- data/lib/apiwork/api/object.rb +70 -5
- data/lib/apiwork/api/router.rb +16 -0
- data/lib/apiwork/configuration/validatable.rb +1 -0
- data/lib/apiwork/configuration.rb +2 -0
- data/lib/apiwork/contract/element.rb +17 -2
- data/lib/apiwork/contract/object/coercer.rb +24 -2
- data/lib/apiwork/contract/object/deserializer.rb +5 -1
- data/lib/apiwork/contract/object/transformer.rb +15 -2
- data/lib/apiwork/contract/object/validator.rb +45 -2
- data/lib/apiwork/contract/object.rb +77 -7
- data/lib/apiwork/element.rb +33 -0
- data/lib/apiwork/export/base.rb +1 -4
- data/lib/apiwork/export/builder_mapper.rb +184 -0
- data/lib/apiwork/export/open_api.rb +9 -2
- data/lib/apiwork/export/sorbus.rb +5 -1
- data/lib/apiwork/export/sorbus_mapper.rb +3 -7
- data/lib/apiwork/export/type_analysis.rb +20 -6
- data/lib/apiwork/export/type_script.rb +4 -1
- data/lib/apiwork/export/type_script_mapper.rb +25 -2
- data/lib/apiwork/export/zod.rb +9 -0
- data/lib/apiwork/export/zod_mapper.rb +22 -1
- data/lib/apiwork/introspection/dump/action.rb +1 -1
- data/lib/apiwork/introspection/dump/param.rb +36 -20
- data/lib/apiwork/introspection/dump/type.rb +31 -25
- data/lib/apiwork/introspection/param/array.rb +26 -0
- data/lib/apiwork/introspection/param/base.rb +16 -18
- data/lib/apiwork/introspection/param/binary.rb +36 -0
- data/lib/apiwork/introspection/param/boolean.rb +36 -0
- data/lib/apiwork/introspection/param/date.rb +36 -0
- data/lib/apiwork/introspection/param/date_time.rb +36 -0
- data/lib/apiwork/introspection/param/decimal.rb +26 -0
- data/lib/apiwork/introspection/param/integer.rb +26 -0
- data/lib/apiwork/introspection/param/number.rb +26 -0
- data/lib/apiwork/introspection/param/record.rb +71 -0
- data/lib/apiwork/introspection/param/string.rb +26 -0
- data/lib/apiwork/introspection/param/time.rb +36 -0
- data/lib/apiwork/introspection/param/uuid.rb +36 -0
- data/lib/apiwork/introspection/param.rb +1 -0
- data/lib/apiwork/object.rb +244 -2
- data/lib/apiwork/representation/attribute.rb +1 -1
- data/lib/apiwork/representation/base.rb +105 -0
- data/lib/apiwork/representation/element.rb +15 -5
- data/lib/apiwork/version.rb +1 -1
- metadata +4 -2
|
@@ -56,7 +56,7 @@ module Apiwork
|
|
|
56
56
|
#
|
|
57
57
|
# @param name [Symbol]
|
|
58
58
|
# The param name.
|
|
59
|
-
# @param type [Symbol, nil] (nil) [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
|
|
59
|
+
# @param type [Symbol, nil] (nil) [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :record, :string, :time, :union, :unknown, :uuid]
|
|
60
60
|
# The param type.
|
|
61
61
|
# @param as [Symbol, nil] (nil)
|
|
62
62
|
# The target attribute name.
|
|
@@ -83,7 +83,7 @@ module Apiwork
|
|
|
83
83
|
# @param nullable [Boolean] (false)
|
|
84
84
|
# Whether the value can be `null`.
|
|
85
85
|
# @param of [Symbol, Hash, nil] (nil)
|
|
86
|
-
# The element type. Arrays only.
|
|
86
|
+
# The element or value type. Arrays and records only.
|
|
87
87
|
# @param optional [Boolean] (false)
|
|
88
88
|
# Whether the param is optional.
|
|
89
89
|
# @param required [Boolean] (false)
|
|
@@ -109,6 +109,7 @@ module Apiwork
|
|
|
109
109
|
name,
|
|
110
110
|
type: nil,
|
|
111
111
|
as: nil,
|
|
112
|
+
custom_type: nil,
|
|
112
113
|
default: nil,
|
|
113
114
|
deprecated: false,
|
|
114
115
|
description: nil,
|
|
@@ -238,6 +239,69 @@ module Apiwork
|
|
|
238
239
|
)
|
|
239
240
|
end
|
|
240
241
|
|
|
242
|
+
# @api public
|
|
243
|
+
# Defines a record param with value type.
|
|
244
|
+
#
|
|
245
|
+
# @param name [Symbol]
|
|
246
|
+
# The param name.
|
|
247
|
+
# @param as [Symbol, nil] (nil)
|
|
248
|
+
# The target attribute name.
|
|
249
|
+
# @param default [Object, nil] (nil)
|
|
250
|
+
# The default value.
|
|
251
|
+
# @param deprecated [Boolean] (false)
|
|
252
|
+
# Whether deprecated. Metadata included in exports.
|
|
253
|
+
# @param description [String, nil] (nil)
|
|
254
|
+
# The description. Metadata included in exports.
|
|
255
|
+
# @param nullable [Boolean] (false)
|
|
256
|
+
# Whether the value can be `null`.
|
|
257
|
+
# @param optional [Boolean] (false)
|
|
258
|
+
# Whether the param is optional.
|
|
259
|
+
# @param required [Boolean] (false)
|
|
260
|
+
# Whether the param is required.
|
|
261
|
+
# @yield block for defining value type
|
|
262
|
+
# @yieldparam element [Contract::Element]
|
|
263
|
+
# @return [void]
|
|
264
|
+
#
|
|
265
|
+
# @example instance_eval style
|
|
266
|
+
# record :scores do
|
|
267
|
+
# integer
|
|
268
|
+
# end
|
|
269
|
+
#
|
|
270
|
+
# @example yield style
|
|
271
|
+
# record :scores do |element|
|
|
272
|
+
# element.integer
|
|
273
|
+
# end
|
|
274
|
+
def record(
|
|
275
|
+
name,
|
|
276
|
+
as: nil,
|
|
277
|
+
default: nil,
|
|
278
|
+
deprecated: false,
|
|
279
|
+
description: nil,
|
|
280
|
+
nullable: false,
|
|
281
|
+
optional: false,
|
|
282
|
+
required: false,
|
|
283
|
+
&block
|
|
284
|
+
)
|
|
285
|
+
raise ConfigurationError, 'record requires a block' unless block
|
|
286
|
+
|
|
287
|
+
element = Element.new(@contract_class)
|
|
288
|
+
block.arity.positive? ? yield(element) : element.instance_eval(&block)
|
|
289
|
+
element.validate!
|
|
290
|
+
|
|
291
|
+
param(
|
|
292
|
+
name,
|
|
293
|
+
as:,
|
|
294
|
+
default:,
|
|
295
|
+
deprecated:,
|
|
296
|
+
description:,
|
|
297
|
+
nullable:,
|
|
298
|
+
optional:,
|
|
299
|
+
required:,
|
|
300
|
+
of: element,
|
|
301
|
+
type: :record,
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
241
305
|
def wrapped?
|
|
242
306
|
@wrapped
|
|
243
307
|
end
|
|
@@ -264,8 +328,10 @@ module Apiwork
|
|
|
264
328
|
type_definition.params.each do |param_name, param_options|
|
|
265
329
|
nested_shape = param_options[:shape]
|
|
266
330
|
|
|
267
|
-
if param_options[:type] == :array && nested_shape.is_a?(
|
|
331
|
+
if param_options[:type] == :array && nested_shape.is_a?(API::Object)
|
|
268
332
|
target_param.param(param_name, **param_options.except(:name))
|
|
333
|
+
elsif param_options[:type] == :array
|
|
334
|
+
target_param.param(param_name, **param_options.except(:name, :shape, :discriminator))
|
|
269
335
|
elsif nested_shape.is_a?(API::Object)
|
|
270
336
|
copy_nested_object_param(target_param, param_name, param_options, nested_shape)
|
|
271
337
|
elsif nested_shape.is_a?(API::Union)
|
|
@@ -416,7 +482,9 @@ module Apiwork
|
|
|
416
482
|
as:,
|
|
417
483
|
options:
|
|
418
484
|
)
|
|
419
|
-
|
|
485
|
+
scope = type_definition.scope || @contract_class
|
|
486
|
+
|
|
487
|
+
union = Union.new(scope, discriminator: type_definition.discriminator)
|
|
420
488
|
|
|
421
489
|
type_definition.variants.each do |variant|
|
|
422
490
|
if variant[:shape].is_a?(API::Object)
|
|
@@ -467,8 +535,10 @@ module Apiwork
|
|
|
467
535
|
options:,
|
|
468
536
|
&block
|
|
469
537
|
)
|
|
538
|
+
scope = type_definition.scope || @contract_class
|
|
539
|
+
|
|
470
540
|
shape = Object.new(
|
|
471
|
-
|
|
541
|
+
scope,
|
|
472
542
|
action_name: @action_name,
|
|
473
543
|
visited_types: visited_types.dup.add([@contract_class.object_id, type]),
|
|
474
544
|
)
|
|
@@ -516,7 +586,7 @@ module Apiwork
|
|
|
516
586
|
end
|
|
517
587
|
|
|
518
588
|
def resolve_of(of, type, &block)
|
|
519
|
-
return nil unless
|
|
589
|
+
return nil unless [:array, :record].include?(type)
|
|
520
590
|
|
|
521
591
|
if block_given?
|
|
522
592
|
element = Element.new(@contract_class)
|
|
@@ -531,7 +601,7 @@ module Apiwork
|
|
|
531
601
|
end
|
|
532
602
|
|
|
533
603
|
def resolve_shape(shape, type, &block)
|
|
534
|
-
return nil if
|
|
604
|
+
return nil if [:array, :record].include?(type)
|
|
535
605
|
|
|
536
606
|
if shape
|
|
537
607
|
shape
|
data/lib/apiwork/element.rb
CHANGED
|
@@ -250,6 +250,19 @@ module Apiwork
|
|
|
250
250
|
of(:binary)
|
|
251
251
|
end
|
|
252
252
|
|
|
253
|
+
# @api public
|
|
254
|
+
# Defines an unknown.
|
|
255
|
+
#
|
|
256
|
+
# @return [void]
|
|
257
|
+
#
|
|
258
|
+
# @example
|
|
259
|
+
# array :values do
|
|
260
|
+
# unknown
|
|
261
|
+
# end
|
|
262
|
+
def unknown
|
|
263
|
+
of(:unknown)
|
|
264
|
+
end
|
|
265
|
+
|
|
253
266
|
# @api public
|
|
254
267
|
# Defines an object.
|
|
255
268
|
#
|
|
@@ -300,6 +313,26 @@ module Apiwork
|
|
|
300
313
|
of(:array, &block)
|
|
301
314
|
end
|
|
302
315
|
|
|
316
|
+
# @api public
|
|
317
|
+
# Defines a record.
|
|
318
|
+
#
|
|
319
|
+
# @yield block defining value type
|
|
320
|
+
# @yieldparam element [Element]
|
|
321
|
+
# @return [void]
|
|
322
|
+
#
|
|
323
|
+
# @example instance_eval style
|
|
324
|
+
# record :scores do
|
|
325
|
+
# integer
|
|
326
|
+
# end
|
|
327
|
+
#
|
|
328
|
+
# @example yield style
|
|
329
|
+
# record :scores do |element|
|
|
330
|
+
# element.integer
|
|
331
|
+
# end
|
|
332
|
+
def record(&block)
|
|
333
|
+
of(:record, &block)
|
|
334
|
+
end
|
|
335
|
+
|
|
303
336
|
# @api public
|
|
304
337
|
# Defines a union.
|
|
305
338
|
#
|
data/lib/apiwork/export/base.rb
CHANGED
|
@@ -263,10 +263,7 @@ module Apiwork
|
|
|
263
263
|
end
|
|
264
264
|
|
|
265
265
|
def extract_options_from_config(config)
|
|
266
|
-
self.class.options.keys.
|
|
267
|
-
value = config.public_send(key)
|
|
268
|
-
hash[key] = value unless value.nil?
|
|
269
|
-
end
|
|
266
|
+
config.to_h.slice(*self.class.options.keys).compact
|
|
270
267
|
end
|
|
271
268
|
|
|
272
269
|
def validate_options!
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apiwork
|
|
4
|
+
module Export
|
|
5
|
+
class BuilderMapper
|
|
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
|
+
end
|
|
15
|
+
|
|
16
|
+
def map(surface)
|
|
17
|
+
build_builders(surface.types)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_builders(types)
|
|
21
|
+
builders = types.sort_by { |name, _type| name.to_s }.flat_map do |name, type|
|
|
22
|
+
if type.union?
|
|
23
|
+
build_union_builders(name, type, types)
|
|
24
|
+
elsif type.object?
|
|
25
|
+
[build_object_builder(name, type)]
|
|
26
|
+
end
|
|
27
|
+
end.compact
|
|
28
|
+
|
|
29
|
+
builders.join("\n\n")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def build_object_builder(name, type)
|
|
35
|
+
type_name = pascal_case(name)
|
|
36
|
+
defaulted, required = classify_fields(type.shape)
|
|
37
|
+
|
|
38
|
+
if defaulted.empty?
|
|
39
|
+
build_passthrough_builder(type_name)
|
|
40
|
+
elsif required.empty?
|
|
41
|
+
build_all_defaulted_builder(type_name, defaulted)
|
|
42
|
+
else
|
|
43
|
+
build_mixed_builder(type_name, defaulted, required)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_passthrough_builder(type_name)
|
|
48
|
+
"export function build#{type_name}(fields: #{type_name}): #{type_name} {\n" \
|
|
49
|
+
" return fields;\n" \
|
|
50
|
+
'}'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_all_defaulted_builder(type_name, defaulted)
|
|
54
|
+
defaults = build_defaults_body(defaulted)
|
|
55
|
+
|
|
56
|
+
"export function build#{type_name}(fields?: Partial<#{type_name}>): #{type_name} {\n" \
|
|
57
|
+
" return {\n" \
|
|
58
|
+
"#{defaults}\n" \
|
|
59
|
+
" ...fields,\n" \
|
|
60
|
+
" };\n" \
|
|
61
|
+
'}'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_mixed_builder(type_name, defaulted, required)
|
|
65
|
+
required_keys = required.keys.map { |name| "'#{@export.transform_key(name)}'" }.sort.join(' | ')
|
|
66
|
+
fields_type = "Pick<#{type_name}, #{required_keys}> & Partial<#{type_name}>"
|
|
67
|
+
defaults = build_defaults_body(defaulted)
|
|
68
|
+
|
|
69
|
+
"export function build#{type_name}(fields: #{fields_type}): #{type_name} {\n" \
|
|
70
|
+
" return {\n" \
|
|
71
|
+
"#{defaults}\n" \
|
|
72
|
+
" ...fields,\n" \
|
|
73
|
+
" };\n" \
|
|
74
|
+
'}'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_defaults_body(defaulted)
|
|
78
|
+
defaulted.sort_by { |name, _param| name.to_s }.map do |name, param|
|
|
79
|
+
" #{@export.transform_key(name)}: #{serialize_default(param)},"
|
|
80
|
+
end.join("\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_union_builders(name, type, types)
|
|
84
|
+
type_name = pascal_case(name)
|
|
85
|
+
builders = [build_passthrough_builder(type_name)]
|
|
86
|
+
|
|
87
|
+
if type.discriminator
|
|
88
|
+
type.variants.each do |variant|
|
|
89
|
+
next unless variant.tag
|
|
90
|
+
next if variant.reference? && types.key?(variant.reference)
|
|
91
|
+
|
|
92
|
+
builders << build_variant_builder(type_name, type.discriminator, variant)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
builders
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_variant_builder(type_name, discriminator, variant)
|
|
100
|
+
discriminator_key = @export.transform_key(discriminator)
|
|
101
|
+
builder_name = variant.reference? ? "build#{pascal_case(variant.reference)}" : "build#{type_name}#{pascal_case(variant.tag)}"
|
|
102
|
+
extract_type = "Extract<#{type_name}, { #{discriminator_key}: '#{variant.tag}' }>"
|
|
103
|
+
|
|
104
|
+
variant_defaults = {}
|
|
105
|
+
variant_required = {}
|
|
106
|
+
|
|
107
|
+
variant_defaults, variant_required = classify_fields(variant.shape) if variant.object? && variant.shape.any?
|
|
108
|
+
|
|
109
|
+
fields_type = build_variant_fields_type(extract_type, discriminator_key, variant_defaults, variant_required)
|
|
110
|
+
|
|
111
|
+
body_lines = [" #{discriminator_key}: '#{variant.tag}',"]
|
|
112
|
+
variant_defaults.sort_by { |name, _param| name.to_s }.each do |name, param|
|
|
113
|
+
body_lines << " #{@export.transform_key(name)}: #{serialize_default(param)},"
|
|
114
|
+
end
|
|
115
|
+
body_lines << ' ...fields,'
|
|
116
|
+
|
|
117
|
+
"export function #{builder_name}(fields: #{fields_type}): #{type_name} {\n" \
|
|
118
|
+
" return {\n" \
|
|
119
|
+
"#{body_lines.join("\n")}\n" \
|
|
120
|
+
" };\n" \
|
|
121
|
+
'}'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_variant_fields_type(extract_type, discriminator_key, defaulted, required)
|
|
125
|
+
omitted_type = "Omit<#{extract_type}, '#{discriminator_key}'>"
|
|
126
|
+
|
|
127
|
+
if required.empty? && defaulted.empty?
|
|
128
|
+
omitted_type
|
|
129
|
+
elsif required.empty?
|
|
130
|
+
"Partial<#{omitted_type}>"
|
|
131
|
+
else
|
|
132
|
+
required_keys = required.keys.map { |name| "'#{@export.transform_key(name)}'" }.sort.join(' | ')
|
|
133
|
+
"Pick<#{omitted_type}, #{required_keys}> & Partial<#{omitted_type}>"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def classify_fields(shape)
|
|
138
|
+
defaulted = {}
|
|
139
|
+
required = {}
|
|
140
|
+
|
|
141
|
+
shape.sort_by { |name, _param| name.to_s }.each do |name, param|
|
|
142
|
+
if defaulted_field?(param)
|
|
143
|
+
defaulted[name] = param
|
|
144
|
+
else
|
|
145
|
+
required[name] = param
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
[defaulted, required]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def defaulted_field?(param)
|
|
153
|
+
return true if param.nullable?
|
|
154
|
+
return true if param.respond_to?(:default) && !param.default.nil?
|
|
155
|
+
|
|
156
|
+
false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def serialize_default(param)
|
|
160
|
+
if param.respond_to?(:default) && !param.default.nil?
|
|
161
|
+
serialize_value(param.default)
|
|
162
|
+
elsif param.nullable?
|
|
163
|
+
'null'
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def serialize_value(value)
|
|
168
|
+
case value
|
|
169
|
+
when String then "'#{value.gsub("'", "\\\\'")}'"
|
|
170
|
+
when Integer, Float then value.to_s
|
|
171
|
+
when BigDecimal then value.to_s('F')
|
|
172
|
+
when TrueClass, FalseClass then value.to_s
|
|
173
|
+
when Array then '[]'
|
|
174
|
+
when Hash then '{}'
|
|
175
|
+
else value.to_s
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def pascal_case(name)
|
|
180
|
+
name.to_s.camelize(:upper)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -319,7 +319,7 @@ module Apiwork
|
|
|
319
319
|
schema = map_param(param)
|
|
320
320
|
|
|
321
321
|
schema[:description] = param.description if param.description
|
|
322
|
-
schema[:example] = param.example if param.example
|
|
322
|
+
schema[:example] = param.example if param.concrete? && param.example
|
|
323
323
|
schema[:deprecated] = true if param.deprecated?
|
|
324
324
|
|
|
325
325
|
schema[:format] = param.format.to_s if param.formattable? && param.format
|
|
@@ -332,6 +332,8 @@ module Apiwork
|
|
|
332
332
|
map_object(param)
|
|
333
333
|
elsif param.array?
|
|
334
334
|
map_array(param)
|
|
335
|
+
elsif param.record?
|
|
336
|
+
map_record(param)
|
|
335
337
|
elsif param.union?
|
|
336
338
|
map_union(param)
|
|
337
339
|
elsif param.literal?
|
|
@@ -353,7 +355,7 @@ module Apiwork
|
|
|
353
355
|
}
|
|
354
356
|
|
|
355
357
|
result[:description] = param.description if param.description
|
|
356
|
-
result[:example] = param.example if param.example
|
|
358
|
+
result[:example] = param.example if param.respond_to?(:example) && param.example
|
|
357
359
|
|
|
358
360
|
param.shape.each do |name, field|
|
|
359
361
|
result[:properties][transform_key(name)] = map_field(field)
|
|
@@ -383,6 +385,11 @@ module Apiwork
|
|
|
383
385
|
}
|
|
384
386
|
end
|
|
385
387
|
|
|
388
|
+
def map_record(param)
|
|
389
|
+
value_schema = param.of ? map_param(param.of) : {}
|
|
390
|
+
{ additionalProperties: value_schema, type: 'object' }
|
|
391
|
+
end
|
|
392
|
+
|
|
386
393
|
def map_inline_object(shape)
|
|
387
394
|
result = { properties: {}, type: 'object' }
|
|
388
395
|
|
|
@@ -7,8 +7,12 @@ module Apiwork
|
|
|
7
7
|
output :string
|
|
8
8
|
file_extension '.ts'
|
|
9
9
|
|
|
10
|
+
option :builders, default: false, type: :boolean
|
|
11
|
+
|
|
10
12
|
def generate
|
|
11
|
-
SorbusMapper.map(self, surface)
|
|
13
|
+
output = SorbusMapper.map(self, surface)
|
|
14
|
+
output += "\n\n#{BuilderMapper.map(self, surface)}" if options[:builders]
|
|
15
|
+
output
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
private
|
|
@@ -37,10 +37,8 @@ module Apiwork
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def build_contract
|
|
40
|
-
contract = {
|
|
41
|
-
|
|
42
|
-
error: build_error_schema,
|
|
43
|
-
}
|
|
40
|
+
contract = { endpoints: build_endpoint_tree(@export.api.resources) }
|
|
41
|
+
contract[:error] = build_error_schema if @surface.types.key?(:error)
|
|
44
42
|
"export const contract = #{format_object(contract, indent: 0)} as const;"
|
|
45
43
|
end
|
|
46
44
|
|
|
@@ -80,9 +78,7 @@ module Apiwork
|
|
|
80
78
|
end
|
|
81
79
|
|
|
82
80
|
def transform_path(path)
|
|
83
|
-
path.gsub(
|
|
84
|
-
"#{::Regexp.last_match(1)}#{@export.transform_key(::Regexp.last_match(2))}"
|
|
85
|
-
end
|
|
81
|
+
path.to_s.gsub(/:(\w+)/) { ":#{@export.transform_key(::Regexp.last_match(1))}" }
|
|
86
82
|
end
|
|
87
83
|
|
|
88
84
|
def extract_path_params(path)
|
|
@@ -4,9 +4,8 @@ module Apiwork
|
|
|
4
4
|
module Export
|
|
5
5
|
class TypeAnalysis
|
|
6
6
|
PRIMITIVE_TYPES = %i[
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
unknown
|
|
7
|
+
array binary boolean date datetime decimal enum float integer json
|
|
8
|
+
literal number object record string text time union unknown uuid
|
|
10
9
|
].to_set.freeze
|
|
11
10
|
|
|
12
11
|
class << self
|
|
@@ -39,11 +38,22 @@ module Apiwork
|
|
|
39
38
|
|
|
40
39
|
def find_cycle_breaking_types(graph)
|
|
41
40
|
lazy_types = Set.new
|
|
41
|
+
reduced_graph = graph
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
loop do
|
|
44
|
+
previous_size = lazy_types.size
|
|
45
|
+
|
|
46
|
+
find_strongly_connected_components(reduced_graph).each do |component|
|
|
47
|
+
next if component.size == 1 && !reduced_graph[component.first].include?(component.first)
|
|
48
|
+
|
|
49
|
+
lazy_types.add(select_cycle_breaker(component, reduced_graph))
|
|
50
|
+
end
|
|
45
51
|
|
|
46
|
-
lazy_types.
|
|
52
|
+
break if lazy_types.size == previous_size
|
|
53
|
+
|
|
54
|
+
reduced_graph = reduced_graph
|
|
55
|
+
.reject { |node, _| lazy_types.include?(node) }
|
|
56
|
+
.transform_values { |deps| deps - lazy_types.to_a }
|
|
47
57
|
end
|
|
48
58
|
|
|
49
59
|
lazy_types
|
|
@@ -114,6 +124,10 @@ module Apiwork
|
|
|
114
124
|
state[:components] << component
|
|
115
125
|
end
|
|
116
126
|
|
|
127
|
+
def select_cycle_breaker(component, graph)
|
|
128
|
+
component.min_by { |node| [-(graph[node] & component).size, node.to_s] }
|
|
129
|
+
end
|
|
130
|
+
|
|
117
131
|
def collect_references(node, references, filter)
|
|
118
132
|
return unless node.is_a?(Hash)
|
|
119
133
|
|
|
@@ -7,10 +7,13 @@ module Apiwork
|
|
|
7
7
|
output :string
|
|
8
8
|
file_extension '.ts'
|
|
9
9
|
|
|
10
|
+
option :builders, default: false, type: :boolean
|
|
10
11
|
option :version, default: '5', enum: %w[4 5], type: :string
|
|
11
12
|
|
|
12
13
|
def generate
|
|
13
|
-
TypeScriptMapper.map(self, surface)
|
|
14
|
+
output = TypeScriptMapper.map(self, surface)
|
|
15
|
+
output += "\n\n#{BuilderMapper.map(self, surface)}" if options[:builders]
|
|
16
|
+
output
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
private
|
|
@@ -30,7 +30,7 @@ module Apiwork
|
|
|
30
30
|
ts_type = map_field(param)
|
|
31
31
|
optional_marker = param.optional? ? '?' : ''
|
|
32
32
|
|
|
33
|
-
prop_jsdoc = jsdoc(description: param.description, example: param.example)
|
|
33
|
+
prop_jsdoc = jsdoc(description: param.description, example: param.concrete? ? param.example : nil)
|
|
34
34
|
if prop_jsdoc
|
|
35
35
|
indented_jsdoc = prop_jsdoc.lines.map { |line| " #{line.chomp}" }.join("\n")
|
|
36
36
|
"#{indented_jsdoc}\n #{key}#{optional_marker}: #{ts_type};"
|
|
@@ -165,6 +165,8 @@ module Apiwork
|
|
|
165
165
|
map_object_type(param)
|
|
166
166
|
elsif param.array?
|
|
167
167
|
map_array_type(param)
|
|
168
|
+
elsif param.record?
|
|
169
|
+
map_record_type(param)
|
|
168
170
|
elsif param.union?
|
|
169
171
|
map_union_type(param)
|
|
170
172
|
elsif param.literal?
|
|
@@ -207,8 +209,29 @@ module Apiwork
|
|
|
207
209
|
end
|
|
208
210
|
end
|
|
209
211
|
|
|
212
|
+
def map_record_type(param)
|
|
213
|
+
value_type = param.of ? map_param(param.of) : 'unknown'
|
|
214
|
+
"Record<string, #{value_type}>"
|
|
215
|
+
end
|
|
216
|
+
|
|
210
217
|
def map_union_type(param)
|
|
211
|
-
param.variants.map
|
|
218
|
+
variant_types = param.variants.map do |variant|
|
|
219
|
+
if param.discriminator && variant.tag && variant.object?
|
|
220
|
+
discriminator_prop = "#{@export.transform_key(param.discriminator)}: '#{variant.tag}'"
|
|
221
|
+
properties = variant.shape.sort_by { |name, _| name.to_s }.map do |name, field|
|
|
222
|
+
"#{@export.transform_key(name)}: #{map_field(field)}"
|
|
223
|
+
end
|
|
224
|
+
all_properties = [discriminator_prop, *properties].join('; ')
|
|
225
|
+
"{ #{all_properties} }"
|
|
226
|
+
elsif param.discriminator && variant.tag
|
|
227
|
+
base_type = map_param(variant)
|
|
228
|
+
"{ #{@export.transform_key(param.discriminator)}: '#{variant.tag}' } & #{base_type}"
|
|
229
|
+
else
|
|
230
|
+
map_param(variant)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
variant_types.sort.join(' | ')
|
|
212
235
|
end
|
|
213
236
|
|
|
214
237
|
def map_literal_type(param)
|
data/lib/apiwork/export/zod.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Apiwork
|
|
|
7
7
|
output :string
|
|
8
8
|
file_extension '.ts'
|
|
9
9
|
|
|
10
|
+
option :builders, default: false, type: :boolean
|
|
10
11
|
option :version, default: '4', enum: %w[4], type: :string
|
|
11
12
|
|
|
12
13
|
def generate
|
|
@@ -26,6 +27,14 @@ module Apiwork
|
|
|
26
27
|
parts << ''
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
if options[:builders]
|
|
31
|
+
builder_output = BuilderMapper.map(self, surface)
|
|
32
|
+
if builder_output.present?
|
|
33
|
+
parts << builder_output
|
|
34
|
+
parts << ''
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
29
38
|
parts.join("\n")
|
|
30
39
|
end
|
|
31
40
|
|
|
@@ -200,6 +200,8 @@ module Apiwork
|
|
|
200
200
|
map_object_type(param)
|
|
201
201
|
elsif param.array?
|
|
202
202
|
map_array_type(param)
|
|
203
|
+
elsif param.record?
|
|
204
|
+
map_record_type(param)
|
|
203
205
|
elsif param.union?
|
|
204
206
|
map_union_type(param)
|
|
205
207
|
elsif param.literal?
|
|
@@ -248,6 +250,11 @@ module Apiwork
|
|
|
248
250
|
base
|
|
249
251
|
end
|
|
250
252
|
|
|
253
|
+
def map_record_type(param)
|
|
254
|
+
value_schema = param.of ? map_param(param.of) : 'z.unknown()'
|
|
255
|
+
"z.record(z.string(), #{value_schema})"
|
|
256
|
+
end
|
|
257
|
+
|
|
251
258
|
def map_union_type(param)
|
|
252
259
|
if param.discriminator
|
|
253
260
|
map_discriminated_union(param)
|
|
@@ -260,7 +267,21 @@ module Apiwork
|
|
|
260
267
|
def map_discriminated_union(param)
|
|
261
268
|
discriminator_field = @export.transform_key(param.discriminator)
|
|
262
269
|
|
|
263
|
-
variant_schemas = param.variants.map
|
|
270
|
+
variant_schemas = param.variants.map do |variant|
|
|
271
|
+
if variant.tag && variant.object?
|
|
272
|
+
discriminator_prop = "#{discriminator_field}: z.literal('#{variant.tag}')"
|
|
273
|
+
properties = variant.shape.sort_by { |name, _| name.to_s }.map do |name, field|
|
|
274
|
+
"#{@export.transform_key(name)}: #{map_field(field)}"
|
|
275
|
+
end
|
|
276
|
+
all_properties = [discriminator_prop, *properties].join(', ')
|
|
277
|
+
"z.object({ #{all_properties} })"
|
|
278
|
+
elsif variant.tag
|
|
279
|
+
base_schema = map_param(variant)
|
|
280
|
+
"#{base_schema}.extend({ #{discriminator_field}: z.literal('#{variant.tag}') })"
|
|
281
|
+
else
|
|
282
|
+
map_param(variant)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
264
285
|
|
|
265
286
|
"z.discriminatedUnion('#{discriminator_field}', [#{variant_schemas.join(', ')}])"
|
|
266
287
|
end
|
|
@@ -52,7 +52,7 @@ module Apiwork
|
|
|
52
52
|
|
|
53
53
|
def build_response(response)
|
|
54
54
|
return { body: {}, description: i18n_lookup(:response, :description), no_content: false } unless response
|
|
55
|
-
return { body:
|
|
55
|
+
return { body: nil, description: response.description || i18n_lookup(:response, :description), no_content: true } if response.no_content?
|
|
56
56
|
|
|
57
57
|
description = response.description || i18n_lookup(:response, :description)
|
|
58
58
|
body_shape = response.body
|