Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docsite/source/nested-data.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,25 @@ end
schema.call(tags: nil).success? # true
schema.call(tags: [{ name: 'Alice' }, { name: 'Bob' }]).success? # true
```

### Nested Array of Arrays

To define a matrix-like structure (an array of arrays), nest `value(:array)` macros:

```ruby
schema = Dry::Schema.Params do
config.validate_keys = true
required(:matrix).value(:array).each do
value(:array).each do
value(:integer, gteq?: 0)
end
end
end

errors = schema.call(matrix: [[1, 2, 3], [4, -5, 6]]).errors

puts errors.to_h.inspect

# => {:matrix=>{1=>{1=>["must be greater than or equal to 0"]}}}
```

23 changes: 18 additions & 5 deletions lib/dry/schema/key_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module Schema
class KeyValidator
extend ::Dry::Initializer

MULTI_INDEX_REGEX = /(\[\d+\])+/
INDEX_REGEX = /\[\d+\]/
DIGIT_REGEX = /\A\d+\z/
BRACKETS = "[]"
Expand Down Expand Up @@ -38,8 +39,8 @@ def call(result)

# @api private
def validate_path(key_paths, path)
if path[INDEX_REGEX]
key = path.gsub(INDEX_REGEX, BRACKETS)
if path[MULTI_INDEX_REGEX]
key = path.gsub(MULTI_INDEX_REGEX, BRACKETS)
if none_key_paths_match?(key_paths, key)
arr = path.gsub(INDEX_REGEX) { ".#{_1[1]}" }
arr.split(DOT).map { DIGIT_REGEX.match?(_1) ? Integer(_1, 10) : _1.to_sym }
Expand Down Expand Up @@ -83,9 +84,7 @@ def key_paths(hash)
if hashes_or_arrays.empty?
[key.to_s]
else
hashes_or_arrays.flat_map.with_index { |el, idx|
key_paths(el).map { ["#{key}[#{idx}]", *_1].join(DOT) }
}
traverse_array(value).map { |sub_path| [key, sub_path].join("") }
end
else
key.to_s
Expand All @@ -99,6 +98,20 @@ def hashes_or_arrays(xs)
(x.is_a?(::Array) || x.is_a?(::Hash)) && !x.empty?
}
end

# @api private
def traverse_array(arr)
arr.each_with_index.flat_map do |el, idx|
case el
when ::Hash
key_paths(el).map { ["[#{idx}]", *_1].join(DOT) }
when ::Array
traverse_array(el).map { ["[#{idx}]", *_1].join("") }
else
[]
end
end
end
end
end
end
185 changes: 185 additions & 0 deletions spec/integration/schema/unexpected_keys_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,191 @@
end
end

context "with a nested array of hashes validator" do
subject(:schema) do
Dry::Schema.define do
config.validate_keys = true
required(:stringkey).filled(:string)
required(:harray).value(:array, size?: 2).each do
hash do
required(:foo).filled(:string)
required(:bar).filled(:integer)
end
end
required(:matrix).value(:array, size?: 2).each do
value(:array, size?: 3).each do
hash do
optional(:fzz).filled(:string)
optional(:bzz).filled(:integer)
optional(:grr).filled(:integer)
end
end
end
end
end

it "doesn't add error messages when there are no unexpected keys" do
array = [
{foo: "a", bar: 1},
{foo: "b", bar: 2}
]
matrix = [
[{fzz: "a", bzz: 1, grr: 3}, {fzz: "b", bzz: 2, grr: 4}, {fzz: "c", bzz: 3, grr: 5}],
[{fzz: "a", bzz: 1}, {fzz: "b", bzz: 2}, {fzz: "c", bzz: 3}]
]
expect(schema.(stringkey: "toto", harray: array, matrix: matrix).errors.to_h).to eq({})
end

it "adds error messages when matrix has wrong type members" do
array = [
{foo: "a", bar: 1},
{foo: "b", bar: 2}
]
matrix = [
[{fzz: 1, bzz: "a"}, {fzz: "b", bzz: 2}, {fzz: "c", bzz: 3}],
[{fzz: "a", bzz: 1}, {fzz: 2, bzz: "b"}, {fzz: "c", bzz: 3}]
]
expect(schema.(stringkey: "toto", harray: array, matrix: matrix).errors.to_h)
.to eq(
{
matrix: {
0 => {0 => {fzz: ["must be a string"], bzz: ["must be an integer"]}},
1 => {1 => {fzz: ["must be a string"], bzz: ["must be an integer"]}}
}
}
)
end

it "adds error messages when matrix has wrong dimensions" do
array = [
{foo: "a", bar: 1},
{foo: "b", bar: 2}
]
matrix = [
[{fzz: "a", bzz: 1}, {fzz: "b", bzz: 2}, {fzz: "c", bzz: 3}],
[{fzz: "a", bzz: 1}, {fzz: "c", bzz: 3}],
[{fzz: "a", bzz: 1}, {fzz: "c", bzz: 3}]
]
expect(schema.(stringkey: "toto", harray: array, matrix: matrix).errors.to_h)
.to eq(
{
matrix: ["size must be 2"]
}
)
end

it "adds error messages when matrix has wrong nested dimensions" do
array = [
{foo: "a", bar: 1},
{foo: "b", bar: 2}
]
matrix = [
[{fzz: "a", bzz: 1}, {fzz: "b", bzz: 2}, {fzz: "c", bzz: 3}],
[{fzz: "a", bzz: 1}, {fzz: "c", bzz: 3}]
]
expect(schema.(stringkey: "toto", harray: array, matrix: matrix).errors.to_h)
.to eq(
{
matrix: {1 => ["size must be 3"]}
}
)
end

it "adds error messages when there are unexpected array elements" do
array = [
{foo: "a", bar: 1, baz: "unexpected"},
{foo: "b", bar: 2}
]
matrix = [
[{title: "a", bzz: 1}, {fzz: "b", bzz: 2}, {fzz: "c", bzz: 3}],
[{fzz: "a", bzz: 1}, {fzz: "b", bzz: 2}, {name: "c", bzz: 3}]
]
expect(schema.(stringkey: "toto", harray: array, matrix: matrix).errors.to_h)
.to eq(
harray: {0 => {baz: ["is not allowed"]}},
matrix: {
0 => {0 => {title: ["is not allowed"]}},
1 => {2 => {name: ["is not allowed"]}}
}
)
end
end

context "with a nested array of strings" do
subject(:schema) do
Dry::Schema.define do
config.validate_keys = true
required(:matrix).value(:array, size?: 2).each do
value(:array, size?: 3).each do
value(:string, size?: 1)
end
end
end
end

it "doesn't add error messages when there are no unexpected keys" do
matrix = [
%w[a b c],
%w[d e f]
]
expect(schema.(matrix: matrix).errors.to_h).to eq({})
end

it "adds error messages when matrix has wrong type members" do
matrix = [
["a", 1, "c"],
%w[d e f]
]
expect(schema.(matrix: matrix).errors.to_h).to eq(
{
matrix: {
0 => {1 => ["must be a string"]}
}
}
)
end

it "adds error messages when matrix has wrong dimensions" do
matrix = [
%w[a b c],
%w[d e f],
%w[g h i]
]
expect(schema.(matrix: matrix).errors.to_h)
.to eq(
{
matrix: ["size must be 2"]
}
)
end

it "adds error messages when matrix has wrong nested dimensions" do
matrix = [
%w[a b c],
%w[d e f g]
]
expect(schema.(matrix: matrix).errors.to_h)
.to eq(
{
matrix: {1 => ["size must be 3"]}
}
)
end

it "adds error messages when there are unexpected array elements" do
matrix = [
%w[a b c],
["d", {foo: "unexpected"}, "f"]
]
expect(schema.(matrix: matrix).errors.to_h)
.to eq(
matrix: {
1 => {1 => {foo: ["is not allowed"]}}
}
)
end
end

context "with a non-nested maybe hash validator" do
subject(:schema) do
Dry::Schema.define do
Expand Down
Loading