Skip to content

Commit

Permalink
coercing of nested arrays (#2054)
Browse files Browse the repository at this point in the history
  • Loading branch information
dnesteryuk committed May 16, 2020
1 parent 23374d6 commit 18ed28c
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* [#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).
* [#2043](https://github.com/ruby-grape/grape/pull/2043): Modify declared for nested array and hash - [@kadotami](https://github.com/kadotami).
* [#2040](https://github.com/ruby-grape/grape/pull/2040): Fix a regression with Array of type nil - [@ericproulx](https://github.com/ericproulx).
* [#2054](https://github.com/ruby-grape/grape/pull/2054): Coercing of nested arrays - [@dnesteryuk](https://github.com/dnesteryuk).
* Your contribution here.

### 1.3.2 (2020/04/12)
Expand Down
17 changes: 12 additions & 5 deletions lib/grape/validations/types/array_coercer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,41 @@ module Grape
module Validations
module Types
# Coerces elements in an array. It might be an array of strings or integers or
# anything else.
# an array of arrays of integers.
#
# It could've been possible to use an +of+
# method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
# provided by dry-types. Unfortunately, it doesn't work for Grape because of
# behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
# maintains Virtus behavior in coercing.
class ArrayCoercer < DryTypeCoercer
register_collection Array

def initialize(type, strict = false)
super

@coercer = scope::Array
@elem_coercer = PrimitiveCoercer.new(type.first, strict)
@subtype = type.first
end

def call(_val)
collection = super

return collection if collection.is_a?(InvalidValue)

coerce_elements collection
end

protected

attr_reader :subtype

def coerce_elements(collection)
return if collection.nil?

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

coerced_elem = @elem_coercer.call(elem)
coerced_elem = elem_coercer.call(elem)

return coerced_elem if coerced_elem.is_a?(InvalidValue)

Expand All @@ -47,11 +50,15 @@ def coerce_elements(collection)
collection
end

# This method maintaine logic which was defined by Virtus for arrays.
# This method maintains logic which was defined by Virtus for arrays.
# Virtus doesn't allow nil in arrays.
def reject?(val)
val.nil?
end

def elem_coercer
@elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
end
end
end
end
Expand Down
6 changes: 1 addition & 5 deletions lib/grape/validations/types/build_coercer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,8 @@ def self.create_coercer_instance(type, method, strict)
Types::CustomTypeCollectionCoercer.new(
Types.map_special(type.first), type.is_a?(Set)
)
elsif type.is_a?(Array)
ArrayCoercer.new type, strict
elsif type.is_a?(Set)
SetCoercer.new type, strict
else
PrimitiveCoercer.new type, strict
DryTypeCoercer.coercer_instance_for(type, strict)
end
end

Expand Down
35 changes: 34 additions & 1 deletion lib/grape/validations/types/dry_type_coercer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,41 @@ module Types
# but check its type. More information there
# https://dry-rb.org/gems/dry-types/1.2/built-in-types/
class DryTypeCoercer
class << self
# Registers a collection coercer which could be found by a type,
# see +collection_coercer_for+ method below. This method is meant for inheritors.
def register_collection(type)
DryTypeCoercer.collection_coercers[type] = self
end

# Returns a collection coercer which corresponds to a given type.
# Example:
#
# collection_coercer_for(Array)
# #=> Grape::Validations::Types::ArrayCoercer
def collection_coercer_for(type)
collection_coercers[type]
end

# Returns an instance of a coercer for a given type
def coercer_instance_for(type, strict = false)
return PrimitiveCoercer.new(type, strict) if type.class == Class

# in case of a collection (Array[Integer]) the type is an instance of a collection,
# so we need to figure out the actual type
collection_coercer_for(type.class).new(type, strict)
end

protected

def collection_coercers
@collection_coercers ||= {}
end
end

def initialize(type, strict = false)
@type = type
@strict = strict
@scope = strict ? DryTypes::Strict : DryTypes::Params
end

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

protected

attr_reader :scope, :type
attr_reader :scope, :type, :strict
end
end
end
Expand Down
10 changes: 6 additions & 4 deletions lib/grape/validations/types/set_coercer.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# frozen_string_literal: true

require 'set'
require_relative 'dry_type_coercer'
require_relative 'array_coercer'

module Grape
module Validations
module Types
# Takes the given array and converts it to a set. Every element of the set
# is also coerced.
class SetCoercer < DryTypeCoercer
class SetCoercer < ArrayCoercer
register_collection Set

def initialize(type, strict = false)
super

@elem_coercer = PrimitiveCoercer.new(type.first, strict)
@coercer = nil
end

def call(value)
Expand All @@ -25,7 +27,7 @@ def call(value)

def coerce_elements(collection)
collection.each_with_object(Set.new) do |elem, memo|
coerced_elem = @elem_coercer.call(elem)
coerced_elem = elem_coercer.call(elem)

return coerced_elem if coerced_elem.is_a?(InvalidValue)

Expand Down
35 changes: 35 additions & 0 deletions spec/grape/validations/types/array_coercer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

require 'spec_helper'

describe Grape::Validations::Types::ArrayCoercer do
subject { described_class.new(type) }

describe '#call' do
context 'an array of primitives' do
let(:type) { Array[String] }

it 'coerces elements in the array' do
expect(subject.call([10, 20])).to eq(%w[10 20])
end
end

context 'an array of arrays' do
let(:type) { Array[Array[Integer]] }

it 'coerces elements in the nested array' do
expect(subject.call([%w[10 20]])).to eq([[10, 20]])
expect(subject.call([['10'], ['20']])).to eq([[10], [20]])
end
end

context 'an array of sets' do
let(:type) { Array[Set[Integer]] }

it 'coerces elements in the nested set' do
expect(subject.call([%w[10 20]])).to eq([Set[10, 20]])
expect(subject.call([['10'], ['20']])).to eq([Set[10], Set[20]])
end
end
end
end
2 changes: 1 addition & 1 deletion spec/grape/validations/types/primitive_coercer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

subject { described_class.new(type, strict) }

describe '.call' do
describe '#call' do
context 'Boolean' do
let(:type) { Grape::API::Boolean }

Expand Down
34 changes: 34 additions & 0 deletions spec/grape/validations/types/set_coercer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require 'spec_helper'

describe Grape::Validations::Types::SetCoercer do
subject { described_class.new(type) }

describe '#call' do
context 'a set of primitives' do
let(:type) { Set[String] }

it 'coerces elements to the set' do
expect(subject.call([10, 20])).to eq(Set['10', '20'])
end
end

context 'a set of sets' do
let(:type) { Set[Set[Integer]] }

it 'coerces elements in the nested set' do
expect(subject.call([%w[10 20]])).to eq(Set[Set[10, 20]])
expect(subject.call([['10'], ['20']])).to eq(Set[Set[10], Set[20]])
end
end

context 'a set of sets of arrays' do
let(:type) { Set[Set[Array[Integer]]] }

it 'coerces elements in the nested set' do
expect(subject.call([[['10'], ['20']]])).to eq(Set[Set[Array[10], Array[20]]])
end
end
end
end

0 comments on commit 18ed28c

Please sign in to comment.