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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02f6a539dd3708f867af6e298daec039dd03bc4ea38f22e21172940332ee800f
|
|
4
|
+
data.tar.gz: a5603ca5b09b0a8baa3195629b848ceb186e3b1e1d40b987539f02195a76d76f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 187ae2ea46e4df89ccca25bc2b88fd5a81b31f1887def0f7acfdb3dbc1fad3284f4b6f91c13956631aa20d6dd6793ac4ff1015ce36c6150c0b29dc669ca2aa18
|
|
7
|
+
data.tar.gz: 844cb5df21fe9803dabd1e450ce6a9c2f1d7f745fcb40d1d92dbfc2d95660d9ee1be719856d1b922ca5a0eeb11f8e711ce8be1eb423f20d90d348f7c55335a32
|
|
@@ -58,6 +58,8 @@ module Apiwork
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def contract_types(contract_class)
|
|
61
|
+
register_representation_types(contract_class)
|
|
62
|
+
|
|
61
63
|
builder_class = self.class.contract_builder
|
|
62
64
|
return unless builder_class
|
|
63
65
|
|
|
@@ -77,6 +79,19 @@ module Apiwork
|
|
|
77
79
|
def serialize(resource, context:, serialize_options:)
|
|
78
80
|
raise NotImplementedError
|
|
79
81
|
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def register_representation_types(contract_class)
|
|
86
|
+
representation_class.type_definitions.each do |name, type_definition|
|
|
87
|
+
case type_definition[:kind]
|
|
88
|
+
when :object
|
|
89
|
+
contract_class.object(name, **type_definition[:options], &type_definition[:block])
|
|
90
|
+
when :union
|
|
91
|
+
contract_class.union(name, **type_definition[:options], &type_definition[:block])
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
80
95
|
end
|
|
81
96
|
end
|
|
82
97
|
end
|
|
@@ -72,7 +72,7 @@ module Apiwork
|
|
|
72
72
|
|
|
73
73
|
element = attribute.element
|
|
74
74
|
if element
|
|
75
|
-
if element.type
|
|
75
|
+
if [:array, :record].include?(element.type)
|
|
76
76
|
param_options[:of] = element.inner
|
|
77
77
|
else
|
|
78
78
|
param_options[:shape] = element.shape
|
|
@@ -95,7 +95,8 @@ module Apiwork
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
if association.singular?
|
|
98
|
-
|
|
98
|
+
resolved_type = association_type || :object
|
|
99
|
+
object.param(name, type: resolved_type, custom_type: association_type, **base_options)
|
|
99
100
|
elsif association.collection?
|
|
100
101
|
if association_type
|
|
101
102
|
object.param(name, type: :array, **base_options) do |param|
|
|
@@ -90,7 +90,7 @@ module Apiwork
|
|
|
90
90
|
writable = action_name != :delete
|
|
91
91
|
|
|
92
92
|
object(type_name) do |object|
|
|
93
|
-
object.literal(Constants::OP,
|
|
93
|
+
object.literal(Constants::OP, value: action_name.to_s)
|
|
94
94
|
object.param(:id, optional: action_name != :delete, type: primary_key_type) unless action_name == :create
|
|
95
95
|
|
|
96
96
|
next unless writable
|
|
@@ -176,7 +176,7 @@ module Apiwork
|
|
|
176
176
|
if attribute.element
|
|
177
177
|
element = attribute.element
|
|
178
178
|
|
|
179
|
-
if element.type
|
|
179
|
+
if [:array, :record].include?(element.type)
|
|
180
180
|
options[:of] = element.inner
|
|
181
181
|
else
|
|
182
182
|
options[:shape] = element.shape
|
data/lib/apiwork/api/base.rb
CHANGED
|
@@ -27,6 +27,7 @@ module Apiwork
|
|
|
27
27
|
# @return [String]
|
|
28
28
|
attr_reader :base_path,
|
|
29
29
|
:enum_registry,
|
|
30
|
+
:explorer_config,
|
|
30
31
|
:export_configs,
|
|
31
32
|
:representation_registry,
|
|
32
33
|
:root_resource,
|
|
@@ -133,6 +134,44 @@ module Apiwork
|
|
|
133
134
|
block.arity.positive? ? yield(@export_configs[name]) : @export_configs[name].instance_eval(&block)
|
|
134
135
|
end
|
|
135
136
|
|
|
137
|
+
# @api public
|
|
138
|
+
# Configures the explorer for this API.
|
|
139
|
+
#
|
|
140
|
+
# The explorer is an interactive UI for browsing and testing API endpoints.
|
|
141
|
+
# Requires the `apiwork-explorer` gem.
|
|
142
|
+
#
|
|
143
|
+
# @yield Block evaluated in explorer context.
|
|
144
|
+
# @yieldparam explorer [Configuration]
|
|
145
|
+
# @return [void]
|
|
146
|
+
#
|
|
147
|
+
# @example Enable with defaults
|
|
148
|
+
# explorer
|
|
149
|
+
#
|
|
150
|
+
# @example Custom configuration
|
|
151
|
+
# explorer do
|
|
152
|
+
# mode :always
|
|
153
|
+
# path '/explorer'
|
|
154
|
+
# end
|
|
155
|
+
def explorer(&block)
|
|
156
|
+
unless defined?(Apiwork::Explorer::Engine)
|
|
157
|
+
raise ConfigurationError,
|
|
158
|
+
'explorer requires the apiwork-explorer gem. ' \
|
|
159
|
+
"Add it to your Gemfile: gem 'apiwork-explorer'"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
unless @explorer_config
|
|
163
|
+
options = Configurable.define do
|
|
164
|
+
option :mode, default: :auto, enum: %i[auto always never], type: :symbol
|
|
165
|
+
option :path, default: '/.explorer', type: :string
|
|
166
|
+
end
|
|
167
|
+
@explorer_config = Configuration.new(options)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
return @explorer_config unless block
|
|
171
|
+
|
|
172
|
+
block.arity.positive? ? yield(@explorer_config) : @explorer_config.instance_eval(&block)
|
|
173
|
+
end
|
|
174
|
+
|
|
136
175
|
# @api public
|
|
137
176
|
# Sets or configures the adapter for this API.
|
|
138
177
|
#
|
|
@@ -644,6 +683,7 @@ module Apiwork
|
|
|
644
683
|
@info = nil
|
|
645
684
|
@locales = []
|
|
646
685
|
@raises = []
|
|
686
|
+
@explorer_config = nil
|
|
647
687
|
@export_configs = {}
|
|
648
688
|
@adapter_config = nil
|
|
649
689
|
@root_resource = Resource.new(self)
|
|
@@ -682,16 +722,33 @@ module Apiwork
|
|
|
682
722
|
end.join('/')
|
|
683
723
|
end
|
|
684
724
|
|
|
725
|
+
def transform_key(key)
|
|
726
|
+
key_string = key.to_s
|
|
727
|
+
|
|
728
|
+
return key_string if key_string.match?(/\A[A-Z]+\z/)
|
|
729
|
+
|
|
730
|
+
case key_format
|
|
731
|
+
when :camel then key_string.camelize(:lower)
|
|
732
|
+
when :pascal then key_string.camelize
|
|
733
|
+
when :kebab then key_string.dasherize
|
|
734
|
+
when :underscore then key_string.underscore
|
|
735
|
+
else key_string
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def normalize_key(key)
|
|
740
|
+
key_string = key.to_s
|
|
741
|
+
|
|
742
|
+
return key_string if key_string.match?(/\A[A-Z]+\z/)
|
|
743
|
+
|
|
744
|
+
key_string.underscore
|
|
745
|
+
end
|
|
746
|
+
|
|
685
747
|
def normalize_request(request)
|
|
686
|
-
return request if
|
|
748
|
+
return request if key_format == :keep
|
|
687
749
|
|
|
688
750
|
request.transform do |hash|
|
|
689
|
-
hash.deep_transform_keys
|
|
690
|
-
key_string = key.to_s
|
|
691
|
-
next key if key_string.match?(/\A[A-Z]+\z/)
|
|
692
|
-
|
|
693
|
-
key_string.underscore.to_sym
|
|
694
|
-
end
|
|
751
|
+
hash.deep_transform_keys { |key| normalize_key(key).to_sym }
|
|
695
752
|
end
|
|
696
753
|
end
|
|
697
754
|
|
|
@@ -701,16 +758,9 @@ module Apiwork
|
|
|
701
758
|
|
|
702
759
|
def prepare_response(response)
|
|
703
760
|
result = adapter.apply_response_transformers(response)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
when :pascal
|
|
708
|
-
result.transform { |hash| hash.deep_transform_keys { |key| key.to_s.camelize.to_sym } }
|
|
709
|
-
when :kebab
|
|
710
|
-
result.transform { |hash| hash.deep_transform_keys { |key| key.to_s.dasherize.to_sym } }
|
|
711
|
-
else
|
|
712
|
-
result
|
|
713
|
-
end
|
|
761
|
+
return result if key_format == :keep
|
|
762
|
+
|
|
763
|
+
result.transform { |hash| hash.deep_transform_keys { |key| transform_key(key).to_sym } }
|
|
714
764
|
end
|
|
715
765
|
|
|
716
766
|
def type?(name, scope: nil)
|
data/lib/apiwork/api/element.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Apiwork
|
|
|
32
32
|
# This is the verbose form. Prefer sugar methods (string, integer, etc.)
|
|
33
33
|
# for static definitions. Use `of` for dynamic element generation.
|
|
34
34
|
#
|
|
35
|
-
# @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
|
|
35
|
+
# @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :unknown, :uuid]
|
|
36
36
|
# The element type. Custom type references are also allowed.
|
|
37
37
|
# @param discriminator [Symbol, nil] (nil)
|
|
38
38
|
# The discriminator field name. Unions only.
|
|
@@ -64,9 +64,25 @@ module Apiwork
|
|
|
64
64
|
# string :name
|
|
65
65
|
# end
|
|
66
66
|
# end
|
|
67
|
+
#
|
|
68
|
+
# @example Discriminated union
|
|
69
|
+
# array :payments do
|
|
70
|
+
# of :union, discriminator: :type do
|
|
71
|
+
# variant tag: 'card' do
|
|
72
|
+
# object do
|
|
73
|
+
# string :last_four
|
|
74
|
+
# end
|
|
75
|
+
# end
|
|
76
|
+
# variant tag: 'bank' do
|
|
77
|
+
# object do
|
|
78
|
+
# string :account_number
|
|
79
|
+
# end
|
|
80
|
+
# end
|
|
81
|
+
# end
|
|
82
|
+
# end
|
|
67
83
|
def of(type, discriminator: nil, enum: nil, format: nil, max: nil, min: nil, value: nil, &block)
|
|
68
84
|
case type
|
|
69
|
-
when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary
|
|
85
|
+
when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary, :unknown
|
|
70
86
|
set_type(type, enum:, format:, max:, min:)
|
|
71
87
|
when :literal
|
|
72
88
|
@type = :literal
|
|
@@ -90,6 +106,15 @@ module Apiwork
|
|
|
90
106
|
@inner = inner
|
|
91
107
|
@shape = inner.shape
|
|
92
108
|
@defined = true
|
|
109
|
+
when :record
|
|
110
|
+
raise ConfigurationError, 'record requires a block' unless block
|
|
111
|
+
|
|
112
|
+
inner = Element.new
|
|
113
|
+
block.arity.positive? ? yield(inner) : inner.instance_eval(&block)
|
|
114
|
+
inner.validate!
|
|
115
|
+
@type = :record
|
|
116
|
+
@inner = inner
|
|
117
|
+
@defined = true
|
|
93
118
|
when :union
|
|
94
119
|
raise ConfigurationError, 'union requires a block' unless block
|
|
95
120
|
|
|
@@ -105,6 +130,12 @@ module Apiwork
|
|
|
105
130
|
@defined = true
|
|
106
131
|
end
|
|
107
132
|
end
|
|
133
|
+
|
|
134
|
+
def reference(type_name)
|
|
135
|
+
@type = type_name
|
|
136
|
+
@custom_type = type_name
|
|
137
|
+
@defined = true
|
|
138
|
+
end
|
|
108
139
|
end
|
|
109
140
|
end
|
|
110
141
|
end
|
data/lib/apiwork/api/object.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Apiwork
|
|
|
7
7
|
#
|
|
8
8
|
# Accessed via `object :name do` in API or contract definitions.
|
|
9
9
|
# Use type methods to define params: {#string}, {#integer}, {#decimal},
|
|
10
|
-
# {#boolean}, {#array}, {#object}, {#union}, {#reference}.
|
|
10
|
+
# {#boolean}, {#array}, {#record}, {#object}, {#union}, {#reference}.
|
|
11
11
|
#
|
|
12
12
|
# @see API::Element Block context for array/variant elements
|
|
13
13
|
# @see Contract::Object Block context for inline objects
|
|
@@ -32,7 +32,7 @@ module Apiwork
|
|
|
32
32
|
#
|
|
33
33
|
# @param name [Symbol]
|
|
34
34
|
# The param name.
|
|
35
|
-
# @param type [Symbol, nil] (nil) [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
|
|
35
|
+
# @param type [Symbol, nil] (nil) [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :record, :string, :time, :union, :unknown, :uuid]
|
|
36
36
|
# The param type.
|
|
37
37
|
# @param as [Symbol, nil] (nil)
|
|
38
38
|
# The target attribute name.
|
|
@@ -59,7 +59,7 @@ module Apiwork
|
|
|
59
59
|
# @param nullable [Boolean] (false)
|
|
60
60
|
# Whether the value can be `null`.
|
|
61
61
|
# @param of [Symbol, Hash, nil] (nil)
|
|
62
|
-
# The element type. Arrays only.
|
|
62
|
+
# The element or value type. Arrays and records only.
|
|
63
63
|
# @param optional [Boolean] (false)
|
|
64
64
|
# Whether the param is optional.
|
|
65
65
|
# @param required [Boolean] (false)
|
|
@@ -85,6 +85,7 @@ module Apiwork
|
|
|
85
85
|
name,
|
|
86
86
|
type: nil,
|
|
87
87
|
as: nil,
|
|
88
|
+
custom_type: nil,
|
|
88
89
|
default: nil,
|
|
89
90
|
deprecated: false,
|
|
90
91
|
description: nil,
|
|
@@ -103,11 +104,12 @@ module Apiwork
|
|
|
103
104
|
&block
|
|
104
105
|
)
|
|
105
106
|
resolved_of = resolve_of(of, type, &block)
|
|
106
|
-
resolved_shape =
|
|
107
|
+
resolved_shape = [:array, :record].include?(type) ? nil : (shape || build_shape(type, discriminator, &block))
|
|
107
108
|
discriminator = resolved_of&.discriminator if type == :array
|
|
108
109
|
|
|
109
110
|
param_hash = {
|
|
110
111
|
as:,
|
|
112
|
+
custom_type:,
|
|
111
113
|
default:,
|
|
112
114
|
deprecated:,
|
|
113
115
|
description:,
|
|
@@ -193,10 +195,73 @@ module Apiwork
|
|
|
193
195
|
)
|
|
194
196
|
end
|
|
195
197
|
|
|
198
|
+
# @api public
|
|
199
|
+
# Defines a record param with value type.
|
|
200
|
+
#
|
|
201
|
+
# @param name [Symbol]
|
|
202
|
+
# The param name.
|
|
203
|
+
# @param as [Symbol, nil] (nil)
|
|
204
|
+
# The target attribute name.
|
|
205
|
+
# @param default [Object, nil] (nil)
|
|
206
|
+
# The default value.
|
|
207
|
+
# @param deprecated [Boolean] (false)
|
|
208
|
+
# Whether deprecated. Metadata included in exports.
|
|
209
|
+
# @param description [String, nil] (nil)
|
|
210
|
+
# The description. Metadata included in exports.
|
|
211
|
+
# @param nullable [Boolean] (false)
|
|
212
|
+
# Whether the value can be `null`.
|
|
213
|
+
# @param optional [Boolean] (false)
|
|
214
|
+
# Whether the param is optional.
|
|
215
|
+
# @param required [Boolean] (false)
|
|
216
|
+
# Whether the param is required.
|
|
217
|
+
# @yield block for defining value type
|
|
218
|
+
# @yieldparam element [API::Element]
|
|
219
|
+
# @return [void]
|
|
220
|
+
#
|
|
221
|
+
# @example instance_eval style
|
|
222
|
+
# record :scores do
|
|
223
|
+
# integer
|
|
224
|
+
# end
|
|
225
|
+
#
|
|
226
|
+
# @example yield style
|
|
227
|
+
# record :scores do |element|
|
|
228
|
+
# element.integer
|
|
229
|
+
# end
|
|
230
|
+
def record(
|
|
231
|
+
name,
|
|
232
|
+
as: nil,
|
|
233
|
+
default: nil,
|
|
234
|
+
deprecated: false,
|
|
235
|
+
description: nil,
|
|
236
|
+
nullable: false,
|
|
237
|
+
optional: false,
|
|
238
|
+
required: false,
|
|
239
|
+
&block
|
|
240
|
+
)
|
|
241
|
+
raise ArgumentError, 'record requires a block' unless block
|
|
242
|
+
|
|
243
|
+
element = Element.new
|
|
244
|
+
block.arity.positive? ? yield(element) : element.instance_eval(&block)
|
|
245
|
+
element.validate!
|
|
246
|
+
|
|
247
|
+
param(
|
|
248
|
+
name,
|
|
249
|
+
as:,
|
|
250
|
+
default:,
|
|
251
|
+
deprecated:,
|
|
252
|
+
description:,
|
|
253
|
+
nullable:,
|
|
254
|
+
optional:,
|
|
255
|
+
required:,
|
|
256
|
+
of: element,
|
|
257
|
+
type: :record,
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
196
261
|
private
|
|
197
262
|
|
|
198
263
|
def resolve_of(of, type, &block)
|
|
199
|
-
return nil unless
|
|
264
|
+
return nil unless [:array, :record].include?(type)
|
|
200
265
|
|
|
201
266
|
if block
|
|
202
267
|
element = Element.new
|
data/lib/apiwork/api/router.rb
CHANGED
|
@@ -35,6 +35,22 @@ module Apiwork
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
if api_class.explorer_config && defined?(Apiwork::Explorer::Engine)
|
|
39
|
+
mount_explorer = case api_class.explorer_config.mode
|
|
40
|
+
when :always then true
|
|
41
|
+
when :never then false
|
|
42
|
+
when :auto then Rails.env.development?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if mount_explorer
|
|
46
|
+
scope path: api_class.transform_path(api_class.base_path) do
|
|
47
|
+
mount Apiwork::Explorer::Engine,
|
|
48
|
+
at: api_class.explorer_config.path,
|
|
49
|
+
defaults: { api_base_path: api_class.base_path }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
38
54
|
scope module: api_class.namespaces.map(&:to_s).join('/').underscore,
|
|
39
55
|
path: api_class.transform_path(api_class.base_path) do
|
|
40
56
|
router.draw_resources(self, api_class.root_resource.resources, api_class)
|
|
@@ -37,7 +37,7 @@ module Apiwork
|
|
|
37
37
|
# This is the verbose form. Prefer sugar methods (string, integer, etc.)
|
|
38
38
|
# for static definitions. Use `of` for dynamic element generation.
|
|
39
39
|
#
|
|
40
|
-
# @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :uuid]
|
|
40
|
+
# @param type [Symbol] [:array, :binary, :boolean, :date, :datetime, :decimal, :integer, :literal, :number, :object, :string, :time, :union, :unknown, :uuid]
|
|
41
41
|
# The element type. Custom type references are also allowed.
|
|
42
42
|
# @param discriminator [Symbol, nil] (nil)
|
|
43
43
|
# The discriminator field name. Unions only.
|
|
@@ -75,7 +75,7 @@ module Apiwork
|
|
|
75
75
|
resolved_enum = enum.is_a?(Symbol) ? resolve_enum(enum) : enum
|
|
76
76
|
|
|
77
77
|
case type
|
|
78
|
-
when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary
|
|
78
|
+
when :string, :integer, :decimal, :boolean, :number, :datetime, :date, :uuid, :time, :binary, :unknown
|
|
79
79
|
set_type(type, format:, max:, min:, enum: resolved_enum)
|
|
80
80
|
when :literal
|
|
81
81
|
@type = :literal
|
|
@@ -99,6 +99,15 @@ module Apiwork
|
|
|
99
99
|
@inner = inner
|
|
100
100
|
@shape = inner.shape
|
|
101
101
|
@defined = true
|
|
102
|
+
when :record
|
|
103
|
+
raise ConfigurationError, 'record requires a block' unless block
|
|
104
|
+
|
|
105
|
+
inner = Element.new(@contract_class)
|
|
106
|
+
block.arity.positive? ? yield(inner) : inner.instance_eval(&block)
|
|
107
|
+
inner.validate!
|
|
108
|
+
@type = :record
|
|
109
|
+
@inner = inner
|
|
110
|
+
@defined = true
|
|
102
111
|
when :union
|
|
103
112
|
raise ConfigurationError, 'union requires a block' unless block
|
|
104
113
|
|
|
@@ -115,6 +124,12 @@ module Apiwork
|
|
|
115
124
|
end
|
|
116
125
|
end
|
|
117
126
|
|
|
127
|
+
def reference(type_name)
|
|
128
|
+
@type = type_name
|
|
129
|
+
@custom_type = type_name
|
|
130
|
+
@defined = true
|
|
131
|
+
end
|
|
132
|
+
|
|
118
133
|
private
|
|
119
134
|
|
|
120
135
|
def resolve_enum(enum)
|
|
@@ -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)
|
|
@@ -191,7 +211,9 @@ module Apiwork
|
|
|
191
211
|
type_definition = @shape.contract_class.resolve_custom_type(type_name)
|
|
192
212
|
return @type_cache[type_name] = nil unless type_definition
|
|
193
213
|
|
|
194
|
-
|
|
214
|
+
scope = type_definition.scope || @shape.contract_class
|
|
215
|
+
|
|
216
|
+
@type_cache[type_name] = Object.new(scope).tap do |type_shape|
|
|
195
217
|
type_shape.copy_type_definition_params(type_definition, type_shape)
|
|
196
218
|
end
|
|
197
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
|
|
@@ -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
|
|