apiwork 0.4.0 → 0.6.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/error/default/api_builder.rb +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 +46 -3
- data/lib/apiwork/contract/object.rb +85 -7
- data/lib/apiwork/controller.rb +15 -2
- 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 +252 -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
|
@@ -163,7 +163,7 @@ module Apiwork
|
|
|
163
163
|
def validate_enum_value(name, value, enum, field_path)
|
|
164
164
|
enum_values = resolve_enum(enum)
|
|
165
165
|
return nil unless enum_values
|
|
166
|
-
return nil if enum_values.
|
|
166
|
+
return nil if enum_values.any? { |enum_value| enum_value.to_s == value.to_s }
|
|
167
167
|
|
|
168
168
|
Issue.new(
|
|
169
169
|
:value_invalid,
|
|
@@ -201,6 +201,8 @@ module Apiwork
|
|
|
201
201
|
validate_shape_object(value, param_options[:shape], field_path, max_depth, current_depth)
|
|
202
202
|
elsif param_options[:type] == :array && value.is_a?(Array)
|
|
203
203
|
validate_array_param(value, param_options, field_path, max_depth, current_depth)
|
|
204
|
+
elsif param_options[:type] == :record && value.is_a?(Hash)
|
|
205
|
+
validate_record_param(value, param_options, field_path, max_depth, current_depth)
|
|
204
206
|
else
|
|
205
207
|
[[], value]
|
|
206
208
|
end
|
|
@@ -230,6 +232,41 @@ module Apiwork
|
|
|
230
232
|
array_issues.empty? ? [[], array_values] : [array_issues, NOT_SET]
|
|
231
233
|
end
|
|
232
234
|
|
|
235
|
+
def validate_record_param(value, param_options, field_path, max_depth, current_depth)
|
|
236
|
+
issues = []
|
|
237
|
+
validated = {}
|
|
238
|
+
|
|
239
|
+
of = param_options[:of]
|
|
240
|
+
of_type = of&.type
|
|
241
|
+
of_shape = of&.shape
|
|
242
|
+
|
|
243
|
+
value.each do |key, item|
|
|
244
|
+
item_path = field_path + [key]
|
|
245
|
+
|
|
246
|
+
if of_shape
|
|
247
|
+
validator = Validator.new(normalize_shape(of_shape))
|
|
248
|
+
shape_result = validator.validate(
|
|
249
|
+
item,
|
|
250
|
+
max_depth:,
|
|
251
|
+
current_depth: current_depth + 1,
|
|
252
|
+
path: item_path,
|
|
253
|
+
)
|
|
254
|
+
if shape_result.invalid?
|
|
255
|
+
issues.concat(shape_result.issues)
|
|
256
|
+
else
|
|
257
|
+
validated[key] = shape_result.params
|
|
258
|
+
end
|
|
259
|
+
elsif of_type
|
|
260
|
+
type_error = validate_type(key, item, of_type, item_path)
|
|
261
|
+
type_error ? issues << type_error : validated[key] = item
|
|
262
|
+
else
|
|
263
|
+
validated[key] = item
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
issues.empty? ? [[], validated] : [issues, NOT_SET]
|
|
268
|
+
end
|
|
269
|
+
|
|
233
270
|
def check_unknown_params(data, path)
|
|
234
271
|
extra_keys = data.keys - @shape.params.keys
|
|
235
272
|
extra_keys.map do |key|
|
|
@@ -280,7 +317,10 @@ module Apiwork
|
|
|
280
317
|
array.each_with_index do |item, index|
|
|
281
318
|
item_path = field_path + [index]
|
|
282
319
|
|
|
283
|
-
if of_shape
|
|
320
|
+
if of&.type == :union && of_shape.is_a?(Apiwork::Union)
|
|
321
|
+
error, validated = validate_union(nil, item, of_shape, item_path, current_depth:, max_depth:)
|
|
322
|
+
error ? issues << error : values << validated
|
|
323
|
+
elsif of_shape
|
|
284
324
|
validator = Validator.new(normalize_shape(of_shape))
|
|
285
325
|
shape_result = validator.validate(
|
|
286
326
|
item,
|
|
@@ -369,6 +409,7 @@ module Apiwork
|
|
|
369
409
|
when :uuid then value.is_a?(String) && value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
|
|
370
410
|
when :object then value.is_a?(Hash)
|
|
371
411
|
when :array then value.is_a?(Array)
|
|
412
|
+
when :record then value.is_a?(Hash)
|
|
372
413
|
when :decimal, :number then value.is_a?(Numeric)
|
|
373
414
|
else true
|
|
374
415
|
end
|
|
@@ -600,7 +641,9 @@ module Apiwork
|
|
|
600
641
|
end
|
|
601
642
|
|
|
602
643
|
def validate_with_type_definition(type_definition, value, path, current_depth:, exclude_param: nil, max_depth:)
|
|
603
|
-
|
|
644
|
+
scope = type_definition.scope || @shape.contract_class
|
|
645
|
+
|
|
646
|
+
type_shape = Object.new(scope, action_name: @shape.action_name)
|
|
604
647
|
type_shape.copy_type_definition_params(type_definition, type_shape)
|
|
605
648
|
type_shape.params.delete(exclude_param) if exclude_param
|
|
606
649
|
|
|
@@ -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,
|
|
@@ -188,6 +189,10 @@ module Apiwork
|
|
|
188
189
|
# Whether deprecated. Metadata included in exports.
|
|
189
190
|
# @param description [String, nil] (nil)
|
|
190
191
|
# The description. Metadata included in exports.
|
|
192
|
+
# @param max [Integer, nil] (nil)
|
|
193
|
+
# The maximum number of elements.
|
|
194
|
+
# @param min [Integer, nil] (nil)
|
|
195
|
+
# The minimum number of elements.
|
|
191
196
|
# @param nullable [Boolean] (false)
|
|
192
197
|
# Whether the value can be `null`.
|
|
193
198
|
# @param optional [Boolean] (false)
|
|
@@ -213,6 +218,8 @@ module Apiwork
|
|
|
213
218
|
default: nil,
|
|
214
219
|
deprecated: false,
|
|
215
220
|
description: nil,
|
|
221
|
+
max: nil,
|
|
222
|
+
min: nil,
|
|
216
223
|
nullable: false,
|
|
217
224
|
optional: false,
|
|
218
225
|
required: false,
|
|
@@ -230,6 +237,8 @@ module Apiwork
|
|
|
230
237
|
default:,
|
|
231
238
|
deprecated:,
|
|
232
239
|
description:,
|
|
240
|
+
max:,
|
|
241
|
+
min:,
|
|
233
242
|
nullable:,
|
|
234
243
|
optional:,
|
|
235
244
|
required:,
|
|
@@ -238,6 +247,69 @@ module Apiwork
|
|
|
238
247
|
)
|
|
239
248
|
end
|
|
240
249
|
|
|
250
|
+
# @api public
|
|
251
|
+
# Defines a record param with value type.
|
|
252
|
+
#
|
|
253
|
+
# @param name [Symbol]
|
|
254
|
+
# The param name.
|
|
255
|
+
# @param as [Symbol, nil] (nil)
|
|
256
|
+
# The target attribute name.
|
|
257
|
+
# @param default [Object, nil] (nil)
|
|
258
|
+
# The default value.
|
|
259
|
+
# @param deprecated [Boolean] (false)
|
|
260
|
+
# Whether deprecated. Metadata included in exports.
|
|
261
|
+
# @param description [String, nil] (nil)
|
|
262
|
+
# The description. Metadata included in exports.
|
|
263
|
+
# @param nullable [Boolean] (false)
|
|
264
|
+
# Whether the value can be `null`.
|
|
265
|
+
# @param optional [Boolean] (false)
|
|
266
|
+
# Whether the param is optional.
|
|
267
|
+
# @param required [Boolean] (false)
|
|
268
|
+
# Whether the param is required.
|
|
269
|
+
# @yield block for defining value type
|
|
270
|
+
# @yieldparam element [Contract::Element]
|
|
271
|
+
# @return [void]
|
|
272
|
+
#
|
|
273
|
+
# @example instance_eval style
|
|
274
|
+
# record :scores do
|
|
275
|
+
# integer
|
|
276
|
+
# end
|
|
277
|
+
#
|
|
278
|
+
# @example yield style
|
|
279
|
+
# record :scores do |element|
|
|
280
|
+
# element.integer
|
|
281
|
+
# end
|
|
282
|
+
def record(
|
|
283
|
+
name,
|
|
284
|
+
as: nil,
|
|
285
|
+
default: nil,
|
|
286
|
+
deprecated: false,
|
|
287
|
+
description: nil,
|
|
288
|
+
nullable: false,
|
|
289
|
+
optional: false,
|
|
290
|
+
required: false,
|
|
291
|
+
&block
|
|
292
|
+
)
|
|
293
|
+
raise ConfigurationError, 'record requires a block' unless block
|
|
294
|
+
|
|
295
|
+
element = Element.new(@contract_class)
|
|
296
|
+
block.arity.positive? ? yield(element) : element.instance_eval(&block)
|
|
297
|
+
element.validate!
|
|
298
|
+
|
|
299
|
+
param(
|
|
300
|
+
name,
|
|
301
|
+
as:,
|
|
302
|
+
default:,
|
|
303
|
+
deprecated:,
|
|
304
|
+
description:,
|
|
305
|
+
nullable:,
|
|
306
|
+
optional:,
|
|
307
|
+
required:,
|
|
308
|
+
of: element,
|
|
309
|
+
type: :record,
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
241
313
|
def wrapped?
|
|
242
314
|
@wrapped
|
|
243
315
|
end
|
|
@@ -264,8 +336,10 @@ module Apiwork
|
|
|
264
336
|
type_definition.params.each do |param_name, param_options|
|
|
265
337
|
nested_shape = param_options[:shape]
|
|
266
338
|
|
|
267
|
-
if param_options[:type] == :array && nested_shape.is_a?(
|
|
339
|
+
if param_options[:type] == :array && nested_shape.is_a?(API::Object)
|
|
268
340
|
target_param.param(param_name, **param_options.except(:name))
|
|
341
|
+
elsif param_options[:type] == :array
|
|
342
|
+
target_param.param(param_name, **param_options.except(:name, :shape, :discriminator))
|
|
269
343
|
elsif nested_shape.is_a?(API::Object)
|
|
270
344
|
copy_nested_object_param(target_param, param_name, param_options, nested_shape)
|
|
271
345
|
elsif nested_shape.is_a?(API::Union)
|
|
@@ -416,7 +490,9 @@ module Apiwork
|
|
|
416
490
|
as:,
|
|
417
491
|
options:
|
|
418
492
|
)
|
|
419
|
-
|
|
493
|
+
scope = type_definition.scope || @contract_class
|
|
494
|
+
|
|
495
|
+
union = Union.new(scope, discriminator: type_definition.discriminator)
|
|
420
496
|
|
|
421
497
|
type_definition.variants.each do |variant|
|
|
422
498
|
if variant[:shape].is_a?(API::Object)
|
|
@@ -467,8 +543,10 @@ module Apiwork
|
|
|
467
543
|
options:,
|
|
468
544
|
&block
|
|
469
545
|
)
|
|
546
|
+
scope = type_definition.scope || @contract_class
|
|
547
|
+
|
|
470
548
|
shape = Object.new(
|
|
471
|
-
|
|
549
|
+
scope,
|
|
472
550
|
action_name: @action_name,
|
|
473
551
|
visited_types: visited_types.dup.add([@contract_class.object_id, type]),
|
|
474
552
|
)
|
|
@@ -516,7 +594,7 @@ module Apiwork
|
|
|
516
594
|
end
|
|
517
595
|
|
|
518
596
|
def resolve_of(of, type, &block)
|
|
519
|
-
return nil unless
|
|
597
|
+
return nil unless [:array, :record].include?(type)
|
|
520
598
|
|
|
521
599
|
if block_given?
|
|
522
600
|
element = Element.new(@contract_class)
|
|
@@ -531,7 +609,7 @@ module Apiwork
|
|
|
531
609
|
end
|
|
532
610
|
|
|
533
611
|
def resolve_shape(shape, type, &block)
|
|
534
|
-
return nil if
|
|
612
|
+
return nil if [:array, :record].include?(type)
|
|
535
613
|
|
|
536
614
|
if shape
|
|
537
615
|
shape
|
data/lib/apiwork/controller.rb
CHANGED
|
@@ -138,10 +138,10 @@ module Apiwork
|
|
|
138
138
|
end
|
|
139
139
|
else
|
|
140
140
|
data[:meta] = meta if meta.present?
|
|
141
|
-
data
|
|
141
|
+
data.deep_symbolize_keys
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
-
response = Response.new(body:)
|
|
144
|
+
response = Response.new(body: deep_as_json(body))
|
|
145
145
|
|
|
146
146
|
if Rails.env.development?
|
|
147
147
|
result = contract_class.parse_response(response, action_name)
|
|
@@ -223,6 +223,19 @@ module Apiwork
|
|
|
223
223
|
|
|
224
224
|
private
|
|
225
225
|
|
|
226
|
+
def deep_as_json(value)
|
|
227
|
+
case value
|
|
228
|
+
when Hash
|
|
229
|
+
value.transform_values { |nested_value| deep_as_json(nested_value) }
|
|
230
|
+
when Array
|
|
231
|
+
value.map { |element| deep_as_json(element) }
|
|
232
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass, Symbol, Time, Date, BigDecimal
|
|
233
|
+
value
|
|
234
|
+
else
|
|
235
|
+
value.as_json
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
226
239
|
def validate_contract
|
|
227
240
|
return unless resource
|
|
228
241
|
return if contract.valid?
|
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
|