grape 0.13.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Appraisals +9 -4
- data/CHANGELOG.md +28 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +166 -0
- data/README.md +305 -163
- data/Rakefile +30 -33
- data/UPGRADING.md +31 -0
- data/benchmark/simple.rb +27 -0
- data/gemfiles/rack_1.5.2.gemfile +13 -0
- data/gemfiles/rails_3.gemfile +2 -2
- data/gemfiles/rails_4.gemfile +1 -2
- data/grape.gemspec +5 -4
- data/lib/grape.rb +9 -5
- data/lib/grape/dsl/configuration.rb +5 -2
- data/lib/grape/dsl/helpers.rb +8 -3
- data/lib/grape/dsl/inside_route.rb +67 -44
- data/lib/grape/dsl/parameters.rb +21 -12
- data/lib/grape/dsl/request_response.rb +1 -1
- data/lib/grape/dsl/routing.rb +3 -4
- data/lib/grape/endpoint.rb +63 -28
- data/lib/grape/error_formatter/base.rb +6 -6
- data/lib/grape/exceptions/base.rb +5 -5
- data/lib/grape/exceptions/invalid_version_header.rb +10 -0
- data/lib/grape/formatter/serializable_hash.rb +3 -2
- data/lib/grape/locale/en.yml +4 -1
- data/lib/grape/middleware/auth/base.rb +2 -2
- data/lib/grape/middleware/auth/dsl.rb +1 -1
- data/lib/grape/middleware/auth/strategies.rb +1 -1
- data/lib/grape/middleware/base.rb +7 -4
- data/lib/grape/middleware/error.rb +3 -2
- data/lib/grape/middleware/filter.rb +1 -1
- data/lib/grape/middleware/formatter.rb +47 -44
- data/lib/grape/middleware/globals.rb +3 -3
- data/lib/grape/middleware/versioner/accept_version_header.rb +5 -7
- data/lib/grape/middleware/versioner/header.rb +113 -50
- data/lib/grape/middleware/versioner/param.rb +5 -8
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +20 -0
- data/lib/grape/middleware/versioner/path.rb +3 -6
- data/lib/grape/path.rb +3 -3
- data/lib/grape/request.rb +40 -0
- data/lib/grape/util/content_types.rb +9 -9
- data/lib/grape/util/env.rb +22 -0
- data/lib/grape/util/strict_hash_configuration.rb +2 -1
- data/lib/grape/validations/attributes_iterator.rb +8 -3
- data/lib/grape/validations/params_scope.rb +83 -15
- data/lib/grape/validations/types.rb +144 -0
- data/lib/grape/validations/types/build_coercer.rb +53 -0
- data/lib/grape/validations/types/custom_type_coercer.rb +183 -0
- data/lib/grape/validations/types/file.rb +28 -0
- data/lib/grape/validations/types/json.rb +65 -0
- data/lib/grape/validations/types/multiple_type_coercer.rb +76 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +59 -0
- data/lib/grape/validations/types/virtus_collection_patch.rb +16 -0
- data/lib/grape/validations/validators/all_or_none.rb +1 -1
- data/lib/grape/validations/validators/allow_blank.rb +3 -3
- data/lib/grape/validations/validators/base.rb +7 -0
- data/lib/grape/validations/validators/coerce.rb +31 -42
- data/lib/grape/validations/validators/presence.rb +2 -3
- data/lib/grape/validations/validators/regexp.rb +2 -4
- data/lib/grape/validations/validators/values.rb +3 -3
- data/lib/grape/version.rb +1 -1
- data/pkg/grape-0.13.0.gem +0 -0
- data/spec/grape/api/custom_validations_spec.rb +5 -4
- data/spec/grape/api/deeply_included_options_spec.rb +7 -7
- data/spec/grape/api/nested_helpers_spec.rb +4 -2
- data/spec/grape/api/shared_helpers_spec.rb +8 -8
- data/spec/grape/api_spec.rb +88 -54
- data/spec/grape/dsl/configuration_spec.rb +13 -0
- data/spec/grape/dsl/helpers_spec.rb +16 -2
- data/spec/grape/dsl/inside_route_spec.rb +3 -2
- data/spec/grape/dsl/parameters_spec.rb +0 -6
- data/spec/grape/dsl/routing_spec.rb +1 -1
- data/spec/grape/endpoint_spec.rb +61 -20
- data/spec/grape/entity_spec.rb +10 -8
- data/spec/grape/exceptions/invalid_accept_header_spec.rb +1 -15
- data/spec/grape/integration/rack_spec.rb +3 -2
- data/spec/grape/middleware/base_spec.rb +7 -5
- data/spec/grape/middleware/error_spec.rb +16 -15
- data/spec/grape/middleware/exception_spec.rb +45 -43
- data/spec/grape/middleware/formatter_spec.rb +34 -0
- data/spec/grape/middleware/versioner/header_spec.rb +79 -47
- data/spec/grape/path_spec.rb +10 -10
- data/spec/grape/presenters/presenter_spec.rb +2 -2
- data/spec/grape/request_spec.rb +100 -0
- data/spec/grape/validations/params_scope_spec.rb +11 -9
- data/spec/grape/validations/types_spec.rb +95 -0
- data/spec/grape/validations/validators/coerce_spec.rb +335 -2
- data/spec/grape/validations/validators/values_spec.rb +15 -15
- data/spec/grape/validations_spec.rb +53 -24
- data/spec/shared/versioning_examples.rb +2 -2
- data/spec/spec_helper.rb +0 -1
- data/spec/support/versioned_helpers.rb +2 -2
- metadata +51 -13
- data/.gitignore +0 -46
- data/.rspec +0 -2
- data/.rubocop.yml +0 -7
- data/.rubocop_todo.yml +0 -84
- data/.travis.yml +0 -20
- data/.yardopts +0 -2
- data/lib/grape/http/request.rb +0 -35
- data/lib/grape/util/parameter_types.rb +0 -58
- data/spec/grape/util/parameter_types_spec.rb +0 -54
data/spec/grape/path_spec.rb
CHANGED
@@ -82,47 +82,47 @@ module Grape
|
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
|
-
describe '#
|
85
|
+
describe '#namespace?' do
|
86
86
|
it 'is false when the namespace is nil' do
|
87
87
|
path = Path.new(anything, nil, anything)
|
88
|
-
expect(path).
|
88
|
+
expect(path.namespace?).to be nil
|
89
89
|
end
|
90
90
|
|
91
91
|
it 'is false when the namespace starts with whitespace' do
|
92
92
|
path = Path.new(anything, ' /foo', anything)
|
93
|
-
expect(path).
|
93
|
+
expect(path.namespace?).to be nil
|
94
94
|
end
|
95
95
|
|
96
96
|
it 'is false when the namespace is the root path' do
|
97
97
|
path = Path.new(anything, '/', anything)
|
98
|
-
expect(path).
|
98
|
+
expect(path.namespace?).to be false
|
99
99
|
end
|
100
100
|
|
101
101
|
it 'is true otherwise' do
|
102
102
|
path = Path.new(anything, '/world', anything)
|
103
|
-
expect(path).to
|
103
|
+
expect(path.namespace?).to be true
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
107
|
-
describe '#
|
107
|
+
describe '#path?' do
|
108
108
|
it 'is false when the path is nil' do
|
109
109
|
path = Path.new(nil, anything, anything)
|
110
|
-
expect(path).
|
110
|
+
expect(path.path?).to be nil
|
111
111
|
end
|
112
112
|
|
113
113
|
it 'is false when the path starts with whitespace' do
|
114
114
|
path = Path.new(' /foo', anything, anything)
|
115
|
-
expect(path).
|
115
|
+
expect(path.path?).to be nil
|
116
116
|
end
|
117
117
|
|
118
118
|
it 'is false when the path is the root path' do
|
119
119
|
path = Path.new('/', anything, anything)
|
120
|
-
expect(path).
|
120
|
+
expect(path.path?).to be false
|
121
121
|
end
|
122
122
|
|
123
123
|
it 'is true otherwise' do
|
124
124
|
path = Path.new('/hello', anything, anything)
|
125
|
-
expect(path).to
|
125
|
+
expect(path.path?).to be true
|
126
126
|
end
|
127
127
|
end
|
128
128
|
|
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module Grape
|
4
4
|
module Presenters
|
5
|
-
module
|
5
|
+
module PresenterSpec
|
6
6
|
class Dummy
|
7
7
|
include Grape::DSL::InsideRoute
|
8
8
|
|
@@ -27,7 +27,7 @@ module Grape
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
subject {
|
30
|
+
subject { PresenterSpec::Dummy.new }
|
31
31
|
|
32
32
|
describe 'present' do
|
33
33
|
let(:hash_mock) do
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
describe Request do
|
5
|
+
let(:default_method) { 'GET' }
|
6
|
+
let(:default_params) { {} }
|
7
|
+
let(:default_options) {
|
8
|
+
{
|
9
|
+
method: method,
|
10
|
+
params: params
|
11
|
+
}
|
12
|
+
}
|
13
|
+
let(:default_env) {
|
14
|
+
Rack::MockRequest.env_for('/', options)
|
15
|
+
}
|
16
|
+
let(:method) { default_method }
|
17
|
+
let(:params) { default_params }
|
18
|
+
let(:options) { default_options }
|
19
|
+
let(:env) { default_env }
|
20
|
+
|
21
|
+
let(:request) {
|
22
|
+
Grape::Request.new(env)
|
23
|
+
}
|
24
|
+
|
25
|
+
describe '#params' do
|
26
|
+
let(:params) {
|
27
|
+
{
|
28
|
+
a: '123',
|
29
|
+
b: 'xyz'
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
it 'returns params' do
|
34
|
+
expect(request.params).to eq('a' => '123', 'b' => 'xyz')
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'with rack.routing_args' do
|
38
|
+
let(:options) {
|
39
|
+
default_options.merge('rack.routing_args' => routing_args)
|
40
|
+
}
|
41
|
+
let(:routing_args) {
|
42
|
+
{
|
43
|
+
version: '123',
|
44
|
+
route_info: '456',
|
45
|
+
c: 'ccc'
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
it 'cuts version and route_info' do
|
50
|
+
expect(request.params).to eq('a' => '123', 'b' => 'xyz', 'c' => 'ccc')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#headers' do
|
56
|
+
let(:options) {
|
57
|
+
default_options.merge(request_headers)
|
58
|
+
}
|
59
|
+
|
60
|
+
describe 'with http headers in env' do
|
61
|
+
let(:request_headers) {
|
62
|
+
{
|
63
|
+
'HTTP_X_GRAPE_IS_COOL' => 'yeah'
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
it 'cuts HTTP_ prefix and capitalizes header name words' do
|
68
|
+
expect(request.headers).to eq('X-Grape-Is-Cool' => 'yeah')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'with non-HTTP_* stuff in env' do
|
73
|
+
let(:request_headers) {
|
74
|
+
{
|
75
|
+
'HTP_X_GRAPE_ENTITY_TOO' => 'but now we are testing Grape'
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
it 'does not include them' do
|
80
|
+
expect(request.headers).to eq({})
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'with symbolic header names' do
|
85
|
+
let(:request_headers) {
|
86
|
+
{
|
87
|
+
HTTP_GRAPE_LIKES_SYMBOLIC: 'it is true'
|
88
|
+
}
|
89
|
+
}
|
90
|
+
let(:env) {
|
91
|
+
default_env.merge(request_headers)
|
92
|
+
}
|
93
|
+
|
94
|
+
it 'converts them to string' do
|
95
|
+
expect(request.headers).to eq('Grape-Likes-Symbolic' => 'it is true')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -90,21 +90,23 @@ describe Grape::Validations::ParamsScope do
|
|
90
90
|
end
|
91
91
|
|
92
92
|
context 'when using custom types' do
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
93
|
+
module ParamsScopeSpec
|
94
|
+
class CustomType
|
95
|
+
attr_reader :value
|
96
|
+
def self.parse(value)
|
97
|
+
fail if value == 'invalid'
|
98
|
+
new(value)
|
99
|
+
end
|
99
100
|
|
100
|
-
|
101
|
-
|
101
|
+
def initialize(value)
|
102
|
+
@value = value
|
103
|
+
end
|
102
104
|
end
|
103
105
|
end
|
104
106
|
|
105
107
|
it 'coerces the parameter via the type\'s parse method' do
|
106
108
|
subject.params do
|
107
|
-
requires :foo, type: CustomType
|
109
|
+
requires :foo, type: ParamsScopeSpec::CustomType
|
108
110
|
end
|
109
111
|
subject.get('/types') { params[:foo].value }
|
110
112
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grape::Validations::Types do
|
4
|
+
module TypesSpec
|
5
|
+
class FooType
|
6
|
+
def self.parse(_)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class BarType
|
11
|
+
def self.parse
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
VirtusA = Virtus::Attribute.build(String)
|
17
|
+
|
18
|
+
module VirtusModule
|
19
|
+
include Virtus.module
|
20
|
+
end
|
21
|
+
|
22
|
+
class VirtusB
|
23
|
+
include VirtusModule
|
24
|
+
end
|
25
|
+
|
26
|
+
class VirtusC
|
27
|
+
include Virtus.model
|
28
|
+
end
|
29
|
+
|
30
|
+
MyAxiom = Axiom::Types::String.new do
|
31
|
+
minimum_length 1
|
32
|
+
maximum_length 30
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '::primitive?' do
|
36
|
+
[
|
37
|
+
Integer, Float, Numeric, BigDecimal,
|
38
|
+
Virtus::Attribute::Boolean, String, Symbol,
|
39
|
+
Date, DateTime, Time, Rack::Multipart::UploadedFile
|
40
|
+
].each do |type|
|
41
|
+
it "recognizes #{type} as a primitive" do
|
42
|
+
expect(described_class.primitive?(type)).to be_truthy
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'identifies unknown types' do
|
47
|
+
expect(described_class.primitive?(Object)).to be_falsy
|
48
|
+
expect(described_class.primitive?(TypesSpec::FooType)).to be_falsy
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '::structure?' do
|
53
|
+
[
|
54
|
+
Hash, Array, Set
|
55
|
+
].each do |type|
|
56
|
+
it "recognizes #{type} as a structure" do
|
57
|
+
expect(described_class.structure?(type)).to be_truthy
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '::recognized?' do
|
63
|
+
[
|
64
|
+
VirtusA, VirtusB, VirtusC, MyAxiom
|
65
|
+
].each do |type|
|
66
|
+
it "recognizes #{type}" do
|
67
|
+
expect(described_class.recognized?(type)).to be_truthy
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '::special?' do
|
73
|
+
[
|
74
|
+
JSON, Array[JSON], File, Rack::Multipart::UploadedFile
|
75
|
+
].each do |type|
|
76
|
+
it "provides special handling for #{type.inspect}" do
|
77
|
+
expect(described_class.special?(type)).to be_truthy
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '::custom?' do
|
83
|
+
it 'returns false if the type does not respond to :parse' do
|
84
|
+
expect(described_class.custom?(Object)).to be_falsy
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'returns true if the type responds to :parse with one argument' do
|
88
|
+
expect(described_class.custom?(TypesSpec::FooType)).to be_truthy
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'returns false if the type\'s #parse method takes other than one argument' do
|
92
|
+
expect(described_class.custom?(TypesSpec::BarType)).to be_falsy
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -224,9 +224,9 @@ describe Grape::Validations::CoerceValidator do
|
|
224
224
|
expect(last_response.body).to eq('TrueClass')
|
225
225
|
end
|
226
226
|
|
227
|
-
it '
|
227
|
+
it 'Rack::Multipart::UploadedFile' do
|
228
228
|
subject.params do
|
229
|
-
requires :file,
|
229
|
+
requires :file, type: Rack::Multipart::UploadedFile
|
230
230
|
end
|
231
231
|
subject.post '/upload' do
|
232
232
|
params[:file].filename
|
@@ -235,6 +235,27 @@ describe Grape::Validations::CoerceValidator do
|
|
235
235
|
post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
|
236
236
|
expect(last_response.status).to eq(201)
|
237
237
|
expect(last_response.body).to eq(File.basename(__FILE__).to_s)
|
238
|
+
|
239
|
+
post '/upload', file: 'not a file'
|
240
|
+
expect(last_response.status).to eq(400)
|
241
|
+
expect(last_response.body).to eq('file is invalid')
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'File' do
|
245
|
+
subject.params do
|
246
|
+
requires :file, coerce: File
|
247
|
+
end
|
248
|
+
subject.post '/upload' do
|
249
|
+
params[:file].filename
|
250
|
+
end
|
251
|
+
|
252
|
+
post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
|
253
|
+
expect(last_response.status).to eq(201)
|
254
|
+
expect(last_response.body).to eq(File.basename(__FILE__).to_s)
|
255
|
+
|
256
|
+
post '/upload', file: 'not a file'
|
257
|
+
expect(last_response.status).to eq(400)
|
258
|
+
expect(last_response.body).to eq('file is invalid')
|
238
259
|
end
|
239
260
|
|
240
261
|
it 'Nests integers' do
|
@@ -252,5 +273,317 @@ describe Grape::Validations::CoerceValidator do
|
|
252
273
|
expect(last_response.body).to eq('Fixnum')
|
253
274
|
end
|
254
275
|
end
|
276
|
+
|
277
|
+
context 'using coerce_with' do
|
278
|
+
it 'uses parse where available' do
|
279
|
+
subject.params do
|
280
|
+
requires :ints, type: Array, coerce_with: JSON do
|
281
|
+
requires :i, type: Integer
|
282
|
+
requires :j
|
283
|
+
end
|
284
|
+
end
|
285
|
+
subject.get '/ints' do
|
286
|
+
ints = params[:ints].first
|
287
|
+
'coercion works' if ints[:i] == 1 && ints[:j] == '2'
|
288
|
+
end
|
289
|
+
|
290
|
+
get '/ints', ints: [{ i: 1, j: '2' }]
|
291
|
+
expect(last_response.status).to eq(400)
|
292
|
+
expect(last_response.body).to eq('ints is invalid')
|
293
|
+
|
294
|
+
get '/ints', ints: '{"i":1,"j":"2"}'
|
295
|
+
expect(last_response.status).to eq(400)
|
296
|
+
expect(last_response.body).to eq('ints[0][i] is missing, ints[0][i] is invalid, ints[0][j] is missing')
|
297
|
+
|
298
|
+
get '/ints', ints: '[{"i":"1","j":"2"}]'
|
299
|
+
expect(last_response.status).to eq(200)
|
300
|
+
expect(last_response.body).to eq('coercion works')
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'accepts any callable' do
|
304
|
+
subject.params do
|
305
|
+
requires :ints, type: Hash, coerce_with: JSON.method(:parse) do
|
306
|
+
requires :int, type: Integer, coerce_with: ->(val) { val == 'three' ? 3 : val }
|
307
|
+
end
|
308
|
+
end
|
309
|
+
subject.get '/ints' do
|
310
|
+
params[:ints][:int]
|
311
|
+
end
|
312
|
+
|
313
|
+
get '/ints', ints: '{"int":"3"}'
|
314
|
+
expect(last_response.status).to eq(400)
|
315
|
+
expect(last_response.body).to eq('ints[int] is invalid')
|
316
|
+
|
317
|
+
get '/ints', ints: '{"int":"three"}'
|
318
|
+
expect(last_response.status).to eq(200)
|
319
|
+
expect(last_response.body).to eq('3')
|
320
|
+
|
321
|
+
get '/ints', ints: '{"int":3}'
|
322
|
+
expect(last_response.status).to eq(200)
|
323
|
+
expect(last_response.body).to eq('3')
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'must be supplied with :type or :coerce' do
|
327
|
+
expect do
|
328
|
+
subject.params do
|
329
|
+
requires :ints, coerce_with: JSON
|
330
|
+
end
|
331
|
+
end.to raise_error(ArgumentError)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
context 'first-class JSON' do
|
336
|
+
it 'parses objects and arrays' do
|
337
|
+
subject.params do
|
338
|
+
requires :splines, type: JSON do
|
339
|
+
requires :x, type: Integer, values: [1, 2, 3]
|
340
|
+
optional :ints, type: Array[Integer]
|
341
|
+
optional :obj, type: Hash do
|
342
|
+
optional :y
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
subject.get '/' do
|
347
|
+
if params[:splines].is_a? Hash
|
348
|
+
params[:splines][:obj][:y]
|
349
|
+
else
|
350
|
+
'arrays work' if params[:splines].any? { |s| s.key? :obj }
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
get '/', splines: '{"x":1,"ints":[1,2,3],"obj":{"y":"woof"}}'
|
355
|
+
expect(last_response.status).to eq(200)
|
356
|
+
expect(last_response.body).to eq('woof')
|
357
|
+
|
358
|
+
get '/', splines: '[{"x":2,"ints":[]},{"x":3,"ints":[4],"obj":{"y":"quack"}}]'
|
359
|
+
expect(last_response.status).to eq(200)
|
360
|
+
expect(last_response.body).to eq('arrays work')
|
361
|
+
|
362
|
+
get '/', splines: '{"x":4,"ints":[2]}'
|
363
|
+
expect(last_response.status).to eq(400)
|
364
|
+
expect(last_response.body).to eq('splines[x] does not have a valid value')
|
365
|
+
|
366
|
+
get '/', splines: '[{"x":1,"ints":[]},{"x":4,"ints":[]}]'
|
367
|
+
expect(last_response.status).to eq(400)
|
368
|
+
expect(last_response.body).to eq('splines[x] does not have a valid value')
|
369
|
+
end
|
370
|
+
|
371
|
+
it 'accepts Array[JSON] shorthand' do
|
372
|
+
subject.params do
|
373
|
+
requires :splines, type: Array[JSON] do
|
374
|
+
requires :x, type: Integer, values: [1, 2, 3]
|
375
|
+
requires :y
|
376
|
+
end
|
377
|
+
end
|
378
|
+
subject.get '/' do
|
379
|
+
params[:splines].first[:y].class.to_s
|
380
|
+
spline = params[:splines].first
|
381
|
+
"#{spline[:x].class}.#{spline[:y].class}"
|
382
|
+
end
|
383
|
+
|
384
|
+
get '/', splines: '{"x":"1","y":"woof"}'
|
385
|
+
expect(last_response.status).to eq(200)
|
386
|
+
expect(last_response.body).to eq('Fixnum.String')
|
387
|
+
|
388
|
+
get '/', splines: '[{"x":1,"y":2},{"x":1,"y":"quack"}]'
|
389
|
+
expect(last_response.status).to eq(200)
|
390
|
+
expect(last_response.body).to eq('Fixnum.Fixnum')
|
391
|
+
|
392
|
+
get '/', splines: '{"x":"4","y":"woof"}'
|
393
|
+
expect(last_response.status).to eq(400)
|
394
|
+
expect(last_response.body).to eq('splines[x] does not have a valid value')
|
395
|
+
|
396
|
+
get '/', splines: '[{"x":"4","y":"woof"}]'
|
397
|
+
expect(last_response.status).to eq(400)
|
398
|
+
expect(last_response.body).to eq('splines[x] does not have a valid value')
|
399
|
+
end
|
400
|
+
|
401
|
+
it "doesn't make sense using coerce_with" do
|
402
|
+
expect do
|
403
|
+
subject.params do
|
404
|
+
requires :bad, type: JSON, coerce_with: JSON do
|
405
|
+
requires :x
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end.to raise_error(ArgumentError)
|
409
|
+
|
410
|
+
expect do
|
411
|
+
subject.params do
|
412
|
+
requires :bad, type: Array[JSON], coerce_with: JSON do
|
413
|
+
requires :x
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end.to raise_error(ArgumentError)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
context 'multiple types' do
|
421
|
+
Boolean = Grape::API::Boolean
|
422
|
+
|
423
|
+
it 'coerces to first possible type' do
|
424
|
+
subject.params do
|
425
|
+
requires :a, types: [Boolean, Integer, String]
|
426
|
+
end
|
427
|
+
subject.get '/' do
|
428
|
+
params[:a].class.to_s
|
429
|
+
end
|
430
|
+
|
431
|
+
get '/', a: 'true'
|
432
|
+
expect(last_response.status).to eq(200)
|
433
|
+
expect(last_response.body).to eq('TrueClass')
|
434
|
+
|
435
|
+
get '/', a: '5'
|
436
|
+
expect(last_response.status).to eq(200)
|
437
|
+
expect(last_response.body).to eq('Fixnum')
|
438
|
+
|
439
|
+
get '/', a: 'anything else'
|
440
|
+
expect(last_response.status).to eq(200)
|
441
|
+
expect(last_response.body).to eq('String')
|
442
|
+
end
|
443
|
+
|
444
|
+
it 'fails when no coercion is possible' do
|
445
|
+
subject.params do
|
446
|
+
requires :a, types: [Boolean, Integer]
|
447
|
+
end
|
448
|
+
subject.get '/' do
|
449
|
+
params[:a].class.to_s
|
450
|
+
end
|
451
|
+
|
452
|
+
get '/', a: true
|
453
|
+
expect(last_response.status).to eq(200)
|
454
|
+
expect(last_response.body).to eq('TrueClass')
|
455
|
+
|
456
|
+
get '/', a: 'not good'
|
457
|
+
expect(last_response.status).to eq(400)
|
458
|
+
expect(last_response.body).to eq('a is invalid')
|
459
|
+
end
|
460
|
+
|
461
|
+
context 'for primitive collections' do
|
462
|
+
before do
|
463
|
+
subject.params do
|
464
|
+
optional :a, types: [String, Array[String]]
|
465
|
+
optional :b, types: [Array[Integer], Array[String]]
|
466
|
+
optional :c, type: Array[Integer, String]
|
467
|
+
optional :d, types: [Integer, String, Set[Integer, String]]
|
468
|
+
end
|
469
|
+
subject.get '/' do
|
470
|
+
(
|
471
|
+
params[:a] ||
|
472
|
+
params[:b] ||
|
473
|
+
params[:c] ||
|
474
|
+
params[:d]
|
475
|
+
).inspect
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
it 'allows singular form declaration' do
|
480
|
+
get '/', a: 'one way'
|
481
|
+
expect(last_response.status).to eq(200)
|
482
|
+
expect(last_response.body).to eq('"one way"')
|
483
|
+
|
484
|
+
get '/', a: %w(the other)
|
485
|
+
expect(last_response.status).to eq(200)
|
486
|
+
expect(last_response.body).to eq('["the", "other"]')
|
487
|
+
|
488
|
+
get '/', a: { a: 1, b: 2 }
|
489
|
+
expect(last_response.status).to eq(400)
|
490
|
+
expect(last_response.body).to eq('a is invalid')
|
491
|
+
|
492
|
+
get '/', a: [1, 2, 3]
|
493
|
+
expect(last_response.status).to eq(200)
|
494
|
+
expect(last_response.body).to eq('["1", "2", "3"]')
|
495
|
+
end
|
496
|
+
|
497
|
+
it 'allows multiple collection types' do
|
498
|
+
get '/', b: [1, 2, 3]
|
499
|
+
expect(last_response.status).to eq(200)
|
500
|
+
expect(last_response.body).to eq('[1, 2, 3]')
|
501
|
+
|
502
|
+
get '/', b: %w(1 2 3)
|
503
|
+
expect(last_response.status).to eq(200)
|
504
|
+
expect(last_response.body).to eq('[1, 2, 3]')
|
505
|
+
|
506
|
+
get '/', b: [1, true, 'three']
|
507
|
+
expect(last_response.status).to eq(200)
|
508
|
+
expect(last_response.body).to eq('["1", "true", "three"]')
|
509
|
+
end
|
510
|
+
|
511
|
+
it 'allows collections with multiple types' do
|
512
|
+
get '/', c: [1, '2', true, 'three']
|
513
|
+
expect(last_response.status).to eq(200)
|
514
|
+
expect(last_response.body).to eq('[1, 2, "true", "three"]')
|
515
|
+
|
516
|
+
get '/', d: '1'
|
517
|
+
expect(last_response.status).to eq(200)
|
518
|
+
expect(last_response.body).to eq('1')
|
519
|
+
|
520
|
+
get '/', d: 'one'
|
521
|
+
expect(last_response.status).to eq(200)
|
522
|
+
expect(last_response.body).to eq('"one"')
|
523
|
+
|
524
|
+
get '/', d: %w(1 two)
|
525
|
+
expect(last_response.status).to eq(200)
|
526
|
+
expect(last_response.body).to eq('#<Set: {1, "two"}>')
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
context 'custom coercion rules' do
|
531
|
+
before do
|
532
|
+
subject.params do
|
533
|
+
requires :a, types: [Boolean, String], coerce_with: (lambda do |val|
|
534
|
+
if val == 'yup'
|
535
|
+
true
|
536
|
+
elsif val == 'false'
|
537
|
+
0
|
538
|
+
else
|
539
|
+
val
|
540
|
+
end
|
541
|
+
end)
|
542
|
+
end
|
543
|
+
subject.get '/' do
|
544
|
+
params[:a].class.to_s
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
it 'respects :coerce_with' do
|
549
|
+
get '/', a: 'yup'
|
550
|
+
expect(last_response.status).to eq(200)
|
551
|
+
expect(last_response.body).to eq('TrueClass')
|
552
|
+
end
|
553
|
+
|
554
|
+
it 'still validates type' do
|
555
|
+
get '/', a: 'false'
|
556
|
+
expect(last_response.status).to eq(400)
|
557
|
+
expect(last_response.body).to eq('a is invalid')
|
558
|
+
end
|
559
|
+
|
560
|
+
it 'performs no additional coercion' do
|
561
|
+
get '/', a: 'true'
|
562
|
+
expect(last_response.status).to eq(200)
|
563
|
+
expect(last_response.body).to eq('String')
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
it 'may not be supplied together with a single type' do
|
568
|
+
expect do
|
569
|
+
subject.params do
|
570
|
+
requires :a, type: Integer, types: [Integer, String]
|
571
|
+
end
|
572
|
+
end.to raise_exception ArgumentError
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
context 'converter' do
|
577
|
+
it 'does not build Virtus::Attribute multiple times' do
|
578
|
+
subject.params do
|
579
|
+
requires :something, type: Array[String]
|
580
|
+
end
|
581
|
+
subject.get do
|
582
|
+
end
|
583
|
+
|
584
|
+
expect(Virtus::Attribute).to receive(:build).at_most(2).times.and_call_original
|
585
|
+
10.times { get '/' }
|
586
|
+
end
|
587
|
+
end
|
255
588
|
end
|
256
589
|
end
|