Skip to content

Commit 18ed28c

Browse files
authored
coercing of nested arrays (ruby-grape#2054)
1 parent 23374d6 commit 18ed28c

File tree

8 files changed

+124
-16
lines changed

8 files changed

+124
-16
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* [#2049](https://github.com/ruby-grape/grape/pull/2049): Coerce an empty string to nil in case of the bool type - [@dnesteryuk](https://github.com/dnesteryuk).
1414
* [#2043](https://github.com/ruby-grape/grape/pull/2043): Modify declared for nested array and hash - [@kadotami](https://github.com/kadotami).
1515
* [#2040](https://github.com/ruby-grape/grape/pull/2040): Fix a regression with Array of type nil - [@ericproulx](https://github.com/ericproulx).
16+
* [#2054](https://github.com/ruby-grape/grape/pull/2054): Coercing of nested arrays - [@dnesteryuk](https://github.com/dnesteryuk).
1617
* Your contribution here.
1718

1819
### 1.3.2 (2020/04/12)

lib/grape/validations/types/array_coercer.rb

+12-5
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,41 @@ module Grape
66
module Validations
77
module Types
88
# Coerces elements in an array. It might be an array of strings or integers or
9-
# anything else.
9+
# an array of arrays of integers.
1010
#
1111
# It could've been possible to use an +of+
1212
# method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
1313
# provided by dry-types. Unfortunately, it doesn't work for Grape because of
1414
# behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
1515
# maintains Virtus behavior in coercing.
1616
class ArrayCoercer < DryTypeCoercer
17+
register_collection Array
18+
1719
def initialize(type, strict = false)
1820
super
1921

2022
@coercer = scope::Array
21-
@elem_coercer = PrimitiveCoercer.new(type.first, strict)
23+
@subtype = type.first
2224
end
2325

2426
def call(_val)
2527
collection = super
26-
2728
return collection if collection.is_a?(InvalidValue)
2829

2930
coerce_elements collection
3031
end
3132

3233
protected
3334

35+
attr_reader :subtype
36+
3437
def coerce_elements(collection)
3538
return if collection.nil?
3639

3740
collection.each_with_index do |elem, index|
3841
return InvalidValue.new if reject?(elem)
3942

40-
coerced_elem = @elem_coercer.call(elem)
43+
coerced_elem = elem_coercer.call(elem)
4144

4245
return coerced_elem if coerced_elem.is_a?(InvalidValue)
4346

@@ -47,11 +50,15 @@ def coerce_elements(collection)
4750
collection
4851
end
4952

50-
# This method maintaine logic which was defined by Virtus for arrays.
53+
# This method maintains logic which was defined by Virtus for arrays.
5154
# Virtus doesn't allow nil in arrays.
5255
def reject?(val)
5356
val.nil?
5457
end
58+
59+
def elem_coercer
60+
@elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
61+
end
5562
end
5663
end
5764
end

lib/grape/validations/types/build_coercer.rb

+1-5
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,8 @@ def self.create_coercer_instance(type, method, strict)
6060
Types::CustomTypeCollectionCoercer.new(
6161
Types.map_special(type.first), type.is_a?(Set)
6262
)
63-
elsif type.is_a?(Array)
64-
ArrayCoercer.new type, strict
65-
elsif type.is_a?(Set)
66-
SetCoercer.new type, strict
6763
else
68-
PrimitiveCoercer.new type, strict
64+
DryTypeCoercer.coercer_instance_for(type, strict)
6965
end
7066
end
7167

lib/grape/validations/types/dry_type_coercer.rb

+34-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,41 @@ module Types
1717
# but check its type. More information there
1818
# https://dry-rb.org/gems/dry-types/1.2/built-in-types/
1919
class DryTypeCoercer
20+
class << self
21+
# Registers a collection coercer which could be found by a type,
22+
# see +collection_coercer_for+ method below. This method is meant for inheritors.
23+
def register_collection(type)
24+
DryTypeCoercer.collection_coercers[type] = self
25+
end
26+
27+
# Returns a collection coercer which corresponds to a given type.
28+
# Example:
29+
#
30+
# collection_coercer_for(Array)
31+
# #=> Grape::Validations::Types::ArrayCoercer
32+
def collection_coercer_for(type)
33+
collection_coercers[type]
34+
end
35+
36+
# Returns an instance of a coercer for a given type
37+
def coercer_instance_for(type, strict = false)
38+
return PrimitiveCoercer.new(type, strict) if type.class == Class
39+
40+
# in case of a collection (Array[Integer]) the type is an instance of a collection,
41+
# so we need to figure out the actual type
42+
collection_coercer_for(type.class).new(type, strict)
43+
end
44+
45+
protected
46+
47+
def collection_coercers
48+
@collection_coercers ||= {}
49+
end
50+
end
51+
2052
def initialize(type, strict = false)
2153
@type = type
54+
@strict = strict
2255
@scope = strict ? DryTypes::Strict : DryTypes::Params
2356
end
2457

@@ -36,7 +69,7 @@ def call(val)
3669

3770
protected
3871

39-
attr_reader :scope, :type
72+
attr_reader :scope, :type, :strict
4073
end
4174
end
4275
end

lib/grape/validations/types/set_coercer.rb

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
# frozen_string_literal: true
22

33
require 'set'
4-
require_relative 'dry_type_coercer'
4+
require_relative 'array_coercer'
55

66
module Grape
77
module Validations
88
module Types
99
# Takes the given array and converts it to a set. Every element of the set
1010
# is also coerced.
11-
class SetCoercer < DryTypeCoercer
11+
class SetCoercer < ArrayCoercer
12+
register_collection Set
13+
1214
def initialize(type, strict = false)
1315
super
1416

15-
@elem_coercer = PrimitiveCoercer.new(type.first, strict)
17+
@coercer = nil
1618
end
1719

1820
def call(value)
@@ -25,7 +27,7 @@ def call(value)
2527

2628
def coerce_elements(collection)
2729
collection.each_with_object(Set.new) do |elem, memo|
28-
coerced_elem = @elem_coercer.call(elem)
30+
coerced_elem = elem_coercer.call(elem)
2931

3032
return coerced_elem if coerced_elem.is_a?(InvalidValue)
3133

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe Grape::Validations::Types::ArrayCoercer do
6+
subject { described_class.new(type) }
7+
8+
describe '#call' do
9+
context 'an array of primitives' do
10+
let(:type) { Array[String] }
11+
12+
it 'coerces elements in the array' do
13+
expect(subject.call([10, 20])).to eq(%w[10 20])
14+
end
15+
end
16+
17+
context 'an array of arrays' do
18+
let(:type) { Array[Array[Integer]] }
19+
20+
it 'coerces elements in the nested array' do
21+
expect(subject.call([%w[10 20]])).to eq([[10, 20]])
22+
expect(subject.call([['10'], ['20']])).to eq([[10], [20]])
23+
end
24+
end
25+
26+
context 'an array of sets' do
27+
let(:type) { Array[Set[Integer]] }
28+
29+
it 'coerces elements in the nested set' do
30+
expect(subject.call([%w[10 20]])).to eq([Set[10, 20]])
31+
expect(subject.call([['10'], ['20']])).to eq([Set[10], Set[20]])
32+
end
33+
end
34+
end
35+
end

spec/grape/validations/types/primitive_coercer_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
subject { described_class.new(type, strict) }
99

10-
describe '.call' do
10+
describe '#call' do
1111
context 'Boolean' do
1212
let(:type) { Grape::API::Boolean }
1313

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe Grape::Validations::Types::SetCoercer do
6+
subject { described_class.new(type) }
7+
8+
describe '#call' do
9+
context 'a set of primitives' do
10+
let(:type) { Set[String] }
11+
12+
it 'coerces elements to the set' do
13+
expect(subject.call([10, 20])).to eq(Set['10', '20'])
14+
end
15+
end
16+
17+
context 'a set of sets' do
18+
let(:type) { Set[Set[Integer]] }
19+
20+
it 'coerces elements in the nested set' do
21+
expect(subject.call([%w[10 20]])).to eq(Set[Set[10, 20]])
22+
expect(subject.call([['10'], ['20']])).to eq(Set[Set[10], Set[20]])
23+
end
24+
end
25+
26+
context 'a set of sets of arrays' do
27+
let(:type) { Set[Set[Array[Integer]]] }
28+
29+
it 'coerces elements in the nested set' do
30+
expect(subject.call([[['10'], ['20']]])).to eq(Set[Set[Array[10], Array[20]]])
31+
end
32+
end
33+
end
34+
end

0 commit comments

Comments
 (0)