dry-schema 1.8.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +3 -3
- data/dry-schema.gemspec +1 -1
- data/lib/dry/schema/compiler.rb +1 -1
- data/lib/dry/schema/dsl.rb +7 -4
- data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
- data/lib/dry/schema/extensions/hints.rb +11 -9
- data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
- data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +232 -0
- data/lib/dry/schema/extensions/json_schema.rb +29 -0
- data/lib/dry/schema/extensions/struct.rb +1 -1
- data/lib/dry/schema/extensions.rb +4 -0
- data/lib/dry/schema/key.rb +75 -74
- data/lib/dry/schema/key_coercer.rb +2 -2
- data/lib/dry/schema/key_validator.rb +44 -23
- data/lib/dry/schema/macros/array.rb +4 -0
- data/lib/dry/schema/macros/core.rb +1 -1
- data/lib/dry/schema/macros/dsl.rb +17 -15
- data/lib/dry/schema/macros/hash.rb +1 -1
- data/lib/dry/schema/macros/key.rb +2 -2
- data/lib/dry/schema/macros/schema.rb +2 -0
- data/lib/dry/schema/macros/value.rb +7 -0
- data/lib/dry/schema/message/or/multi_path.rb +7 -5
- data/lib/dry/schema/message_compiler.rb +13 -10
- data/lib/dry/schema/messages/i18n.rb +98 -96
- data/lib/dry/schema/messages/namespaced.rb +1 -0
- data/lib/dry/schema/messages/yaml.rb +165 -158
- data/lib/dry/schema/predicate.rb +2 -2
- data/lib/dry/schema/predicate_inferrer.rb +2 -0
- data/lib/dry/schema/primitive_inferrer.rb +2 -0
- data/lib/dry/schema/processor.rb +1 -1
- data/lib/dry/schema/result.rb +5 -7
- data/lib/dry/schema/trace.rb +5 -1
- data/lib/dry/schema/type_registry.rb +1 -2
- data/lib/dry/schema/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 630c8c7019a02d6f22386ef4ac1ca68321af93bd3a5e34f8b62b21d28a7eee69
|
4
|
+
data.tar.gz: e53baa861af349395446deb0183e7358438e4ca4ea7fa78b25d4c4f04e4ea2a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ed11d1eba31d808c75018d5ea78fc649f4f59486c2047554aeabac5cd71e9b2d6e64b88661c8f0615b9a9402c177d2c51f49cd49e3ae6e697c45f9f952dab18
|
7
|
+
data.tar.gz: 5c085c4994f662a44eb372e92a894c76d83eefdce88389ed6c1947004c7bd39204b843ea5eb408af86ad986e6978dde67227b9bdcf7d888d1a8e277b457c4345
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
<!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
|
2
2
|
|
3
|
+
## 1.9.0 2022-02-15
|
4
|
+
|
5
|
+
|
6
|
+
### Added
|
7
|
+
|
8
|
+
- [EXPERIMENTAL] `json_schema` extension which allows you to convert a schema into a JSON schema (via #369) (@ianks)
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
|
12
|
+
- Composing schemas no longer crashes in certain scenarios (issue #342 fixed via #366) (@vsuhachev)
|
13
|
+
- Fix info extension for typed arrays (issue #394 fixed via #397) (@CandyFet)
|
14
|
+
|
15
|
+
|
16
|
+
[Compare v1.8.0...v1.9.0](https://github.com/dry-rb/dry-schema/compare/v1.8.0...v1.9.0)
|
17
|
+
|
3
18
|
## 1.8.0 2021-09-12
|
4
19
|
|
5
20
|
|
data/README.md
CHANGED
@@ -8,7 +8,7 @@
|
|
8
8
|
# dry-schema [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
|
9
9
|
|
10
10
|
[![Gem Version](https://badge.fury.io/rb/dry-schema.svg)][gem]
|
11
|
-
[![CI Status](https://github.com/dry-rb/dry-schema/workflows/
|
11
|
+
[![CI Status](https://github.com/dry-rb/dry-schema/workflows/ci/badge.svg)][actions]
|
12
12
|
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/961f5c776f1d49218b2cede3745e059c)][codacy]
|
13
13
|
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/961f5c776f1d49218b2cede3745e059c)][codacy]
|
14
14
|
[![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=master)][inchpages]
|
@@ -22,8 +22,8 @@
|
|
22
22
|
|
23
23
|
This library officially supports the following Ruby versions:
|
24
24
|
|
25
|
-
* MRI `>= 2.
|
26
|
-
*
|
25
|
+
* MRI `>= 2.7.0`
|
26
|
+
* jruby `>= 9.3` (postponed until 2.7 is supported)
|
27
27
|
|
28
28
|
## License
|
29
29
|
|
data/dry-schema.gemspec
CHANGED
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-schema"
|
32
32
|
spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-schema/issues"
|
33
33
|
|
34
|
-
spec.required_ruby_version = ">= 2.
|
34
|
+
spec.required_ruby_version = ">= 2.7.0"
|
35
35
|
|
36
36
|
# to update dependencies edit project.yml
|
37
37
|
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
|
data/lib/dry/schema/compiler.rb
CHANGED
data/lib/dry/schema/dsl.rb
CHANGED
@@ -80,8 +80,10 @@ module Dry
|
|
80
80
|
# Build a new DSL object and evaluate provided block
|
81
81
|
#
|
82
82
|
# @param [Hash] options
|
83
|
-
# @option options [Class] :processor The processor type
|
84
|
-
#
|
83
|
+
# @option options [Class] :processor The processor type
|
84
|
+
# (`Params`, `JSON` or a custom sub-class)
|
85
|
+
# @option options [Compiler] :compiler An instance of a rule compiler
|
86
|
+
# (must be compatible with `Schema::Compiler`) (optional)
|
85
87
|
# @option options [Array[DSL]] :parent One or more instances of the parent DSL (optional)
|
86
88
|
# @option options [Config] :config A configuration object (optional)
|
87
89
|
#
|
@@ -168,7 +170,8 @@ module Dry
|
|
168
170
|
# A generic method for defining keys
|
169
171
|
#
|
170
172
|
# @param [Symbol] name The key name
|
171
|
-
# @param [Class] macro The macro sub-class (ie `Macros::Required` or
|
173
|
+
# @param [Class] macro The macro sub-class (ie `Macros::Required` or
|
174
|
+
# any other `Macros::Key` subclass)
|
172
175
|
#
|
173
176
|
# @return [Macros::Key]
|
174
177
|
#
|
@@ -383,7 +386,7 @@ module Dry
|
|
383
386
|
#
|
384
387
|
# @api protected
|
385
388
|
def rules
|
386
|
-
parent_rules.merge(macros.
|
389
|
+
parent_rules.merge(macros.to_h { [_1.name, _1.to_rule] }.compact)
|
387
390
|
end
|
388
391
|
|
389
392
|
# Build a key map from defined types
|
@@ -35,12 +35,14 @@ module Dry
|
|
35
35
|
end
|
36
36
|
|
37
37
|
# @api private
|
38
|
+
# rubocop: disable Metrics/AbcSize
|
39
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
40
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
38
41
|
def exclude?(messages, opts)
|
39
42
|
Array(messages).all? do |msg|
|
40
|
-
hints = opts
|
41
|
-
.
|
42
|
-
|
43
|
-
.reject { |hint| hint.predicate == :filled? }
|
43
|
+
hints = opts.hints.reject { |h|
|
44
|
+
msg.eql?(h) || h.predicate.eql?(:filled?)
|
45
|
+
}
|
44
46
|
|
45
47
|
key_failure = opts.key_failure?(msg.path)
|
46
48
|
predicate = msg.predicate
|
@@ -52,6 +54,9 @@ module Dry
|
|
52
54
|
HINT_OTHER_EXCLUSION.include?(predicate)
|
53
55
|
end
|
54
56
|
end
|
57
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
58
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
59
|
+
# rubocop: enable Metrics/AbcSize
|
55
60
|
|
56
61
|
# @api private
|
57
62
|
def message_type(options)
|
@@ -22,17 +22,19 @@ module Dry
|
|
22
22
|
# @see Message::Or
|
23
23
|
#
|
24
24
|
# @api public
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
module Or
|
26
|
+
class SinglePath
|
27
|
+
# @api private
|
28
|
+
def hint?
|
29
|
+
false
|
30
|
+
end
|
29
31
|
end
|
30
|
-
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
class MultiPath
|
34
|
+
# @api private
|
35
|
+
def hint?
|
36
|
+
false
|
37
|
+
end
|
36
38
|
end
|
37
39
|
end
|
38
40
|
|
@@ -96,7 +96,16 @@ module Dry
|
|
96
96
|
keys[rest[0][1]] = {required: opts.fetch(:required, true)}
|
97
97
|
else
|
98
98
|
type = PREDICATE_TO_TYPE[name]
|
99
|
-
|
99
|
+
assign_type(key, type) if type
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @api private
|
104
|
+
def assign_type(key, type)
|
105
|
+
if keys[key][:type]
|
106
|
+
keys[key][:member] = type
|
107
|
+
else
|
108
|
+
keys[key][:type] = type
|
100
109
|
end
|
101
110
|
end
|
102
111
|
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/schema/constants"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
# @api private
|
8
|
+
module JSONSchema
|
9
|
+
# @api private
|
10
|
+
class SchemaCompiler
|
11
|
+
# An error raised when a predicate cannot be converted
|
12
|
+
UnknownConversionError = Class.new(StandardError)
|
13
|
+
|
14
|
+
IDENTITY = ->(v, _) { v }.freeze
|
15
|
+
TO_INTEGER = ->(v, _) { v.to_i }.freeze
|
16
|
+
|
17
|
+
PREDICATE_TO_TYPE = {
|
18
|
+
array?: {type: "array"},
|
19
|
+
bool?: {type: "boolean"},
|
20
|
+
date?: {type: "string", format: "date"},
|
21
|
+
date_time?: {type: "string", format: "date-time"},
|
22
|
+
decimal?: {type: "number"},
|
23
|
+
float?: {type: "number"},
|
24
|
+
hash?: {type: "object"},
|
25
|
+
int?: {type: "integer"},
|
26
|
+
nil?: {type: "null"},
|
27
|
+
str?: {type: "string"},
|
28
|
+
time?: {type: "string", format: "time"},
|
29
|
+
min_size?: {minLength: TO_INTEGER},
|
30
|
+
max_size?: {maxLength: TO_INTEGER},
|
31
|
+
included_in?: {enum: ->(v, _) { v.to_a }},
|
32
|
+
filled?: EMPTY_HASH,
|
33
|
+
uri?: {format: "uri"},
|
34
|
+
uuid_v1?: {
|
35
|
+
pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-1[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
|
36
|
+
},
|
37
|
+
uuid_v2?: {
|
38
|
+
pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-2[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
|
39
|
+
},
|
40
|
+
uuid_v3?: {
|
41
|
+
pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
|
42
|
+
},
|
43
|
+
uuid_v4?: {
|
44
|
+
pattern: "^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}$"
|
45
|
+
},
|
46
|
+
uuid_v5?: {
|
47
|
+
pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
|
48
|
+
},
|
49
|
+
gt?: {exclusiveMinimum: IDENTITY},
|
50
|
+
gteq?: {mininum: IDENTITY},
|
51
|
+
lt?: {exclusiveMaximum: IDENTITY},
|
52
|
+
lteq?: {maximum: IDENTITY},
|
53
|
+
odd?: {type: "integer", not: {multipleOf: 2}},
|
54
|
+
even?: {type: "integer", multipleOf: 2}
|
55
|
+
}.freeze
|
56
|
+
|
57
|
+
# @api private
|
58
|
+
attr_reader :keys, :required
|
59
|
+
|
60
|
+
# @api private
|
61
|
+
def initialize(root: false, loose: false)
|
62
|
+
@keys = EMPTY_HASH.dup
|
63
|
+
@required = Set.new
|
64
|
+
@root = root
|
65
|
+
@loose = loose
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
def to_hash
|
70
|
+
result = {}
|
71
|
+
result[:$schema] = "http://json-schema.org/draft-06/schema#" if root?
|
72
|
+
result.merge!(type: "object", properties: keys, required: required.to_a)
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
alias_method :to_h, :to_hash
|
77
|
+
|
78
|
+
# @api private
|
79
|
+
def call(ast)
|
80
|
+
visit(ast)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @api private
|
84
|
+
def visit(node, opts = EMPTY_HASH)
|
85
|
+
meth, rest = node
|
86
|
+
public_send(:"visit_#{meth}", rest, opts)
|
87
|
+
end
|
88
|
+
|
89
|
+
# @api private
|
90
|
+
def visit_set(node, opts = EMPTY_HASH)
|
91
|
+
target = (key = opts[:key]) ? self.class.new : self
|
92
|
+
|
93
|
+
node.map { |child| target.visit(child, opts) }
|
94
|
+
|
95
|
+
return unless key
|
96
|
+
|
97
|
+
target_info = opts[:member] ? {items: target.to_h} : target.to_h
|
98
|
+
type = opts[:member] ? "array" : "object"
|
99
|
+
|
100
|
+
keys.update(key => {**keys[key], type: type, **target_info})
|
101
|
+
end
|
102
|
+
|
103
|
+
# @api private
|
104
|
+
def visit_and(node, opts = EMPTY_HASH)
|
105
|
+
left, right = node
|
106
|
+
|
107
|
+
# We need to know the type first to apply filled macro
|
108
|
+
if left[1][0] == :filled?
|
109
|
+
visit(right, opts)
|
110
|
+
visit(left, opts)
|
111
|
+
else
|
112
|
+
visit(left, opts)
|
113
|
+
visit(right, opts)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# @api private
|
118
|
+
def visit_implication(node, opts = EMPTY_HASH)
|
119
|
+
node.each do |el|
|
120
|
+
visit(el, **opts, required: false)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# @api private
|
125
|
+
def visit_each(node, opts = EMPTY_HASH)
|
126
|
+
visit(node, opts.merge(member: true))
|
127
|
+
end
|
128
|
+
|
129
|
+
# @api private
|
130
|
+
def visit_key(node, opts = EMPTY_HASH)
|
131
|
+
name, rest = node
|
132
|
+
|
133
|
+
if opts.fetch(:required, :true)
|
134
|
+
required << name.to_s
|
135
|
+
else
|
136
|
+
opts.delete(:required)
|
137
|
+
end
|
138
|
+
|
139
|
+
visit(rest, opts.merge(key: name))
|
140
|
+
end
|
141
|
+
|
142
|
+
# @api private
|
143
|
+
def visit_not(node, opts = EMPTY_HASH)
|
144
|
+
_name, rest = node
|
145
|
+
|
146
|
+
visit_predicate(rest, opts)
|
147
|
+
end
|
148
|
+
|
149
|
+
# @api private
|
150
|
+
def visit_predicate(node, opts = EMPTY_HASH)
|
151
|
+
name, rest = node
|
152
|
+
|
153
|
+
if name.equal?(:key?)
|
154
|
+
prop_name = rest[0][1]
|
155
|
+
keys[prop_name] = {}
|
156
|
+
else
|
157
|
+
target = keys[opts[:key]]
|
158
|
+
type_opts = fetch_type_opts_for_predicate(name, rest, target)
|
159
|
+
|
160
|
+
if target[:type]&.include?("array")
|
161
|
+
target[:items] ||= {}
|
162
|
+
merge_opts!(target[:items], type_opts)
|
163
|
+
else
|
164
|
+
merge_opts!(target, type_opts)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @api private
|
170
|
+
def fetch_type_opts_for_predicate(name, rest, target)
|
171
|
+
type_opts = PREDICATE_TO_TYPE.fetch(name) do
|
172
|
+
raise_unknown_conversion_error!(:predicate, name) unless loose?
|
173
|
+
|
174
|
+
EMPTY_HASH
|
175
|
+
end.dup
|
176
|
+
type_opts.transform_values! { |v| v.respond_to?(:call) ? v.call(rest[0][1], target) : v }
|
177
|
+
type_opts.merge!(fetch_filled_options(target[:type], target)) if name == :filled?
|
178
|
+
type_opts
|
179
|
+
end
|
180
|
+
|
181
|
+
# @api private
|
182
|
+
def fetch_filled_options(type, _target)
|
183
|
+
case type
|
184
|
+
when "string"
|
185
|
+
{minLength: 1}
|
186
|
+
when "array"
|
187
|
+
raise_unknown_conversion_error!(:type, :array) unless loose?
|
188
|
+
|
189
|
+
{not: {type: "null"}}
|
190
|
+
else
|
191
|
+
{not: {type: "null"}}
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# @api private
|
196
|
+
def merge_opts!(orig_opts, new_opts)
|
197
|
+
new_type = new_opts[:type]
|
198
|
+
orig_type = orig_opts[:type]
|
199
|
+
|
200
|
+
if orig_type && new_type && orig_type != new_type
|
201
|
+
new_opts[:type] = [orig_type, new_type]
|
202
|
+
end
|
203
|
+
|
204
|
+
orig_opts.merge!(new_opts)
|
205
|
+
end
|
206
|
+
|
207
|
+
# @api private
|
208
|
+
def root?
|
209
|
+
@root
|
210
|
+
end
|
211
|
+
|
212
|
+
# @api private
|
213
|
+
def loose?
|
214
|
+
@loose
|
215
|
+
end
|
216
|
+
|
217
|
+
def raise_unknown_conversion_error!(type, name)
|
218
|
+
message = <<~MSG
|
219
|
+
Could not find an equivalent conversion for #{type} #{name.inspect}.
|
220
|
+
|
221
|
+
This means that your generated JSON schema may be missing this validation.
|
222
|
+
|
223
|
+
You can ignore this by generating the schema in "loose" mode, i.e.:
|
224
|
+
my_schema.json_schema(loose: true)
|
225
|
+
MSG
|
226
|
+
|
227
|
+
raise UnknownConversionError, message.chomp
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/schema/extensions/json_schema/schema_compiler"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Schema
|
7
|
+
# JSONSchema extension
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
module JSONSchema
|
11
|
+
module SchemaMethods
|
12
|
+
# Convert the schema into a JSON schema hash
|
13
|
+
#
|
14
|
+
# @param [Symbol] loose Compile the schema in "loose" mode
|
15
|
+
#
|
16
|
+
# @return [Hash<Symbol=>Hash>]
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def json_schema(loose: false)
|
20
|
+
compiler = SchemaCompiler.new(root: true, loose: loose)
|
21
|
+
compiler.call(to_ast)
|
22
|
+
compiler.to_hash
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Processor.include(JSONSchema::SchemaMethods)
|
28
|
+
end
|
29
|
+
end
|
data/lib/dry/schema/key.rb
CHANGED
@@ -85,98 +85,99 @@ module Dry
|
|
85
85
|
def coerced_name
|
86
86
|
@__coerced_name__ ||= coercer[name]
|
87
87
|
end
|
88
|
-
end
|
89
|
-
|
90
|
-
# A specialized key type which handles nested hashes
|
91
|
-
#
|
92
|
-
# @api private
|
93
|
-
class Key::Hash < Key
|
94
|
-
include Dry.Equalizer(:name, :members, :coercer)
|
95
88
|
|
89
|
+
# A specialized key type which handles nested hashes
|
90
|
+
#
|
96
91
|
# @api private
|
97
|
-
|
92
|
+
class Hash < self
|
93
|
+
include Dry.Equalizer(:name, :members, :coercer)
|
98
94
|
|
99
|
-
|
100
|
-
|
101
|
-
super(id, **opts)
|
102
|
-
@members = members
|
103
|
-
end
|
95
|
+
# @api private
|
96
|
+
attr_reader :members
|
104
97
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
98
|
+
# @api private
|
99
|
+
def initialize(id, members:, **opts)
|
100
|
+
super(id, **opts)
|
101
|
+
@members = members
|
102
|
+
end
|
109
103
|
|
110
|
-
|
111
|
-
read(source)
|
112
|
-
|
113
|
-
|
114
|
-
end
|
104
|
+
# @api private
|
105
|
+
def read(source)
|
106
|
+
super if source.is_a?(::Hash)
|
107
|
+
end
|
115
108
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
109
|
+
def write(source, target)
|
110
|
+
read(source) { |value|
|
111
|
+
target[coerced_name] = value.is_a?(::Hash) ? members.write(value) : value
|
112
|
+
}
|
113
|
+
end
|
120
114
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
115
|
+
# @api private
|
116
|
+
def coercible(&coercer)
|
117
|
+
new(coercer: coercer, members: members.coercible(&coercer))
|
118
|
+
end
|
125
119
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
120
|
+
# @api private
|
121
|
+
def stringified
|
122
|
+
new(name: name.to_s, members: members.stringified)
|
123
|
+
end
|
130
124
|
|
131
|
-
|
132
|
-
|
133
|
-
|
125
|
+
# @api private
|
126
|
+
def to_dot_notation
|
127
|
+
[name].product(members.flat_map(&:to_dot_notation)).map { |e| e.join(DOT) }
|
128
|
+
end
|
129
|
+
|
130
|
+
# @api private
|
131
|
+
def dump
|
132
|
+
{name => members.map(&:dump)}
|
133
|
+
end
|
134
134
|
end
|
135
|
-
end
|
136
135
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
136
|
+
# A specialized key type which handles nested arrays
|
137
|
+
#
|
138
|
+
# @api private
|
139
|
+
class Array < self
|
140
|
+
include Dry.Equalizer(:name, :member, :coercer)
|
142
141
|
|
143
|
-
|
142
|
+
attr_reader :member
|
144
143
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
144
|
+
# @api private
|
145
|
+
def initialize(id, member:, **opts)
|
146
|
+
super(id, **opts)
|
147
|
+
@member = member
|
148
|
+
end
|
150
149
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
150
|
+
# @api private
|
151
|
+
def write(source, target)
|
152
|
+
read(source) { |value|
|
153
|
+
target[coerced_name] =
|
154
|
+
if value.is_a?(::Array)
|
155
|
+
value.map { |el| el.is_a?(::Hash) ? member.write(el) : el }
|
156
|
+
else
|
157
|
+
value
|
158
|
+
end
|
159
|
+
}
|
160
|
+
end
|
161
161
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
162
|
+
# @api private
|
163
|
+
def coercible(&coercer)
|
164
|
+
new(coercer: coercer, member: member.coercible(&coercer))
|
165
|
+
end
|
166
166
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
167
|
+
# @api private
|
168
|
+
def stringified
|
169
|
+
new(name: name.to_s, member: member.stringified)
|
170
|
+
end
|
171
171
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
172
|
+
# @api private
|
173
|
+
def to_dot_notation
|
174
|
+
[:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
|
175
|
+
end
|
176
176
|
|
177
|
-
|
178
|
-
|
179
|
-
|
177
|
+
# @api private
|
178
|
+
def dump
|
179
|
+
[name, member.dump]
|
180
|
+
end
|
180
181
|
end
|
181
182
|
end
|
182
183
|
end
|