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.

Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -69
  3. data/Gemfile +3 -3
  4. data/Gemfile.lock +37 -41
  5. data/README.md +49 -7
  6. data/UPGRADING.md +56 -1
  7. data/gemfiles/rack_edge.gemfile +34 -0
  8. data/gemfiles/rails_5.gemfile +1 -1
  9. data/gemfiles/rails_edge.gemfile +34 -0
  10. data/lib/grape/api.rb +1 -1
  11. data/lib/grape/dsl/helpers.rb +1 -0
  12. data/lib/grape/dsl/inside_route.rb +2 -0
  13. data/lib/grape/dsl/parameters.rb +24 -12
  14. data/lib/grape/dsl/request_response.rb +2 -3
  15. data/lib/grape/dsl/routing.rb +4 -0
  16. data/lib/grape/endpoint.rb +15 -16
  17. data/lib/grape/error_formatter/base.rb +1 -0
  18. data/lib/grape/formatter/serializable_hash.rb +2 -2
  19. data/lib/grape/http/headers.rb +2 -0
  20. data/lib/grape/middleware/base.rb +3 -4
  21. data/lib/grape/middleware/error.rb +1 -1
  22. data/lib/grape/middleware/versioner/path.rb +1 -1
  23. data/lib/grape/router.rb +37 -21
  24. data/lib/grape/router/attribute_translator.rb +1 -1
  25. data/lib/grape/router/pattern.rb +9 -1
  26. data/lib/grape/router/route.rb +4 -0
  27. data/lib/grape/validations/params_scope.rb +24 -6
  28. data/lib/grape/validations/validators/base.rb +1 -2
  29. data/lib/grape/validations/validators/regexp.rb +2 -1
  30. data/lib/grape/version.rb +1 -1
  31. data/pkg/grape-0.17.0.gem +0 -0
  32. data/spec/grape/api/patch_method_helpers_spec.rb +1 -2
  33. data/spec/grape/api_spec.rb +87 -21
  34. data/spec/grape/dsl/desc_spec.rb +2 -4
  35. data/spec/grape/dsl/inside_route_spec.rb +29 -22
  36. data/spec/grape/dsl/parameters_spec.rb +15 -1
  37. data/spec/grape/endpoint_spec.rb +53 -19
  38. data/spec/grape/middleware/formatter_spec.rb +39 -30
  39. data/spec/grape/middleware/versioner/param_spec.rb +15 -10
  40. data/spec/grape/middleware/versioner/path_spec.rb +4 -3
  41. data/spec/grape/util/inheritable_setting_spec.rb +2 -1
  42. data/spec/grape/util/strict_hash_configuration_spec.rb +1 -2
  43. data/spec/grape/validations/params_scope_spec.rb +182 -0
  44. data/spec/grape/validations/validators/default_spec.rb +1 -3
  45. data/spec/grape/validations/validators/presence_spec.rb +29 -1
  46. data/spec/grape/validations/validators/regexp_spec.rb +88 -0
  47. metadata +5 -3
  48. data/pkg/grape-0.18.0.gem +0 -0
@@ -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 DELETE OPTIONS).each do |method|
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(:entity_mock1) do
306
- entity_mock1 = Object.new
307
- allow(entity_mock1).to receive(:represent).and_return(dummy1: 'dummy1')
308
- entity_mock1
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(:entity_mock2) do
312
- entity_mock2 = Object.new
313
- allow(entity_mock2).to receive(:represent).and_return(dummy2: 'dummy2')
314
- entity_mock2
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: entity_mock1
320
- subject.present 'dummy2', with: entity_mock2
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(:entity_mock1) do
332
- entity_mock1 = Object.new
333
- allow(entity_mock1).to receive(:represent).and_return(dummy1: 'dummy1')
334
- entity_mock1
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(:entity_mock2) do
338
- entity_mock2 = Object.new
339
- allow(entity_mock2).to receive(:represent).and_return('not a hash')
340
- entity_mock2
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: entity_mock1
351
+ subject.present 'dummy1', with: entity_mock_one
346
352
  expect do
347
- subject.present 'dummy2', with: entity_mock2
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
- (opts.key?(:message) && !opts[:message].nil?) ? opts.delete(:message) : nil
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
@@ -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
- verbs = %w(post get head delete put options patch)
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
- verbs.each do |verb|
1100
- it "allows for the anchoring option with a #{verb.upcase} method" do
1101
- subject.send(verb, '/example', anchor: true) do
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 "anchors paths by default for the #{verb.upcase} method" do
1109
- subject.send(verb, '/example') do
1110
- verb
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(verb, '/example/and/some/more')
1113
- expect(last_response.status).to eql 404
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
- it "responds to /example/and/some/more for the non-anchored #{verb.upcase} method" do
1117
- subject.send(verb, '/example', anchor: false) do
1118
- verb
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(:app) { ->(_env) { [200, {}, [@body || { 'foo' => 'bar' }]] } }
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(@body)) }
14
+ bodies.each { |b| expect(b).to eq(MultiJson.dump(body)) }
14
15
  end
15
16
 
16
- it 'calls #to_json since default format is json' do
17
- @body = ['foo']
18
- @body.instance_eval do
19
- def to_json
20
- '"bar"'
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
- subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('"bar"') }
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
- it 'calls #to_json if the content type is jsonapi' do
28
- @body = { 'foos' => [{ 'bar' => 'baz' }] }
29
- @body.instance_eval do
30
- def to_json
31
- '{"foos":[{"bar":"baz"}] }'
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
- subject.call('PATH_INFO' => '/somewhere', 'HTTP_ACCEPT' => 'application/vnd.api+json').to_a.last.each { |b| expect(b).to eq('{"foos":[{"bar":"baz"}] }') }
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
- it 'calls #to_xml if the content type is xml' do
39
- @body = 'string'
40
- @body.instance_eval do
41
- def to_xml
42
- '<bar/>'
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
- subject.call('PATH_INFO' => '/somewhere.xml', 'HTTP_ACCEPT' => 'application/json').to_a.last.each { |b| expect(b).to eq('<bar/>') }
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
- it 'uses default json formatter' do
193
- @body = ['blah']
194
- _, _, body = subject.call('PATH_INFO' => '/info.json')
195
- expect(body.body).to eq(['["blah"]'])
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(:app) { ->(_env) { [200, {}, @body] } }
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
- subject { Grape::Middleware::Versioner::Param.new(app, @options || {}) }
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
- before { @options = { version_options: { parameter: 'v' } } }
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
- before { @options = { versions: %w(v1 v2) } }
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
- it 'returns a 200 when no version is set (matches the first version found)' do
49
- @options = {
50
- versions: ['v1'],
51
- version_options: { using: :header }
52
- }
53
- env = Rack::MockRequest.env_for('/awesome', params: {})
54
- expect(subject.call(env).first).to eq(200)
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
- subject { Grape::Middleware::Versioner::Path.new(app, @options || {}) }
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
- before { @options = { pattern: /v./i } }
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
- before { @options = { versions: versions } }
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] })
@@ -30,8 +30,7 @@ module Grape
30
30
  expect(subject.settings).to eq(config1: 'alpha',
31
31
  config2: 'beta',
32
32
  config3: { config4: 'gamma' },
33
- config5: { config6: { config7: 7, config8: 8 } }
34
- )
33
+ config5: { config6: { config7: 7, config8: 8 } })
35
34
  end
36
35
  end
37
36
  end