apiwork 0.3.1 → 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 +4 -3
- data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +13 -9
- data/lib/apiwork/api/base.rb +105 -17
- data/lib/apiwork/api/element.rb +35 -4
- data/lib/apiwork/api/object.rb +72 -7
- 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 +19 -4
- data/lib/apiwork/contract/object/coercer.rb +31 -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 +49 -11
- data/lib/apiwork/contract/object.rb +79 -9
- data/lib/apiwork/element.rb +34 -1
- 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/api.rb +18 -0
- data/lib/apiwork/introspection/dump/action.rb +1 -1
- data/lib/apiwork/introspection/dump/api.rb +2 -0
- data/lib/apiwork/introspection/dump/param.rb +36 -20
- data/lib/apiwork/introspection/dump/resource.rb +7 -4
- 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 +15 -25
- 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/introspection.rb +17 -4
- data/lib/apiwork/object.rb +246 -4
- data/lib/apiwork/representation/attribute.rb +2 -2
- data/lib/apiwork/representation/base.rb +107 -2
- data/lib/apiwork/representation/element.rb +15 -5
- data/lib/apiwork/version.rb +1 -1
- metadata +6 -4
|
@@ -85,6 +85,7 @@ module Apiwork
|
|
|
85
85
|
|
|
86
86
|
return coerce_union(value, param_options[:union]) if type == :union
|
|
87
87
|
return coerce_array(value, param_options) if type == :array && value.is_a?(Array)
|
|
88
|
+
return coerce_record(value, param_options) if type == :record && value.is_a?(Hash)
|
|
88
89
|
return Coercer.coerce(param_options[:shape], value) if param_options[:shape] && value.is_a?(Hash)
|
|
89
90
|
|
|
90
91
|
if value.is_a?(Hash) && type && !PRIMITIVES.key?(type)
|
|
@@ -105,7 +106,9 @@ module Apiwork
|
|
|
105
106
|
custom_shape = resolve_custom_shape(of_type) if of_type && !PRIMITIVES.key?(of_type)
|
|
106
107
|
|
|
107
108
|
array.map do |item|
|
|
108
|
-
if
|
|
109
|
+
if of_type == :union && item.is_a?(Hash)
|
|
110
|
+
coerce_union(item, of_shape)
|
|
111
|
+
elsif of_shape && item.is_a?(Hash)
|
|
109
112
|
Coercer.coerce(of_shape, item)
|
|
110
113
|
elsif of_type && PRIMITIVES.key?(of_type)
|
|
111
114
|
coerced = coerce_primitive(item, of_type)
|
|
@@ -120,6 +123,23 @@ module Apiwork
|
|
|
120
123
|
end
|
|
121
124
|
end
|
|
122
125
|
|
|
126
|
+
def coerce_record(hash, param_options)
|
|
127
|
+
of = param_options[:of]
|
|
128
|
+
of_type = of&.type
|
|
129
|
+
of_shape = of&.shape
|
|
130
|
+
|
|
131
|
+
hash.transform_values do |item|
|
|
132
|
+
if of_shape && item.is_a?(Hash)
|
|
133
|
+
Coercer.coerce(of_shape, item)
|
|
134
|
+
elsif of_type && PRIMITIVES.key?(of_type)
|
|
135
|
+
coerced = coerce_primitive(item, of_type)
|
|
136
|
+
coerced.nil? ? item : coerced
|
|
137
|
+
else
|
|
138
|
+
item
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
123
143
|
def coerce_union(value, union)
|
|
124
144
|
if union.variants.any? { |variant| variant[:type] == :boolean }
|
|
125
145
|
coerced = coerce_primitive(value, :boolean)
|
|
@@ -156,6 +176,13 @@ module Apiwork
|
|
|
156
176
|
|
|
157
177
|
next if discriminator
|
|
158
178
|
|
|
179
|
+
if PRIMITIVES.key?(variant_type) && !value.is_a?(Hash) && !value.is_a?(Array)
|
|
180
|
+
coerced = coerce_primitive(value, variant_type)
|
|
181
|
+
return coerced unless coerced.nil?
|
|
182
|
+
|
|
183
|
+
next
|
|
184
|
+
end
|
|
185
|
+
|
|
159
186
|
custom_shape = resolve_custom_shape(variant_type)
|
|
160
187
|
next unless custom_shape
|
|
161
188
|
|
|
@@ -184,7 +211,9 @@ module Apiwork
|
|
|
184
211
|
type_definition = @shape.contract_class.resolve_custom_type(type_name)
|
|
185
212
|
return @type_cache[type_name] = nil unless type_definition
|
|
186
213
|
|
|
187
|
-
|
|
214
|
+
scope = type_definition.scope || @shape.contract_class
|
|
215
|
+
|
|
216
|
+
@type_cache[type_name] = Object.new(scope).tap do |type_shape|
|
|
188
217
|
type_shape.copy_type_definition_params(type_definition, type_shape)
|
|
189
218
|
end
|
|
190
219
|
end
|
|
@@ -85,10 +85,14 @@ module Apiwork
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def deserialize_array(array, param_options)
|
|
88
|
+
of = param_options[:of]
|
|
89
|
+
|
|
88
90
|
array.map do |item|
|
|
89
91
|
next item unless item.is_a?(Hash)
|
|
90
92
|
|
|
91
|
-
if
|
|
93
|
+
if of&.type == :union && of.shape.is_a?(Apiwork::Union)
|
|
94
|
+
deserialize_union(item, of.shape)
|
|
95
|
+
elsif param_options[:shape]
|
|
92
96
|
Deserializer.deserialize(param_options[:shape], item)
|
|
93
97
|
else
|
|
94
98
|
item
|
|
@@ -32,11 +32,22 @@ module Apiwork
|
|
|
32
32
|
|
|
33
33
|
if param_options[:shape] && value.is_a?(Hash)
|
|
34
34
|
transformed[name] = Transformer.transform(param_options[:shape], value)
|
|
35
|
-
elsif param_options[:type] == :
|
|
35
|
+
elsif param_options[:type] == :record && value.is_a?(Hash)
|
|
36
36
|
of = param_options[:of]
|
|
37
37
|
of_shape = of&.shape
|
|
38
38
|
|
|
39
39
|
if of_shape
|
|
40
|
+
transformed[name] = value.transform_values do |item|
|
|
41
|
+
item.is_a?(Hash) ? Transformer.transform(of_shape, item) : item
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
elsif param_options[:type] == :array && value.is_a?(Array)
|
|
45
|
+
of = param_options[:of]
|
|
46
|
+
of_shape = of&.shape
|
|
47
|
+
|
|
48
|
+
if of&.type == :union
|
|
49
|
+
transformed[name] = value
|
|
50
|
+
elsif of_shape
|
|
40
51
|
transformed[name] = value.map do |item|
|
|
41
52
|
item.is_a?(Hash) ? Transformer.transform(of_shape, item) : item
|
|
42
53
|
end
|
|
@@ -85,7 +96,9 @@ module Apiwork
|
|
|
85
96
|
end
|
|
86
97
|
|
|
87
98
|
def build_type_shape(type_definition, contract_class)
|
|
88
|
-
|
|
99
|
+
scope = type_definition.scope || contract_class
|
|
100
|
+
|
|
101
|
+
type_shape = Object.new(scope, action_name: @shape.action_name)
|
|
89
102
|
type_shape.copy_type_definition_params(type_definition, type_shape)
|
|
90
103
|
type_shape
|
|
91
104
|
end
|
|
@@ -129,14 +129,7 @@ module Apiwork
|
|
|
129
129
|
return nil if param_options[:optional]
|
|
130
130
|
return nil if param_options[:nullable] && data.key?(name) && value.nil?
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
when :boolean, :string
|
|
134
|
-
value.nil?
|
|
135
|
-
else
|
|
136
|
-
value.blank?
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
return nil unless missing
|
|
132
|
+
return nil unless value.nil?
|
|
140
133
|
|
|
141
134
|
if param_options[:enum].present?
|
|
142
135
|
Issue.new(
|
|
@@ -208,6 +201,8 @@ module Apiwork
|
|
|
208
201
|
validate_shape_object(value, param_options[:shape], field_path, max_depth, current_depth)
|
|
209
202
|
elsif param_options[:type] == :array && value.is_a?(Array)
|
|
210
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)
|
|
211
206
|
else
|
|
212
207
|
[[], value]
|
|
213
208
|
end
|
|
@@ -237,6 +232,41 @@ module Apiwork
|
|
|
237
232
|
array_issues.empty? ? [[], array_values] : [array_issues, NOT_SET]
|
|
238
233
|
end
|
|
239
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
|
+
|
|
240
270
|
def check_unknown_params(data, path)
|
|
241
271
|
extra_keys = data.keys - @shape.params.keys
|
|
242
272
|
extra_keys.map do |key|
|
|
@@ -287,7 +317,10 @@ module Apiwork
|
|
|
287
317
|
array.each_with_index do |item, index|
|
|
288
318
|
item_path = field_path + [index]
|
|
289
319
|
|
|
290
|
-
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
|
|
291
324
|
validator = Validator.new(normalize_shape(of_shape))
|
|
292
325
|
shape_result = validator.validate(
|
|
293
326
|
item,
|
|
@@ -376,6 +409,7 @@ module Apiwork
|
|
|
376
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)
|
|
377
410
|
when :object then value.is_a?(Hash)
|
|
378
411
|
when :array then value.is_a?(Array)
|
|
412
|
+
when :record then value.is_a?(Hash)
|
|
379
413
|
when :decimal, :number then value.is_a?(Numeric)
|
|
380
414
|
else true
|
|
381
415
|
end
|
|
@@ -437,6 +471,8 @@ module Apiwork
|
|
|
437
471
|
most_specific_error = error
|
|
438
472
|
elsif error.code == :value_invalid && (most_specific_error.nil? || most_specific_error.code != :field_unknown)
|
|
439
473
|
most_specific_error = error
|
|
474
|
+
elsif error.path.length > path.length && most_specific_error.nil?
|
|
475
|
+
most_specific_error = error
|
|
440
476
|
end
|
|
441
477
|
end
|
|
442
478
|
|
|
@@ -448,7 +484,7 @@ module Apiwork
|
|
|
448
484
|
translate_detail(:type_invalid),
|
|
449
485
|
path:,
|
|
450
486
|
meta: {
|
|
451
|
-
actual: value.is_a?(Hash) ? :
|
|
487
|
+
actual: value.is_a?(Hash) ? :object : value.class.name.underscore.to_sym,
|
|
452
488
|
expected: expected_types.join(' | '),
|
|
453
489
|
field: name,
|
|
454
490
|
},
|
|
@@ -605,7 +641,9 @@ module Apiwork
|
|
|
605
641
|
end
|
|
606
642
|
|
|
607
643
|
def validate_with_type_definition(type_definition, value, path, current_depth:, exclude_param: nil, max_depth:)
|
|
608
|
-
|
|
644
|
+
scope = type_definition.scope || @shape.contract_class
|
|
645
|
+
|
|
646
|
+
type_shape = Object.new(scope, action_name: @shape.action_name)
|
|
609
647
|
type_shape.copy_type_definition_params(type_definition, type_shape)
|
|
610
648
|
type_shape.params.delete(exclude_param) if exclude_param
|
|
611
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.
|
|
@@ -72,10 +72,10 @@ module Apiwork
|
|
|
72
72
|
# The allowed values or enum reference.
|
|
73
73
|
# @param example [Object, nil] (nil)
|
|
74
74
|
# The example value. Metadata included in exports.
|
|
75
|
-
# @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :url, :uuid]
|
|
75
|
+
# @param format [Symbol, nil] (nil) [:date, :datetime, :double, :email, :float, :hostname, :int32, :int64, :ipv4, :ipv6, :password, :text, :url, :uuid]
|
|
76
76
|
# Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
|
|
77
77
|
# Valid formats by type: `:decimal`/`:number` (`:double`, `:float`), `:integer` (`:int32`, `:int64`),
|
|
78
|
-
# `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:url`, `:uuid`).
|
|
78
|
+
# `:string` (`:date`, `:datetime`, `:email`, `:hostname`, `:ipv4`, `:ipv6`, `:password`, `:text`, `:url`, `:uuid`).
|
|
79
79
|
# @param max [Integer, nil] (nil)
|
|
80
80
|
# The maximum. For `:array`: size. For `:decimal`, `:integer`, `:number`: value. For `:string`: length.
|
|
81
81
|
# @param min [Integer, nil] (nil)
|
|
@@ -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
|
@@ -82,7 +82,7 @@ module Apiwork
|
|
|
82
82
|
#
|
|
83
83
|
# @param enum [Array, Symbol, nil] (nil)
|
|
84
84
|
# The allowed values.
|
|
85
|
-
# @param format [Symbol, nil] (nil) [:date, :datetime, :email, :hostname, :ipv4, :ipv6, :password, :url, :uuid]
|
|
85
|
+
# @param format [Symbol, nil] (nil) [:date, :datetime, :email, :hostname, :ipv4, :ipv6, :password, :text, :url, :uuid]
|
|
86
86
|
# Format hint for exports. Does not change the type, but exports may add validation or documentation based on it.
|
|
87
87
|
# Valid formats by type: `:string`.
|
|
88
88
|
# @param max [Integer, nil] (nil)
|
|
@@ -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
|
|