dry-schema 1.8.0 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -1
  3. data/README.md +4 -4
  4. data/dry-schema.gemspec +2 -2
  5. data/lib/dry/schema/compiler.rb +1 -1
  6. data/lib/dry/schema/dsl.rb +7 -4
  7. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
  8. data/lib/dry/schema/extensions/hints.rb +11 -9
  9. data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
  10. data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +232 -0
  11. data/lib/dry/schema/extensions/json_schema.rb +29 -0
  12. data/lib/dry/schema/extensions/struct.rb +1 -1
  13. data/lib/dry/schema/extensions.rb +4 -0
  14. data/lib/dry/schema/key.rb +75 -74
  15. data/lib/dry/schema/key_coercer.rb +2 -2
  16. data/lib/dry/schema/key_validator.rb +44 -23
  17. data/lib/dry/schema/macros/array.rb +4 -0
  18. data/lib/dry/schema/macros/core.rb +1 -1
  19. data/lib/dry/schema/macros/dsl.rb +17 -15
  20. data/lib/dry/schema/macros/hash.rb +1 -1
  21. data/lib/dry/schema/macros/key.rb +2 -2
  22. data/lib/dry/schema/macros/schema.rb +2 -0
  23. data/lib/dry/schema/macros/value.rb +7 -0
  24. data/lib/dry/schema/message/or/multi_path.rb +7 -5
  25. data/lib/dry/schema/message_compiler.rb +13 -10
  26. data/lib/dry/schema/messages/i18n.rb +98 -96
  27. data/lib/dry/schema/messages/namespaced.rb +6 -0
  28. data/lib/dry/schema/messages/yaml.rb +165 -158
  29. data/lib/dry/schema/predicate.rb +2 -2
  30. data/lib/dry/schema/predicate_inferrer.rb +2 -0
  31. data/lib/dry/schema/primitive_inferrer.rb +2 -0
  32. data/lib/dry/schema/processor.rb +2 -2
  33. data/lib/dry/schema/result.rb +5 -7
  34. data/lib/dry/schema/trace.rb +5 -1
  35. data/lib/dry/schema/type_registry.rb +1 -2
  36. data/lib/dry/schema/version.rb +1 -1
  37. metadata +11 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc91fbbcd77a2535c39ac072f7725e2cc89cf40fb1710c1f7f2f32867d37351e
4
- data.tar.gz: a725bbc8ae7735fb576b827c5ee4dc00235e51287e7fb20882d8235fc61d72f5
3
+ metadata.gz: c27dabeb33b1644739f90d7bbac22dfe85ae79d742da8376cad64bdfff46c514
4
+ data.tar.gz: 5b4df5b8215933c70fd3c04fd3a8cd74c0ff7bdfe4919110f95753348ce28726
5
5
  SHA512:
6
- metadata.gz: 1dcb14a96c4d9e1c38ca7bafaf04a621ee71cffc75bb4f3328d46752ec291f4ee580fea7f21bc4b430277af601ca31e29c5358d3c72d38ec53e128b411921245
7
- data.tar.gz: 27d66147c3be3d089d92b56ddec1b00a230d83e04606a52c14c10cb0f40648b8beb893f874e00916c5450769b1bdaf72c0df54e3e89464f96bc96986904fe465
6
+ metadata.gz: 9111228ae652344f54044d93fd7516d67cdefa0789587b786d58291d40f9c61c009207c5b3ef68d7302e8e889a125e51fbd4bfa3a5ee6b8b2629a2e06cf7a538
7
+ data.tar.gz: bc1701018a9c35fb618c92a50fb4d68f24957f3d22431e343457dba65645d6476a5c7378620fe934b8520cfd20d62a2d2de1ac5c04af1f0130dfba5de86cb2f9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
2
 
3
+ ## 1.9.2 2022-05-28
4
+
5
+
6
+ ### Fixed
7
+
8
+ - Fix loose JSON schemas for nested hashes (via #401) (@tomdalling)
9
+ - Correct spelling error 'mininum' to 'minimum' in json-schema extension (via #404) (@svenanderzen)
10
+
11
+ ### Changed
12
+
13
+ - [performance] YAML message backend allocates less strings (via #399) (@casperisfine)
14
+
15
+ [Compare v1.9.1...v1.9.2](https://github.com/dry-rb/dry-schema/compare/v1.9.1...v1.9.2)
16
+
17
+ ## 1.9.1 2022-02-17
18
+
19
+
20
+ ### Fixed
21
+
22
+ - Namespaced messages no longer crashes in certain scenarios (see dry-rb/dry-validation#692 fixed via #398) (@krekoten)
23
+
24
+
25
+ [Compare v1.9.0...v1.9.1](https://github.com/dry-rb/dry-schema/compare/v1.9.0...v1.9.1)
26
+
27
+ ## 1.9.0 2022-02-15
28
+
29
+
30
+ ### Added
31
+
32
+ - [EXPERIMENTAL] `json_schema` extension which allows you to convert a schema into a JSON schema (via #369) (@ianks)
33
+
34
+ ### Fixed
35
+
36
+ - Composing schemas no longer crashes in certain scenarios (issue #342 fixed via #366) (@vsuhachev)
37
+ - Fix info extension for typed arrays (issue #394 fixed via #397) (@CandyFet)
38
+
39
+
40
+ [Compare v1.8.0...v1.9.0](https://github.com/dry-rb/dry-schema/compare/v1.8.0...v1.9.0)
41
+
3
42
  ## 1.8.0 2021-09-12
4
43
 
5
44
 
@@ -121,7 +160,7 @@ This release ships with a bunch of internal refactorings that should improve per
121
160
  - Key validation works correctly with a non-nested maybe hashes (issue #311 fixed via #312) (@svobom57)
122
161
 
123
162
 
124
- [Compare v1.5.3...master](https://github.com/dry-rb/dry-schema/compare/v1.5.3...master)
163
+ [Compare v1.5.3...main](https://github.com/dry-rb/dry-schema/compare/v1.5.3...main)
125
164
 
126
165
  ## 1.5.3 2020-08-21
127
166
 
data/README.md CHANGED
@@ -8,10 +8,10 @@
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/CI/badge.svg)][actions]
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
- [![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=master)][inchpages]
14
+ [![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=main)][inchpages]
15
15
 
16
16
  ## Links
17
17
 
@@ -22,8 +22,8 @@
22
22
 
23
23
  This library officially supports the following Ruby versions:
24
24
 
25
- * MRI `>= 2.6.0`
26
- * ~~jruby~~ `>= 9.3` (we are waiting for [2.6 support](https://github.com/jruby/jruby/issues/6161))
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
@@ -27,11 +27,11 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ["lib"]
28
28
 
29
29
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
30
- spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-schema/blob/master/CHANGELOG.md"
30
+ spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-schema/blob/main/CHANGELOG.md"
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.6.0"
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"
@@ -47,7 +47,7 @@ module Dry
47
47
  # @return [Boolean]
48
48
  #
49
49
  # @api private
50
- def supports?(predicate)
50
+ def support?(predicate)
51
51
  predicates.key?(predicate)
52
52
  end
53
53
  end
@@ -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 (`Params`, `JSON` or a custom sub-class)
84
- # @option options [Compiler] :compiler An instance of a rule compiler (must be compatible with `Schema::Compiler`) (optional)
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 any other `Macros::Key` subclass)
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.map { |m| [m.name, m.to_rule] }.to_h.compact)
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
- .hints
42
- .reject { |hint| msg == hint }
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
- class Or::SinglePath
26
- # @api private
27
- def hint?
28
- false
25
+ module Or
26
+ class SinglePath
27
+ # @api private
28
+ def hint?
29
+ false
30
+ end
29
31
  end
30
- end
31
32
 
32
- class Or::MultiPath
33
- # @api private
34
- def hint?
35
- false
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
- keys[key][:type] = type if type
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?: {minimum: 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(loose: loose?) : 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
@@ -18,7 +18,7 @@ module Dry
18
18
  end
19
19
 
20
20
  super(args[0].schema, *args.drop(1))
21
- type(schema_dsl.types[name].constructor(args[0]))
21
+ type(schema_dsl.types[name].constructor(args[0].schema))
22
22
  else
23
23
  super
24
24
  end
@@ -15,3 +15,7 @@ end
15
15
  Dry::Schema.register_extension(:info) do
16
16
  require "dry/schema/extensions/info"
17
17
  end
18
+
19
+ Dry::Schema.register_extension(:json_schema) do
20
+ require "dry/schema/extensions/json_schema"
21
+ end
@@ -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
- attr_reader :members
92
+ class Hash < self
93
+ include Dry.Equalizer(:name, :members, :coercer)
98
94
 
99
- # @api private
100
- def initialize(id, members:, **opts)
101
- super(id, **opts)
102
- @members = members
103
- end
95
+ # @api private
96
+ attr_reader :members
104
97
 
105
- # @api private
106
- def read(source)
107
- super if source.is_a?(::Hash)
108
- end
98
+ # @api private
99
+ def initialize(id, members:, **opts)
100
+ super(id, **opts)
101
+ @members = members
102
+ end
109
103
 
110
- def write(source, target)
111
- read(source) { |value|
112
- target[coerced_name] = value.is_a?(::Hash) ? members.write(value) : value
113
- }
114
- end
104
+ # @api private
105
+ def read(source)
106
+ super if source.is_a?(::Hash)
107
+ end
115
108
 
116
- # @api private
117
- def coercible(&coercer)
118
- new(coercer: coercer, members: members.coercible(&coercer))
119
- end
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
- # @api private
122
- def stringified
123
- new(name: name.to_s, members: members.stringified)
124
- end
115
+ # @api private
116
+ def coercible(&coercer)
117
+ new(coercer: coercer, members: members.coercible(&coercer))
118
+ end
125
119
 
126
- # @api private
127
- def to_dot_notation
128
- [name].product(members.flat_map(&:to_dot_notation)).map { |e| e.join(DOT) }
129
- end
120
+ # @api private
121
+ def stringified
122
+ new(name: name.to_s, members: members.stringified)
123
+ end
130
124
 
131
- # @api private
132
- def dump
133
- {name => members.map(&:dump)}
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
- # A specialized key type which handles nested arrays
138
- #
139
- # @api private
140
- class Key::Array < Key
141
- include Dry.Equalizer(:name, :member, :coercer)
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
- attr_reader :member
142
+ attr_reader :member
144
143
 
145
- # @api private
146
- def initialize(id, member:, **opts)
147
- super(id, **opts)
148
- @member = member
149
- end
144
+ # @api private
145
+ def initialize(id, member:, **opts)
146
+ super(id, **opts)
147
+ @member = member
148
+ end
150
149
 
151
- # @api private
152
- def write(source, target)
153
- read(source) { |value|
154
- target[coerced_name] = 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
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
- # @api private
163
- def coercible(&coercer)
164
- new(coercer: coercer, member: member.coercible(&coercer))
165
- end
162
+ # @api private
163
+ def coercible(&coercer)
164
+ new(coercer: coercer, member: member.coercible(&coercer))
165
+ end
166
166
 
167
- # @api private
168
- def stringified
169
- new(name: name.to_s, member: member.stringified)
170
- end
167
+ # @api private
168
+ def stringified
169
+ new(name: name.to_s, member: member.stringified)
170
+ end
171
171
 
172
- # @api private
173
- def to_dot_notation
174
- [:"#{name}[]"].product(member.to_dot_notation).map { |el| el.join(DOT) }
175
- end
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
- # @api private
178
- def dump
179
- [name, member.dump]
177
+ # @api private
178
+ def dump
179
+ [name, member.dump]
180
+ end
180
181
  end
181
182
  end
182
183
  end