grape 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/LICENSE +1 -1
- data/README.md +19 -6
- data/UPGRADING.md +120 -16
- data/lib/grape/api/instance.rb +12 -7
- data/lib/grape/dsl/inside_route.rb +37 -14
- data/lib/grape/http/headers.rb +1 -0
- data/lib/grape/middleware/versioner/header.rb +1 -1
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
- data/lib/grape/path.rb +2 -2
- data/lib/grape/router/attribute_translator.rb +23 -2
- data/lib/grape/router/route.rb +3 -22
- data/lib/grape/router.rb +6 -14
- data/lib/grape/util/base_inheritable.rb +9 -6
- data/lib/grape/util/reverse_stackable_values.rb +3 -1
- data/lib/grape/util/stackable_values.rb +3 -1
- data/lib/grape/validations/types/array_coercer.rb +14 -5
- data/lib/grape/validations/types/build_coercer.rb +5 -8
- data/lib/grape/validations/types/custom_type_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
- data/lib/grape/validations/types/file.rb +15 -13
- data/lib/grape/validations/types/json.rb +40 -36
- data/lib/grape/validations/types/primitive_coercer.rb +11 -4
- data/lib/grape/validations/types/set_coercer.rb +6 -4
- data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
- data/lib/grape/validations/types.rb +6 -5
- data/lib/grape/validations/validators/coerce.rb +3 -10
- data/lib/grape/validations/validators/default.rb +0 -1
- data/lib/grape/validations/validators/regexp.rb +1 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api/instance_spec.rb +50 -0
- data/spec/grape/endpoint_spec.rb +18 -5
- data/spec/grape/path_spec.rb +4 -4
- data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
- data/spec/grape/validations/types/primitive_coercer_spec.rb +5 -1
- data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
- data/spec/grape/validations/types_spec.rb +1 -1
- data/spec/grape/validations/validators/coerce_spec.rb +207 -29
- data/spec/grape/validations/validators/default_spec.rb +121 -0
- data/spec/grape/validations/validators/values_spec.rb +1 -1
- data/spec/grape/validations_spec.rb +5 -5
- metadata +9 -5
@@ -29,9 +29,11 @@ module Grape
|
|
29
29
|
protected
|
30
30
|
|
31
31
|
def concat_values(inherited_value, new_value)
|
32
|
+
return inherited_value unless new_value
|
33
|
+
|
32
34
|
[].tap do |value|
|
33
35
|
value.concat(inherited_value)
|
34
|
-
value.concat(new_value)
|
36
|
+
value.concat(new_value)
|
35
37
|
end
|
36
38
|
end
|
37
39
|
end
|
@@ -6,7 +6,7 @@ module Grape
|
|
6
6
|
module Validations
|
7
7
|
module Types
|
8
8
|
# Coerces elements in an array. It might be an array of strings or integers or
|
9
|
-
#
|
9
|
+
# an array of arrays of integers.
|
10
10
|
#
|
11
11
|
# It could've been possible to use an +of+
|
12
12
|
# method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
|
@@ -14,16 +14,17 @@ module Grape
|
|
14
14
|
# behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
|
15
15
|
# maintains Virtus behavior in coercing.
|
16
16
|
class ArrayCoercer < DryTypeCoercer
|
17
|
+
register_collection Array
|
18
|
+
|
17
19
|
def initialize(type, strict = false)
|
18
20
|
super
|
19
21
|
|
20
22
|
@coercer = scope::Array
|
21
|
-
@
|
23
|
+
@subtype = type.first
|
22
24
|
end
|
23
25
|
|
24
26
|
def call(_val)
|
25
27
|
collection = super
|
26
|
-
|
27
28
|
return collection if collection.is_a?(InvalidValue)
|
28
29
|
|
29
30
|
coerce_elements collection
|
@@ -31,11 +32,15 @@ module Grape
|
|
31
32
|
|
32
33
|
protected
|
33
34
|
|
35
|
+
attr_reader :subtype
|
36
|
+
|
34
37
|
def coerce_elements(collection)
|
38
|
+
return if collection.nil?
|
39
|
+
|
35
40
|
collection.each_with_index do |elem, index|
|
36
41
|
return InvalidValue.new if reject?(elem)
|
37
42
|
|
38
|
-
coerced_elem =
|
43
|
+
coerced_elem = elem_coercer.call(elem)
|
39
44
|
|
40
45
|
return coerced_elem if coerced_elem.is_a?(InvalidValue)
|
41
46
|
|
@@ -45,11 +50,15 @@ module Grape
|
|
45
50
|
collection
|
46
51
|
end
|
47
52
|
|
48
|
-
# This method
|
53
|
+
# This method maintains logic which was defined by Virtus for arrays.
|
49
54
|
# Virtus doesn't allow nil in arrays.
|
50
55
|
def reject?(val)
|
51
56
|
val.nil?
|
52
57
|
end
|
58
|
+
|
59
|
+
def elem_coercer
|
60
|
+
@elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
|
61
|
+
end
|
53
62
|
end
|
54
63
|
end
|
55
64
|
end
|
@@ -42,6 +42,9 @@ module Grape
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def self.create_coercer_instance(type, method, strict)
|
45
|
+
# Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!!
|
46
|
+
type = Types.map_special(type)
|
47
|
+
|
45
48
|
# Use a special coercer for multiply-typed parameters.
|
46
49
|
if Types.multiple?(type)
|
47
50
|
MultipleTypeCoercer.new(type, method)
|
@@ -55,16 +58,10 @@ module Grape
|
|
55
58
|
# method is supplied.
|
56
59
|
elsif Types.collection_of_custom?(type)
|
57
60
|
Types::CustomTypeCollectionCoercer.new(
|
58
|
-
type.first, type.is_a?(Set)
|
61
|
+
Types.map_special(type.first), type.is_a?(Set)
|
59
62
|
)
|
60
|
-
elsif Types.special?(type)
|
61
|
-
Types::SPECIAL[type].new
|
62
|
-
elsif type.is_a?(Array)
|
63
|
-
ArrayCoercer.new type, strict
|
64
|
-
elsif type.is_a?(Set)
|
65
|
-
SetCoercer.new type, strict
|
66
63
|
else
|
67
|
-
|
64
|
+
DryTypeCoercer.coercer_instance_for(type, strict)
|
68
65
|
end
|
69
66
|
end
|
70
67
|
|
@@ -17,8 +17,41 @@ module Grape
|
|
17
17
|
# but check its type. More information there
|
18
18
|
# https://dry-rb.org/gems/dry-types/1.2/built-in-types/
|
19
19
|
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
|
+
|
20
52
|
def initialize(type, strict = false)
|
21
53
|
@type = type
|
54
|
+
@strict = strict
|
22
55
|
@scope = strict ? DryTypes::Strict : DryTypes::Params
|
23
56
|
end
|
24
57
|
|
@@ -27,6 +60,8 @@ module Grape
|
|
27
60
|
#
|
28
61
|
# @param val [Object]
|
29
62
|
def call(val)
|
63
|
+
return if val.nil?
|
64
|
+
|
30
65
|
@coercer[val]
|
31
66
|
rescue Dry::Types::CoercionError => _e
|
32
67
|
InvalidValue.new
|
@@ -34,7 +69,7 @@ module Grape
|
|
34
69
|
|
35
70
|
protected
|
36
71
|
|
37
|
-
attr_reader :scope, :type
|
72
|
+
attr_reader :scope, :type, :strict
|
38
73
|
end
|
39
74
|
end
|
40
75
|
end
|
@@ -7,21 +7,23 @@ module Grape
|
|
7
7
|
# Actual handling of these objects is provided by +Rack::Request+;
|
8
8
|
# this class is here only to assert that rack's handling has succeeded.
|
9
9
|
class File
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
class << self
|
11
|
+
def parse(input)
|
12
|
+
return if input.nil?
|
13
|
+
return InvalidValue.new unless parsed?(input)
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
# Processing of multipart file objects
|
16
|
+
# is already taken care of by Rack::Request.
|
17
|
+
# Nothing to do here.
|
18
|
+
input
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
def parsed?(value)
|
22
|
+
# Rack::Request creates a Hash with filename,
|
23
|
+
# content type and an IO object. Do a bit of basic
|
24
|
+
# duck-typing.
|
25
|
+
value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile)
|
26
|
+
end
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
@@ -12,35 +12,37 @@ module Grape
|
|
12
12
|
# validation system will apply nested validation rules to
|
13
13
|
# all returned objects.
|
14
14
|
class Json
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
class << self
|
16
|
+
# Coerce the input into a JSON-like data structure.
|
17
|
+
#
|
18
|
+
# @param input [String] a JSON-encoded parameter value
|
19
|
+
# @return [Hash,Array<Hash>,nil]
|
20
|
+
def parse(input)
|
21
|
+
return input if parsed?(input)
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
# Allow nulls and blank strings
|
24
|
+
return if input.nil? || input.match?(/^\s*$/)
|
25
|
+
JSON.parse(input, symbolize_names: true)
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
28
|
+
# Checks that the input was parsed successfully
|
29
|
+
# and isn't something odd such as an array of primitives.
|
30
|
+
#
|
31
|
+
# @param value [Object] result of {#parse}
|
32
|
+
# @return [true,false]
|
33
|
+
def parsed?(value)
|
34
|
+
value.is_a?(::Hash) || coerced_collection?(value)
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
+
protected
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
# Is the value an array of JSON-like objects?
|
40
|
+
#
|
41
|
+
# @param value [Object] result of {#parse}
|
42
|
+
# @return [true,false]
|
43
|
+
def coerced_collection?(value)
|
44
|
+
value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash }
|
45
|
+
end
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
@@ -49,18 +51,20 @@ module Grape
|
|
49
51
|
# objects and arrays of objects, but wraps single objects
|
50
52
|
# in an Array.
|
51
53
|
class JsonArray < Json
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
54
|
+
class << self
|
55
|
+
# See {Json#parse}. Wraps single objects in an array.
|
56
|
+
#
|
57
|
+
# @param input [String] JSON-encoded parameter value
|
58
|
+
# @return [Array<Hash>]
|
59
|
+
def parse(input)
|
60
|
+
json = super
|
61
|
+
Array.wrap(json) unless json.nil?
|
62
|
+
end
|
60
63
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
+
# See {Json#coerced_collection?}
|
65
|
+
def parsed?(value)
|
66
|
+
coerced_collection? value
|
67
|
+
end
|
64
68
|
end
|
65
69
|
end
|
66
70
|
end
|
@@ -11,10 +11,10 @@ module Grape
|
|
11
11
|
class PrimitiveCoercer < DryTypeCoercer
|
12
12
|
MAPPING = {
|
13
13
|
Grape::API::Boolean => DryTypes::Params::Bool,
|
14
|
+
BigDecimal => DryTypes::Params::Decimal,
|
14
15
|
|
15
16
|
# unfortunately, a +Params+ scope doesn't contain String
|
16
|
-
String => DryTypes::Coercible::String
|
17
|
-
BigDecimal => DryTypes::Coercible::Decimal
|
17
|
+
String => DryTypes::Coercible::String
|
18
18
|
}.freeze
|
19
19
|
|
20
20
|
STRICT_MAPPING = {
|
@@ -36,7 +36,7 @@ module Grape
|
|
36
36
|
|
37
37
|
def call(val)
|
38
38
|
return InvalidValue.new if reject?(val)
|
39
|
-
return nil if val.nil?
|
39
|
+
return nil if val.nil? || treat_as_nil?(val)
|
40
40
|
return '' if val == ''
|
41
41
|
|
42
42
|
super
|
@@ -46,7 +46,7 @@ module Grape
|
|
46
46
|
|
47
47
|
attr_reader :type
|
48
48
|
|
49
|
-
# This method
|
49
|
+
# This method maintains logic which was defined by Virtus. For example,
|
50
50
|
# dry-types is ok to convert an array or a hash to a string, it is supported,
|
51
51
|
# but Virtus wouldn't accept it. So, this method only exists to not introduce
|
52
52
|
# breaking changes.
|
@@ -55,6 +55,13 @@ module Grape
|
|
55
55
|
(val.is_a?(String) && type == Hash) ||
|
56
56
|
(val.is_a?(Hash) && type == String)
|
57
57
|
end
|
58
|
+
|
59
|
+
# Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
|
60
|
+
# absence of a value and coerces it into nil. See a discussion there
|
61
|
+
# https://github.com/ruby-grape/grape/pull/2045
|
62
|
+
def treat_as_nil?(val)
|
63
|
+
val == '' && type == Grape::API::Boolean
|
64
|
+
end
|
58
65
|
end
|
59
66
|
end
|
60
67
|
end
|
@@ -1,18 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'set'
|
4
|
-
require_relative '
|
4
|
+
require_relative 'array_coercer'
|
5
5
|
|
6
6
|
module Grape
|
7
7
|
module Validations
|
8
8
|
module Types
|
9
9
|
# Takes the given array and converts it to a set. Every element of the set
|
10
10
|
# is also coerced.
|
11
|
-
class SetCoercer <
|
11
|
+
class SetCoercer < ArrayCoercer
|
12
|
+
register_collection Set
|
13
|
+
|
12
14
|
def initialize(type, strict = false)
|
13
15
|
super
|
14
16
|
|
15
|
-
@
|
17
|
+
@coercer = nil
|
16
18
|
end
|
17
19
|
|
18
20
|
def call(value)
|
@@ -25,7 +27,7 @@ module Grape
|
|
25
27
|
|
26
28
|
def coerce_elements(collection)
|
27
29
|
collection.each_with_object(Set.new) do |elem, memo|
|
28
|
-
coerced_elem =
|
30
|
+
coerced_elem = elem_coercer.call(elem)
|
29
31
|
|
30
32
|
return coerced_elem if coerced_elem.is_a?(InvalidValue)
|
31
33
|
|
@@ -42,7 +42,6 @@ module Grape
|
|
42
42
|
Grape::API::Boolean,
|
43
43
|
String,
|
44
44
|
Symbol,
|
45
|
-
Rack::Multipart::UploadedFile,
|
46
45
|
TrueClass,
|
47
46
|
FalseClass
|
48
47
|
].freeze
|
@@ -54,8 +53,7 @@ module Grape
|
|
54
53
|
Set
|
55
54
|
].freeze
|
56
55
|
|
57
|
-
#
|
58
|
-
# and type-checking logic.
|
56
|
+
# Special custom types provided by Grape.
|
59
57
|
SPECIAL = {
|
60
58
|
JSON => Json,
|
61
59
|
Array[JSON] => JsonArray,
|
@@ -130,7 +128,6 @@ module Grape
|
|
130
128
|
!primitive?(type) &&
|
131
129
|
!structure?(type) &&
|
132
130
|
!multiple?(type) &&
|
133
|
-
!special?(type) &&
|
134
131
|
type.respond_to?(:parse) &&
|
135
132
|
type.method(:parse).arity == 1
|
136
133
|
end
|
@@ -143,7 +140,11 @@ module Grape
|
|
143
140
|
def self.collection_of_custom?(type)
|
144
141
|
(type.is_a?(Array) || type.is_a?(Set)) &&
|
145
142
|
type.length == 1 &&
|
146
|
-
custom?(type.first)
|
143
|
+
(custom?(type.first) || special?(type.first))
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.map_special(type)
|
147
|
+
SPECIAL.fetch(type, type)
|
147
148
|
end
|
148
149
|
end
|
149
150
|
end
|
@@ -47,7 +47,9 @@ module Grape
|
|
47
47
|
# h[:list] = list
|
48
48
|
# h
|
49
49
|
# => #<Hashie::Mash list=[1, 2, 3, 4]>
|
50
|
-
params[attr_name]
|
50
|
+
return if params[attr_name].class == new_value.class && params[attr_name] == new_value
|
51
|
+
|
52
|
+
params[attr_name] = new_value
|
51
53
|
end
|
52
54
|
|
53
55
|
private
|
@@ -65,16 +67,7 @@ module Grape
|
|
65
67
|
end
|
66
68
|
|
67
69
|
def coerce_value(val)
|
68
|
-
# define default values for structures, the dry-types lib which is used
|
69
|
-
# for coercion doesn't accept nil as a value, so it would fail
|
70
|
-
if val.nil?
|
71
|
-
return [] if type == Array || type.is_a?(Array)
|
72
|
-
return Set.new if type == Set
|
73
|
-
return {} if type == Hash
|
74
|
-
end
|
75
|
-
|
76
70
|
converter.call(val)
|
77
|
-
|
78
71
|
# Some custom types might fail, so it should be treated as an invalid value
|
79
72
|
rescue StandardError
|
80
73
|
Types::InvalidValue.new
|
@@ -5,7 +5,7 @@ module Grape
|
|
5
5
|
class RegexpValidator < Base
|
6
6
|
def validate_param!(attr_name, params)
|
7
7
|
return unless params.respond_to?(:key?) && params.key?(attr_name)
|
8
|
-
return if Array.wrap(params[attr_name]).all? { |param| param.nil? ||
|
8
|
+
return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.match?((options_key?(:value) ? @option[:value] : @option)) }
|
9
9
|
raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp))
|
10
10
|
end
|
11
11
|
end
|
data/lib/grape/version.rb
CHANGED
@@ -51,4 +51,54 @@ describe Grape::API::Instance do
|
|
51
51
|
expect(an_instance.top_level_setting.parent).to be_nil
|
52
52
|
end
|
53
53
|
end
|
54
|
+
|
55
|
+
context 'with multiple moutes' do
|
56
|
+
let(:first) do
|
57
|
+
Class.new(Grape::API::Instance) do
|
58
|
+
namespace(:some_namespace) do
|
59
|
+
route :any, '*path' do
|
60
|
+
error!('Not found! (1)', 404)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
let(:second) do
|
66
|
+
Class.new(Grape::API::Instance) do
|
67
|
+
namespace(:another_namespace) do
|
68
|
+
route :any, '*path' do
|
69
|
+
error!('Not found! (2)', 404)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
let(:root_api) do
|
75
|
+
first_instance = first
|
76
|
+
second_instance = second
|
77
|
+
Class.new(Grape::API) do
|
78
|
+
mount first_instance
|
79
|
+
mount first_instance
|
80
|
+
mount second_instance
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'does not raise a FrozenError on first instance' do
|
85
|
+
expect { patch '/some_namespace/anything' }.not_to \
|
86
|
+
raise_error
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'responds the correct body at the first instance' do
|
90
|
+
patch '/some_namespace/anything'
|
91
|
+
expect(last_response.body).to eq 'Not found! (1)'
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'does not raise a FrozenError on second instance' do
|
95
|
+
expect { get '/another_namespace/other' }.not_to \
|
96
|
+
raise_error
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'responds the correct body at the second instance' do
|
100
|
+
get '/another_namespace/foobar'
|
101
|
+
expect(last_response.body).to eq 'Not found! (2)'
|
102
|
+
end
|
103
|
+
end
|
54
104
|
end
|
data/spec/grape/endpoint_spec.rb
CHANGED
@@ -296,9 +296,12 @@ describe Grape::Endpoint do
|
|
296
296
|
optional :seventh
|
297
297
|
end
|
298
298
|
end
|
299
|
+
optional :nested_arr, type: Array do
|
300
|
+
optional :eighth
|
301
|
+
end
|
299
302
|
end
|
300
|
-
optional :
|
301
|
-
optional :
|
303
|
+
optional :arr, type: Array do
|
304
|
+
optional :nineth
|
302
305
|
end
|
303
306
|
end
|
304
307
|
end
|
@@ -390,7 +393,7 @@ describe Grape::Endpoint do
|
|
390
393
|
|
391
394
|
get '/declared?first=present&nested[fourth]=1'
|
392
395
|
expect(last_response.status).to eq(200)
|
393
|
-
expect(JSON.parse(last_response.body)['nested'].keys.size).to eq
|
396
|
+
expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 4
|
394
397
|
end
|
395
398
|
|
396
399
|
it 'builds nested params when given array' do
|
@@ -421,7 +424,7 @@ describe Grape::Endpoint do
|
|
421
424
|
|
422
425
|
get '/declared?first=present'
|
423
426
|
expect(last_response.status).to eq(200)
|
424
|
-
expect(JSON.parse(last_response.body)['nested']).to
|
427
|
+
expect(JSON.parse(last_response.body)['nested']).to eq({})
|
425
428
|
end
|
426
429
|
|
427
430
|
it 'to be an array when include_missing is true' do
|
@@ -431,7 +434,17 @@ describe Grape::Endpoint do
|
|
431
434
|
|
432
435
|
get '/declared?first=present'
|
433
436
|
expect(last_response.status).to eq(200)
|
434
|
-
expect(JSON.parse(last_response.body)['
|
437
|
+
expect(JSON.parse(last_response.body)['arr']).to be_a(Array)
|
438
|
+
end
|
439
|
+
|
440
|
+
it 'to be an array when nested and include_missing is true' do
|
441
|
+
subject.get '/declared' do
|
442
|
+
declared(params, include_missing: true)
|
443
|
+
end
|
444
|
+
|
445
|
+
get '/declared?first=present&nested[fourth]=1'
|
446
|
+
expect(last_response.status).to eq(200)
|
447
|
+
expect(JSON.parse(last_response.body)['nested']['nested_arr']).to be_a(Array)
|
435
448
|
end
|
436
449
|
|
437
450
|
it 'to be nil when include_missing is false' do
|
data/spec/grape/path_spec.rb
CHANGED
@@ -87,12 +87,12 @@ module Grape
|
|
87
87
|
describe '#namespace?' do
|
88
88
|
it 'is false when the namespace is nil' do
|
89
89
|
path = Path.new(anything, nil, anything)
|
90
|
-
expect(path.namespace?).to
|
90
|
+
expect(path.namespace?).to be_falsey
|
91
91
|
end
|
92
92
|
|
93
93
|
it 'is false when the namespace starts with whitespace' do
|
94
94
|
path = Path.new(anything, ' /foo', anything)
|
95
|
-
expect(path.namespace?).to
|
95
|
+
expect(path.namespace?).to be_falsey
|
96
96
|
end
|
97
97
|
|
98
98
|
it 'is false when the namespace is the root path' do
|
@@ -109,12 +109,12 @@ module Grape
|
|
109
109
|
describe '#path?' do
|
110
110
|
it 'is false when the path is nil' do
|
111
111
|
path = Path.new(nil, anything, anything)
|
112
|
-
expect(path.path?).to
|
112
|
+
expect(path.path?).to be_falsey
|
113
113
|
end
|
114
114
|
|
115
115
|
it 'is false when the path starts with whitespace' do
|
116
116
|
path = Path.new(' /foo', anything, anything)
|
117
|
-
expect(path.path?).to
|
117
|
+
expect(path.path?).to be_falsey
|
118
118
|
end
|
119
119
|
|
120
120
|
it 'is false when the path is the root path' do
|
@@ -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
|