grape 0.18.0 → 0.19.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/CHANGELOG.md +52 -69
- data/Gemfile +3 -3
- data/Gemfile.lock +37 -41
- data/README.md +49 -7
- data/UPGRADING.md +56 -1
- data/gemfiles/rack_edge.gemfile +34 -0
- data/gemfiles/rails_5.gemfile +1 -1
- data/gemfiles/rails_edge.gemfile +34 -0
- data/lib/grape/api.rb +1 -1
- data/lib/grape/dsl/helpers.rb +1 -0
- data/lib/grape/dsl/inside_route.rb +2 -0
- data/lib/grape/dsl/parameters.rb +24 -12
- data/lib/grape/dsl/request_response.rb +2 -3
- data/lib/grape/dsl/routing.rb +4 -0
- data/lib/grape/endpoint.rb +15 -16
- data/lib/grape/error_formatter/base.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +2 -2
- data/lib/grape/http/headers.rb +2 -0
- data/lib/grape/middleware/base.rb +3 -4
- data/lib/grape/middleware/error.rb +1 -1
- data/lib/grape/middleware/versioner/path.rb +1 -1
- data/lib/grape/router.rb +37 -21
- data/lib/grape/router/attribute_translator.rb +1 -1
- data/lib/grape/router/pattern.rb +9 -1
- data/lib/grape/router/route.rb +4 -0
- data/lib/grape/validations/params_scope.rb +24 -6
- data/lib/grape/validations/validators/base.rb +1 -2
- data/lib/grape/validations/validators/regexp.rb +2 -1
- data/lib/grape/version.rb +1 -1
- data/pkg/grape-0.17.0.gem +0 -0
- data/spec/grape/api/patch_method_helpers_spec.rb +1 -2
- data/spec/grape/api_spec.rb +87 -21
- data/spec/grape/dsl/desc_spec.rb +2 -4
- data/spec/grape/dsl/inside_route_spec.rb +29 -22
- data/spec/grape/dsl/parameters_spec.rb +15 -1
- data/spec/grape/endpoint_spec.rb +53 -19
- data/spec/grape/middleware/formatter_spec.rb +39 -30
- data/spec/grape/middleware/versioner/param_spec.rb +15 -10
- data/spec/grape/middleware/versioner/path_spec.rb +4 -3
- data/spec/grape/util/inheritable_setting_spec.rb +2 -1
- data/spec/grape/util/strict_hash_configuration_spec.rb +1 -2
- data/spec/grape/validations/params_scope_spec.rb +182 -0
- data/spec/grape/validations/validators/default_spec.rb +1 -3
- data/spec/grape/validations/validators/presence_spec.rb +29 -1
- data/spec/grape/validations/validators/regexp_spec.rb +88 -0
- metadata +5 -3
- data/pkg/grape-0.18.0.gem +0 -0
data/spec/grape/dsl/desc_spec.rb
CHANGED
@@ -34,8 +34,7 @@ module Grape
|
|
34
34
|
XOptionalHeader: {
|
35
35
|
description: 'Not really needed',
|
36
36
|
required: false
|
37
|
-
}
|
38
|
-
]
|
37
|
+
}]
|
39
38
|
}
|
40
39
|
|
41
40
|
subject.desc 'The description' do
|
@@ -51,8 +50,7 @@ module Grape
|
|
51
50
|
XOptionalHeader: {
|
52
51
|
description: 'Not really needed',
|
53
52
|
required: false
|
54
|
-
}
|
55
|
-
]
|
53
|
+
}]
|
56
54
|
end
|
57
55
|
|
58
56
|
expect(subject.namespace_setting(:description)).to eq(expected_options)
|
@@ -91,7 +91,7 @@ describe Grape::Endpoint do
|
|
91
91
|
end
|
92
92
|
|
93
93
|
describe '#status' do
|
94
|
-
%w(GET PUT
|
94
|
+
%w(GET PUT OPTIONS).each do |method|
|
95
95
|
it 'defaults to 200 on GET' do
|
96
96
|
request = Grape::Request.new(Rack::MockRequest.env_for('/', method: method))
|
97
97
|
expect(subject).to receive(:request).and_return(request)
|
@@ -105,6 +105,12 @@ describe Grape::Endpoint do
|
|
105
105
|
expect(subject.status).to eq 201
|
106
106
|
end
|
107
107
|
|
108
|
+
it 'defaults to 204 on DELETE' do
|
109
|
+
request = Grape::Request.new(Rack::MockRequest.env_for('/', method: 'DELETE'))
|
110
|
+
expect(subject).to receive(:request).and_return(request)
|
111
|
+
expect(subject.status).to eq 204
|
112
|
+
end
|
113
|
+
|
108
114
|
it 'returns status set' do
|
109
115
|
subject.status 501
|
110
116
|
expect(subject.status).to eq 501
|
@@ -302,22 +308,22 @@ describe Grape::Endpoint do
|
|
302
308
|
end
|
303
309
|
|
304
310
|
describe 'multiple entities' do
|
305
|
-
let(:
|
306
|
-
|
307
|
-
allow(
|
308
|
-
|
311
|
+
let(:entity_mock_one) do
|
312
|
+
entity_mock_one = Object.new
|
313
|
+
allow(entity_mock_one).to receive(:represent).and_return(dummy1: 'dummy1')
|
314
|
+
entity_mock_one
|
309
315
|
end
|
310
316
|
|
311
|
-
let(:
|
312
|
-
|
313
|
-
allow(
|
314
|
-
|
317
|
+
let(:entity_mock_two) do
|
318
|
+
entity_mock_two = Object.new
|
319
|
+
allow(entity_mock_two).to receive(:represent).and_return(dummy2: 'dummy2')
|
320
|
+
entity_mock_two
|
315
321
|
end
|
316
322
|
|
317
323
|
describe 'instance' do
|
318
324
|
before do
|
319
|
-
subject.present 'dummy1', with:
|
320
|
-
subject.present 'dummy2', with:
|
325
|
+
subject.present 'dummy1', with: entity_mock_one
|
326
|
+
subject.present 'dummy2', with: entity_mock_two
|
321
327
|
end
|
322
328
|
|
323
329
|
it 'presents both dummy objects' do
|
@@ -328,23 +334,23 @@ describe Grape::Endpoint do
|
|
328
334
|
end
|
329
335
|
|
330
336
|
describe 'non mergeable entity' do
|
331
|
-
let(:
|
332
|
-
|
333
|
-
allow(
|
334
|
-
|
337
|
+
let(:entity_mock_one) do
|
338
|
+
entity_mock_one = Object.new
|
339
|
+
allow(entity_mock_one).to receive(:represent).and_return(dummy1: 'dummy1')
|
340
|
+
entity_mock_one
|
335
341
|
end
|
336
342
|
|
337
|
-
let(:
|
338
|
-
|
339
|
-
allow(
|
340
|
-
|
343
|
+
let(:entity_mock_two) do
|
344
|
+
entity_mock_two = Object.new
|
345
|
+
allow(entity_mock_two).to receive(:represent).and_return('not a hash')
|
346
|
+
entity_mock_two
|
341
347
|
end
|
342
348
|
|
343
349
|
describe 'instance' do
|
344
350
|
it 'fails' do
|
345
|
-
subject.present 'dummy1', with:
|
351
|
+
subject.present 'dummy1', with: entity_mock_one
|
346
352
|
expect do
|
347
|
-
subject.present 'dummy2', with:
|
353
|
+
subject.present 'dummy2', with: entity_mock_two
|
348
354
|
end.to raise_error ArgumentError, 'Representation of type String cannot be merged.'
|
349
355
|
end
|
350
356
|
end
|
@@ -356,7 +362,8 @@ describe Grape::Endpoint do
|
|
356
362
|
|
357
363
|
it 'is not available by default' do
|
358
364
|
expect { subject.declared({}) }.to raise_error(
|
359
|
-
Grape::DSL::InsideRoute::MethodNotYetAvailable
|
365
|
+
Grape::DSL::InsideRoute::MethodNotYetAvailable
|
366
|
+
)
|
360
367
|
end
|
361
368
|
end
|
362
369
|
end
|
@@ -31,10 +31,15 @@ module Grape
|
|
31
31
|
@validates
|
32
32
|
end
|
33
33
|
|
34
|
+
def new_group_scope(args)
|
35
|
+
@group = args.clone.first
|
36
|
+
yield
|
37
|
+
end
|
38
|
+
|
34
39
|
def extract_message_option(attrs)
|
35
40
|
return nil unless attrs.is_a?(Array)
|
36
41
|
opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
|
37
|
-
|
42
|
+
opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
|
38
43
|
end
|
39
44
|
end
|
40
45
|
end
|
@@ -92,6 +97,15 @@ module Grape
|
|
92
97
|
end
|
93
98
|
end
|
94
99
|
|
100
|
+
describe '#with' do
|
101
|
+
it 'creates a scope with group attributes' do
|
102
|
+
subject.with(type: Integer) { subject.optional :id, desc: 'Identity.' }
|
103
|
+
|
104
|
+
expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }])
|
105
|
+
expect(subject.push_declared_params_reader).to eq([[:id]])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
95
109
|
describe '#mutually_exclusive' do
|
96
110
|
it 'adds an mutally exclusive parameter validation' do
|
97
111
|
subject.mutually_exclusive :media, :audio
|
data/spec/grape/endpoint_spec.rb
CHANGED
@@ -268,7 +268,8 @@ describe Grape::Endpoint do
|
|
268
268
|
subject.get('/declared') { declared(params) }
|
269
269
|
|
270
270
|
expect { get('/declared') }.to raise_error(
|
271
|
-
Grape::DSL::InsideRoute::MethodNotYetAvailable
|
271
|
+
Grape::DSL::InsideRoute::MethodNotYetAvailable
|
272
|
+
)
|
272
273
|
end
|
273
274
|
|
274
275
|
it 'has as many keys as there are declared params' do
|
@@ -1094,32 +1095,65 @@ describe Grape::Endpoint do
|
|
1094
1095
|
end
|
1095
1096
|
|
1096
1097
|
context 'anchoring' do
|
1097
|
-
|
1098
|
+
describe 'delete 204' do
|
1099
|
+
it 'allows for the anchoring option with a delete method' do
|
1100
|
+
subject.send(:delete, '/example', anchor: true) {}
|
1101
|
+
send(:delete, '/example/and/some/more')
|
1102
|
+
expect(last_response.status).to eql 404
|
1103
|
+
end
|
1098
1104
|
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
verb
|
1103
|
-
end
|
1104
|
-
send(verb, '/example/and/some/more')
|
1105
|
+
it 'anchors paths by default for the delete method' do
|
1106
|
+
subject.send(:delete, '/example') {}
|
1107
|
+
send(:delete, '/example/and/some/more')
|
1105
1108
|
expect(last_response.status).to eql 404
|
1106
1109
|
end
|
1107
1110
|
|
1108
|
-
it
|
1109
|
-
subject.send(
|
1110
|
-
|
1111
|
+
it 'responds to /example/and/some/more for the non-anchored delete method' do
|
1112
|
+
subject.send(:delete, '/example', anchor: false) {}
|
1113
|
+
send(:delete, '/example/and/some/more')
|
1114
|
+
expect(last_response.status).to eql 204
|
1115
|
+
expect(last_response.body).to be_empty
|
1116
|
+
end
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
describe 'delete 200, with response body' do
|
1120
|
+
it 'responds to /example/and/some/more for the non-anchored delete method' do
|
1121
|
+
subject.send(:delete, '/example', anchor: false) do
|
1122
|
+
status 200
|
1123
|
+
body 'deleted'
|
1111
1124
|
end
|
1112
|
-
send(
|
1113
|
-
expect(last_response.status).to eql
|
1125
|
+
send(:delete, '/example/and/some/more')
|
1126
|
+
expect(last_response.status).to eql 200
|
1127
|
+
expect(last_response.body).not_to be_empty
|
1114
1128
|
end
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
describe 'all other' do
|
1132
|
+
%w(post get head put options patch).each do |verb|
|
1133
|
+
it "allows for the anchoring option with a #{verb.upcase} method" do
|
1134
|
+
subject.send(verb, '/example', anchor: true) do
|
1135
|
+
verb
|
1136
|
+
end
|
1137
|
+
send(verb, '/example/and/some/more')
|
1138
|
+
expect(last_response.status).to eql 404
|
1139
|
+
end
|
1115
1140
|
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1141
|
+
it "anchors paths by default for the #{verb.upcase} method" do
|
1142
|
+
subject.send(verb, '/example') do
|
1143
|
+
verb
|
1144
|
+
end
|
1145
|
+
send(verb, '/example/and/some/more')
|
1146
|
+
expect(last_response.status).to eql 404
|
1147
|
+
end
|
1148
|
+
|
1149
|
+
it "responds to /example/and/some/more for the non-anchored #{verb.upcase} method" do
|
1150
|
+
subject.send(verb, '/example', anchor: false) do
|
1151
|
+
verb
|
1152
|
+
end
|
1153
|
+
send(verb, '/example/and/some/more')
|
1154
|
+
expect(last_response.status).to eql verb == 'post' ? 201 : 200
|
1155
|
+
expect(last_response.body).to eql verb == 'head' ? '' : verb
|
1119
1156
|
end
|
1120
|
-
send(verb, '/example/and/some/more')
|
1121
|
-
expect(last_response.status).to eql verb == 'post' ? 201 : 200
|
1122
|
-
expect(last_response.body).to eql verb == 'head' ? '' : verb
|
1123
1157
|
end
|
1124
1158
|
end
|
1125
1159
|
end
|
@@ -4,46 +4,53 @@ describe Grape::Middleware::Formatter do
|
|
4
4
|
subject { Grape::Middleware::Formatter.new(app) }
|
5
5
|
before { allow(subject).to receive(:dup).and_return(subject) }
|
6
6
|
|
7
|
-
let(:
|
7
|
+
let(:body) { { 'foo' => 'bar' } }
|
8
|
+
let(:app) { ->(_env) { [200, {}, [body]] } }
|
8
9
|
|
9
10
|
context 'serialization' do
|
11
|
+
let(:body) { { 'abc' => 'def' } }
|
10
12
|
it 'looks at the bodies for possibly serializable data' do
|
11
|
-
@body = { 'abc' => 'def' }
|
12
13
|
_, _, bodies = *subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json')
|
13
|
-
bodies.each { |b| expect(b).to eq(MultiJson.dump(
|
14
|
+
bodies.each { |b| expect(b).to eq(MultiJson.dump(body)) }
|
14
15
|
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
context 'default format' do
|
18
|
+
let(:body) { ['foo'] }
|
19
|
+
it 'calls #to_json since default format is json' do
|
20
|
+
body.instance_eval do
|
21
|
+
def to_json
|
22
|
+
'"bar"'
|
23
|
+
end
|
21
24
|
end
|
22
|
-
end
|
23
25
|
|
24
|
-
|
26
|
+
subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('"bar"') }
|
27
|
+
end
|
25
28
|
end
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
context 'jsonapi' do
|
31
|
+
let(:body) { { 'foos' => [{ 'bar' => 'baz' }] } }
|
32
|
+
it 'calls #to_json if the content type is jsonapi' do
|
33
|
+
body.instance_eval do
|
34
|
+
def to_json
|
35
|
+
'{"foos":[{"bar":"baz"}] }'
|
36
|
+
end
|
32
37
|
end
|
33
|
-
end
|
34
38
|
|
35
|
-
|
39
|
+
subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/vnd.api+json').to_a.last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') }
|
40
|
+
end
|
36
41
|
end
|
37
42
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
context 'xml' do
|
44
|
+
let(:body) { 'string' }
|
45
|
+
it 'calls #to_xml if the content type is xml' do
|
46
|
+
body.instance_eval do
|
47
|
+
def to_xml
|
48
|
+
'<bar/>'
|
49
|
+
end
|
43
50
|
end
|
44
|
-
end
|
45
51
|
|
46
|
-
|
52
|
+
subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('<bar/>') }
|
53
|
+
end
|
47
54
|
end
|
48
55
|
end
|
49
56
|
|
@@ -189,10 +196,12 @@ describe Grape::Middleware::Formatter do
|
|
189
196
|
_, _, body = subject.call('PATH_INFO' => '/info.custom')
|
190
197
|
expect(body.body).to eq(['CUSTOM FORMAT'])
|
191
198
|
end
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
199
|
+
context 'default' do
|
200
|
+
let(:body) { ['blah'] }
|
201
|
+
it 'uses default json formatter' do
|
202
|
+
_, _, body = subject.call('PATH_INFO' => '/info.json')
|
203
|
+
expect(body.body).to eq(['["blah"]'])
|
204
|
+
end
|
196
205
|
end
|
197
206
|
it 'uses custom json formatter' do
|
198
207
|
subject.options[:formatters][:json] = ->(_obj, _env) { 'CUSTOM JSON FORMAT' }
|
@@ -284,10 +293,10 @@ describe Grape::Middleware::Formatter do
|
|
284
293
|
end
|
285
294
|
|
286
295
|
context 'send file' do
|
287
|
-
let(:
|
296
|
+
let(:body) { Grape::ServeFile::FileResponse.new('file') }
|
297
|
+
let(:app) { ->(_env) { [200, {}, body] } }
|
288
298
|
|
289
299
|
it 'returns Grape::Uril::SendFileReponse' do
|
290
|
-
@body = Grape::ServeFile::FileResponse.new('file')
|
291
300
|
env = { 'PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json' }
|
292
301
|
expect(subject.call(env)).to be_a(Grape::ServeFile::SendfileResponse)
|
293
302
|
end
|
@@ -2,7 +2,8 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Grape::Middleware::Versioner::Param do
|
4
4
|
let(:app) { ->(env) { [200, env, env['api.version']] } }
|
5
|
-
|
5
|
+
let(:options) { {} }
|
6
|
+
subject { Grape::Middleware::Versioner::Param.new(app, options) }
|
6
7
|
|
7
8
|
it 'sets the API version based on the default param (apiver)' do
|
8
9
|
env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v1' })
|
@@ -22,7 +23,7 @@ describe Grape::Middleware::Versioner::Param do
|
|
22
23
|
end
|
23
24
|
|
24
25
|
context 'with specified parameter name' do
|
25
|
-
|
26
|
+
let(:options) { { version_options: { parameter: 'v' } } }
|
26
27
|
it 'sets the API version based on the custom parameter name' do
|
27
28
|
env = Rack::MockRequest.env_for('/awesome', params: { 'v' => 'v1' })
|
28
29
|
expect(subject.call(env)[1]['api.version']).to eq('v1')
|
@@ -34,7 +35,7 @@ describe Grape::Middleware::Versioner::Param do
|
|
34
35
|
end
|
35
36
|
|
36
37
|
context 'with specified versions' do
|
37
|
-
|
38
|
+
let(:options) { { versions: %w(v1 v2) } }
|
38
39
|
it 'throws an error if a non-allowed version is specified' do
|
39
40
|
env = Rack::MockRequest.env_for('/awesome', params: { 'apiver' => 'v3' })
|
40
41
|
expect(catch(:error) { subject.call(env) }[:status]).to eq(404)
|
@@ -45,13 +46,17 @@ describe Grape::Middleware::Versioner::Param do
|
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
49
|
+
context 'when no version is set' do
|
50
|
+
let(:options) do
|
51
|
+
{
|
52
|
+
versions: ['v1'],
|
53
|
+
version_options: { using: :header }
|
54
|
+
}
|
55
|
+
end
|
56
|
+
it 'returns a 200 (matches the first version found)' do
|
57
|
+
env = Rack::MockRequest.env_for('/awesome', params: {})
|
58
|
+
expect(subject.call(env).first).to eq(200)
|
59
|
+
end
|
55
60
|
end
|
56
61
|
|
57
62
|
context 'when there are multiple versions without a custom param' do
|
@@ -2,7 +2,8 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Grape::Middleware::Versioner::Path do
|
4
4
|
let(:app) { ->(env) { [200, env, env['api.version']] } }
|
5
|
-
|
5
|
+
let(:options) { {} }
|
6
|
+
subject { Grape::Middleware::Versioner::Path.new(app, options) }
|
6
7
|
|
7
8
|
it 'sets the API version based on the first path' do
|
8
9
|
expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1')
|
@@ -17,7 +18,7 @@ describe Grape::Middleware::Versioner::Path do
|
|
17
18
|
end
|
18
19
|
|
19
20
|
context 'with a pattern' do
|
20
|
-
|
21
|
+
let(:options) { { pattern: /v./i } }
|
21
22
|
it 'sets the version if it matches' do
|
22
23
|
expect(subject.call('PATH_INFO' => '/v1/awesome').last).to eq('v1')
|
23
24
|
end
|
@@ -29,7 +30,7 @@ describe Grape::Middleware::Versioner::Path do
|
|
29
30
|
|
30
31
|
[%w(v1 v2), [:v1, :v2], [:v1, 'v2'], ['v1', :v2]].each do |versions|
|
31
32
|
context "with specified versions as #{versions}" do
|
32
|
-
|
33
|
+
let(:options) { { versions: versions } }
|
33
34
|
|
34
35
|
it 'throws an error if a non-allowed version is specified' do
|
35
36
|
expect(catch(:error) { subject.call('PATH_INFO' => '/v3/awesome') }[:status]).to eq(404)
|
@@ -228,7 +228,8 @@ module Grape
|
|
228
228
|
expect(subject.to_hash).to include(global: { global_thing: :global_foo_bar })
|
229
229
|
expect(subject.to_hash).to include(namespace: { namespace_thing: :namespace_foo_bar })
|
230
230
|
expect(subject.to_hash).to include(namespace_inheritable: {
|
231
|
-
namespace_inheritable_thing: :namespace_inheritable_foo_bar
|
231
|
+
namespace_inheritable_thing: :namespace_inheritable_foo_bar
|
232
|
+
})
|
232
233
|
expect(subject.to_hash).to include(namespace_stackable: { namespace_stackable_thing: [:namespace_stackable_foo_bar, [:namespace_stackable_foo_bar]] })
|
233
234
|
expect(subject.to_hash).to include(namespace_reverse_stackable:
|
234
235
|
{ namespace_reverse_stackable_thing: [[:namespace_reverse_stackable_foo_bar], :namespace_reverse_stackable_foo_bar] })
|