grape 1.2.4 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -4
  3. data/README.md +144 -4
  4. data/grape.gemspec +3 -1
  5. data/lib/grape.rb +94 -66
  6. data/lib/grape/api.rb +46 -4
  7. data/lib/grape/api/instance.rb +23 -12
  8. data/lib/grape/dsl/desc.rb +11 -2
  9. data/lib/grape/dsl/validations.rb +4 -3
  10. data/lib/grape/eager_load.rb +18 -0
  11. data/lib/grape/endpoint.rb +3 -3
  12. data/lib/grape/error_formatter.rb +1 -1
  13. data/lib/grape/exceptions/validation_errors.rb +4 -2
  14. data/lib/grape/formatter.rb +1 -1
  15. data/lib/grape/middleware/auth/base.rb +2 -4
  16. data/lib/grape/middleware/base.rb +2 -0
  17. data/lib/grape/middleware/helpers.rb +10 -0
  18. data/lib/grape/parser.rb +1 -1
  19. data/lib/grape/util/base_inheritable.rb +34 -0
  20. data/lib/grape/util/inheritable_values.rb +5 -25
  21. data/lib/grape/util/lazy_block.rb +25 -0
  22. data/lib/grape/util/lazy_value.rb +5 -0
  23. data/lib/grape/util/reverse_stackable_values.rb +7 -36
  24. data/lib/grape/util/stackable_values.rb +19 -22
  25. data/lib/grape/validations/attributes_iterator.rb +5 -3
  26. data/lib/grape/validations/multiple_attributes_iterator.rb +11 -0
  27. data/lib/grape/validations/params_scope.rb +12 -12
  28. data/lib/grape/validations/single_attribute_iterator.rb +13 -0
  29. data/lib/grape/validations/validator_factory.rb +6 -11
  30. data/lib/grape/validations/validators/all_or_none.rb +6 -13
  31. data/lib/grape/validations/validators/at_least_one_of.rb +5 -13
  32. data/lib/grape/validations/validators/base.rb +11 -10
  33. data/lib/grape/validations/validators/coerce.rb +4 -0
  34. data/lib/grape/validations/validators/default.rb +1 -1
  35. data/lib/grape/validations/validators/exactly_one_of.rb +6 -23
  36. data/lib/grape/validations/validators/multiple_params_base.rb +14 -10
  37. data/lib/grape/validations/validators/mutual_exclusion.rb +6 -18
  38. data/lib/grape/version.rb +1 -1
  39. data/spec/grape/api/defines_boolean_in_params_spec.rb +37 -0
  40. data/spec/grape/api_remount_spec.rb +158 -0
  41. data/spec/grape/api_spec.rb +72 -0
  42. data/spec/grape/endpoint_spec.rb +1 -1
  43. data/spec/grape/exceptions/base_spec.rb +4 -0
  44. data/spec/grape/exceptions/validation_errors_spec.rb +6 -4
  45. data/spec/grape/integration/rack_spec.rb +22 -6
  46. data/spec/grape/middleware/base_spec.rb +8 -0
  47. data/spec/grape/middleware/formatter_spec.rb +11 -1
  48. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +29 -0
  49. data/spec/grape/validations/params_scope_spec.rb +13 -0
  50. data/spec/grape/validations/single_attribute_iterator_spec.rb +33 -0
  51. data/spec/grape/validations/validators/all_or_none_spec.rb +138 -30
  52. data/spec/grape/validations/validators/at_least_one_of_spec.rb +173 -29
  53. data/spec/grape/validations/validators/coerce_spec.rb +6 -2
  54. data/spec/grape/validations/validators/exactly_one_of_spec.rb +202 -38
  55. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +184 -27
  56. data/spec/grape/validations_spec.rb +32 -20
  57. metadata +103 -115
  58. data/Appraisals +0 -28
  59. data/Dangerfile +0 -2
  60. data/Gemfile +0 -33
  61. data/Gemfile.lock +0 -231
  62. data/Guardfile +0 -10
  63. data/RELEASING.md +0 -111
  64. data/Rakefile +0 -25
  65. data/benchmark/simple.rb +0 -27
  66. data/benchmark/simple_with_type_coercer.rb +0 -22
  67. data/gemfiles/multi_json.gemfile +0 -35
  68. data/gemfiles/multi_xml.gemfile +0 -35
  69. data/gemfiles/rack_1.5.2.gemfile.lock +0 -232
  70. data/gemfiles/rack_edge.gemfile +0 -35
  71. data/gemfiles/rails_3.gemfile +0 -36
  72. data/gemfiles/rails_3.gemfile.lock +0 -288
  73. data/gemfiles/rails_4.gemfile +0 -35
  74. data/gemfiles/rails_4.gemfile.lock +0 -280
  75. data/gemfiles/rails_5.gemfile +0 -35
  76. data/gemfiles/rails_5.gemfile.lock +0 -312
  77. data/gemfiles/rails_edge.gemfile +0 -35
  78. data/pkg/grape-1.2.0.gem +0 -0
  79. data/pkg/grape-1.2.1.gem +0 -0
  80. data/pkg/grape-1.2.3.gem +0 -0
@@ -1,4 +1,4 @@
1
1
  module Grape
2
2
  # The current version of Grape.
3
- VERSION = '1.2.4'.freeze
3
+ VERSION = '1.2.5'.freeze
4
4
  end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::API::Instance do
4
+ describe 'boolean constant' do
5
+ module DefinesBooleanInstanceSpec
6
+ class API < Grape::API
7
+ params do
8
+ requires :message, type: Boolean
9
+ end
10
+ post :echo do
11
+ { class: params[:message].class.name, value: params[:message] }
12
+ end
13
+ end
14
+ end
15
+
16
+ def app
17
+ DefinesBooleanInstanceSpec::API
18
+ end
19
+
20
+ let(:expected_body) do
21
+ { class: 'TrueClass', value: true }.to_s
22
+ end
23
+
24
+ it 'sets Boolean as a Virtus::Attribute::Boolean' do
25
+ post '/echo?message=true'
26
+ expect(last_response.status).to eq(201)
27
+ expect(last_response.body).to eq expected_body
28
+ end
29
+
30
+ context 'Params endpoint type' do
31
+ subject { DefinesBooleanInstanceSpec::API.new.router.map['POST'].first.options[:params]['message'][:type] }
32
+ it 'params type is a Virtus::Attribute::Boolean' do
33
+ is_expected.to eq 'Virtus::Attribute::Boolean'
34
+ end
35
+ end
36
+ end
37
+ end
@@ -97,6 +97,96 @@ describe Grape::API do
97
97
  end
98
98
  end
99
99
 
100
+ context 'when using an expression derived from a configuration' do
101
+ subject(:a_remounted_api) do
102
+ Class.new(Grape::API) do
103
+ get(mounted { "api_name_#{configuration[:api_name]}" }) do
104
+ 'success'
105
+ end
106
+ end
107
+ end
108
+
109
+ before do
110
+ root_api.mount a_remounted_api, with: {
111
+ api_name: 'a_name'
112
+ }
113
+ end
114
+
115
+ it 'mounts the endpoint with the name' do
116
+ get 'api_name_a_name'
117
+ expect(last_response.body).to eq 'success'
118
+ end
119
+
120
+ it 'does not mount the endpoint with a null name' do
121
+ get 'api_name_'
122
+ expect(last_response.body).not_to eq 'success'
123
+ end
124
+
125
+ context 'when the expression lives in a namespace' do
126
+ subject(:a_remounted_api) do
127
+ Class.new(Grape::API) do
128
+ namespace :base do
129
+ get(mounted { "api_name_#{configuration[:api_name]}" }) do
130
+ 'success'
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ it 'mounts the endpoint with the name' do
137
+ get 'base/api_name_a_name'
138
+ expect(last_response.body).to eq 'success'
139
+ end
140
+
141
+ it 'does not mount the endpoint with a null name' do
142
+ get 'base/api_name_'
143
+ expect(last_response.body).not_to eq 'success'
144
+ end
145
+ end
146
+ end
147
+
148
+ context 'when executing a standard block within a `mounted` block with all dynamic params' do
149
+ subject(:a_remounted_api) do
150
+ Class.new(Grape::API) do
151
+ mounted do
152
+ desc configuration[:description] do
153
+ headers configuration[:headers]
154
+ end
155
+ get configuration[:endpoint] do
156
+ configuration[:response]
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ let(:api_endpoint) { 'custom_endpoint' }
163
+ let(:api_response) { 'custom response' }
164
+ let(:endpoint_description) { 'this is a custom API' }
165
+ let(:headers) do
166
+ {
167
+ 'XAuthToken' => {
168
+ 'description' => 'Validates your identity',
169
+ 'required' => true
170
+ }
171
+ }
172
+ end
173
+
174
+ it 'mounts the API and obtains the description and headers definition' do
175
+ root_api.mount a_remounted_api, with: {
176
+ description: endpoint_description,
177
+ headers: headers,
178
+ endpoint: api_endpoint,
179
+ response: api_response
180
+ }
181
+ get api_endpoint
182
+ expect(last_response.body).to eq api_response
183
+ expect(a_remounted_api.instances.last.endpoints.first.options[:route_options][:description])
184
+ .to eq endpoint_description
185
+ expect(a_remounted_api.instances.last.endpoints.first.options[:route_options][:headers])
186
+ .to eq headers
187
+ end
188
+ end
189
+
100
190
  context 'when executing a custom block on mount' do
101
191
  subject(:a_remounted_api) do
102
192
  Class.new(Grape::API) do
@@ -264,6 +354,74 @@ describe Grape::API do
264
354
  end
265
355
  end
266
356
 
357
+ context 'a very complex configuration example' do
358
+ before do
359
+ top_level_api = Class.new(Grape::API) do
360
+ remounted_api = Class.new(Grape::API) do
361
+ get configuration[:endpoint_name] do
362
+ configuration[:response]
363
+ end
364
+ end
365
+
366
+ expression_namespace = mounted { configuration[:namespace].to_s * 2 }
367
+ given(mounted { configuration[:should_mount_expressed] != false }) do
368
+ namespace expression_namespace do
369
+ mount remounted_api, with: { endpoint_name: configuration[:endpoint_name], response: configuration[:endpoint_response] }
370
+ end
371
+ end
372
+ end
373
+ root_api.mount top_level_api, with: configuration_options
374
+ end
375
+
376
+ context 'when the namespace should be mounted' do
377
+ let(:configuration_options) do
378
+ {
379
+ should_mount_expressed: true,
380
+ namespace: 'bang',
381
+ endpoint_name: 'james',
382
+ endpoint_response: 'bond'
383
+ }
384
+ end
385
+
386
+ it 'gets a response' do
387
+ get 'bangbang/james'
388
+ expect(last_response.body).to eq 'bond'
389
+ end
390
+ end
391
+
392
+ context 'when should be mounted is nil' do
393
+ let(:configuration_options) do
394
+ {
395
+ should_mount_expressed: nil,
396
+ namespace: 'bang',
397
+ endpoint_name: 'james',
398
+ endpoint_response: 'bond'
399
+ }
400
+ end
401
+
402
+ it 'gets a response' do
403
+ get 'bangbang/james'
404
+ expect(last_response.body).to eq 'bond'
405
+ end
406
+ end
407
+
408
+ context 'when it should not be mounted' do
409
+ let(:configuration_options) do
410
+ {
411
+ should_mount_expressed: false,
412
+ namespace: 'bang',
413
+ endpoint_name: 'james',
414
+ endpoint_response: 'bond'
415
+ }
416
+ end
417
+
418
+ it 'gets a response' do
419
+ get 'bangbang/james'
420
+ expect(last_response.body).not_to eq 'bond'
421
+ end
422
+ end
423
+ end
424
+
267
425
  context 'when the configuration is read in a helper' do
268
426
  subject(:a_remounted_api) do
269
427
  Class.new(Grape::API) do
@@ -885,6 +885,40 @@ XML
885
885
  end
886
886
  end
887
887
 
888
+ describe '.compile!' do
889
+ it 'requires the grape/eager_load file' do
890
+ expect(app).to receive(:require).with('grape/eager_load') { nil }
891
+ app.compile!
892
+ end
893
+
894
+ it 'compiles the instance for rack!' do
895
+ stubbed_object = double(:instance_for_rack)
896
+ allow(app).to receive(:instance_for_rack) { stubbed_object }
897
+ end
898
+ end
899
+
900
+ # NOTE: this method is required to preserve the ability of pre-mounting
901
+ # the root API into a namespace, it may be deprecated in the future.
902
+ describe 'instance_for_rack' do
903
+ context 'when the app was not mounted' do
904
+ it 'returns the base_instance' do
905
+ expect(app.send(:instance_for_rack)).to eq app.base_instance
906
+ end
907
+ end
908
+
909
+ context 'when the app was mounted' do
910
+ it 'returns the first mounted instance' do
911
+ mounted_app = app
912
+ Class.new(Grape::API) do
913
+ namespace 'new_namespace' do
914
+ mount mounted_app
915
+ end
916
+ end
917
+ expect(app.send(:instance_for_rack)).to eq app.send(:mounted_instances).first
918
+ end
919
+ end
920
+ end
921
+
888
922
  describe 'filters' do
889
923
  it 'adds a before filter' do
890
924
  subject.before { @foo = 'first' }
@@ -3720,6 +3754,44 @@ XML
3720
3754
  end
3721
3755
  end
3722
3756
 
3757
+ describe '.configure' do
3758
+ context 'when given a block' do
3759
+ it 'returns self' do
3760
+ expect(subject.configure {}).to be subject
3761
+ end
3762
+
3763
+ it 'calls the block passing the config' do
3764
+ call = [false, nil]
3765
+ subject.configure do |config|
3766
+ call = [true, config]
3767
+ end
3768
+
3769
+ expect(call[0]).to be true
3770
+ expect(call[1]).not_to be_nil
3771
+ end
3772
+ end
3773
+
3774
+ context 'when not given a block' do
3775
+ it 'returns a configuration object' do
3776
+ expect(subject.configure).to respond_to(:[], :[]=)
3777
+ end
3778
+ end
3779
+
3780
+ it 'allows configuring the api' do
3781
+ subject.configure do |config|
3782
+ config[:hello] = 'hello'
3783
+ config[:bread] = 'bread'
3784
+ end
3785
+
3786
+ subject.get '/hello-bread' do
3787
+ "#{configuration[:hello]} #{configuration[:bread]}"
3788
+ end
3789
+
3790
+ get '/hello-bread'
3791
+ expect(last_response.body).to eq 'hello bread'
3792
+ end
3793
+ end
3794
+
3723
3795
  context 'catch-all' do
3724
3796
  before do
3725
3797
  api1 = Class.new(Grape::API)
@@ -8,7 +8,7 @@ describe Grape::Endpoint do
8
8
  end
9
9
 
10
10
  describe '.before_each' do
11
- after { Grape::Endpoint.before_each(nil) }
11
+ after { Grape::Endpoint.before_each.clear }
12
12
 
13
13
  it 'is settable via block' do
14
14
  block = ->(_endpoint) { 'noop' }
@@ -8,8 +8,11 @@ describe Grape::Exceptions::Base do
8
8
  let(:attributes) { { klass: String, to_format: 'xml' } }
9
9
 
10
10
  after do
11
+ I18n.enforce_available_locales = true
11
12
  I18n.available_locales = %i[en]
13
+ I18n.locale = :en
12
14
  I18n.default_locale = :en
15
+ I18n.reload!
13
16
  end
14
17
 
15
18
  context 'when I18n enforces available locales' do
@@ -29,6 +32,7 @@ describe Grape::Exceptions::Base do
29
32
  context 'when the fallback locale is not available' do
30
33
  before do
31
34
  I18n.available_locales = %i[de jp]
35
+ I18n.locale = :de
32
36
  I18n.default_locale = :de
33
37
  end
34
38
 
@@ -77,10 +77,12 @@ describe Grape::Exceptions::ValidationErrors do
77
77
  end
78
78
  get '/exactly_one_of', beer: 'string', wine: 'anotherstring'
79
79
  expect(last_response.status).to eq(400)
80
- expect(JSON.parse(last_response.body)).to eq([
81
- 'params' => %w[beer wine],
82
- 'messages' => ['are mutually exclusive']
83
- ])
80
+ expect(JSON.parse(last_response.body)).to eq(
81
+ [
82
+ 'params' => %w[beer wine juice],
83
+ 'messages' => ['are missing, exactly one parameter must be provided']
84
+ ]
85
+ )
84
86
  end
85
87
  end
86
88
  end
@@ -19,16 +19,32 @@ describe Rack do
19
19
  }
20
20
  env = Rack::MockRequest.env_for('/', options)
21
21
 
22
- unless RUBY_PLATFORM == 'java'
23
- major, minor, patch = Rack.release.split('.').map(&:to_i)
24
- patch ||= 0 # rack <= 1.5.2 does not specify patch version
25
- pending 'Rack 1.5.3 or 1.6.1 required' unless major >= 2 || (major >= 1 && ((minor == 5 && patch >= 3) || (minor >= 6)))
26
- end
27
-
28
22
  expect(JSON.parse(app.call(env)[2].body.first)['params_keys']).to match_array('test')
29
23
  ensure
30
24
  input.close
31
25
  input.unlink
32
26
  end
33
27
  end
28
+
29
+ context 'when the app is mounted' do
30
+ def app
31
+ @main_app ||= Class.new(Grape::API) do
32
+ get 'ping'
33
+ end
34
+ end
35
+
36
+ let!(:base) do
37
+ app_to_mount = app
38
+ Class.new(Grape::API) do
39
+ namespace 'namespace' do
40
+ mount app_to_mount
41
+ end
42
+ end
43
+ end
44
+
45
+ it 'finds the app on the namespace' do
46
+ get '/namespace/ping'
47
+ expect(last_response.status).to eq 200
48
+ end
49
+ end
34
50
  end
@@ -114,6 +114,14 @@ describe Grape::Middleware::Base do
114
114
  end
115
115
  end
116
116
 
117
+ describe '#context' do
118
+ subject { Grape::Middleware::Base.new(blank_app) }
119
+ it 'allows access to response context' do
120
+ subject.call(Grape::Env::API_ENDPOINT => { header: 'some header' })
121
+ expect(subject.context).to eq(header: 'some header')
122
+ end
123
+ end
124
+
117
125
  context 'options' do
118
126
  it 'persists options passed at initialization' do
119
127
  expect(Grape::Middleware::Base.new(blank_app, abc: true).options[:abc]).to be true
@@ -213,7 +213,13 @@ describe Grape::Middleware::Formatter do
213
213
  context 'no content responses' do
214
214
  let(:no_content_response) { ->(status) { [status, {}, ['']] } }
215
215
 
216
- Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.each do |status|
216
+ STATUSES_WITHOUT_BODY = if Gem::Version.new(Rack.release) >= Gem::Version.new('2.1.0')
217
+ Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.keys
218
+ else
219
+ Rack::Utils::STATUS_WITH_NO_ENTITY_BODY
220
+ end
221
+
222
+ STATUSES_WITHOUT_BODY.each do |status|
217
223
  it "does not modify a #{status} response" do
218
224
  expected_response = no_content_response[status]
219
225
  allow(app).to receive(:call).and_return(expected_response)
@@ -391,6 +397,10 @@ describe Grape::Middleware::Formatter do
391
397
  Grape::Formatter.register :invalid, InvalidFormatter
392
398
  Grape::ContentTypes::CONTENT_TYPES[:invalid] = 'application/x-invalid'
393
399
  end
400
+ after do
401
+ Grape::ContentTypes::CONTENT_TYPES.delete(:invalid)
402
+ Grape::Formatter.default_elements.delete(:invalid)
403
+ end
394
404
 
395
405
  it 'returns response by invalid formatter' do
396
406
  env = { 'PATH_INFO' => '/hello.invalid', 'HTTP_ACCEPT' => 'application/x-invalid' }
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Validations::MultipleAttributesIterator do
4
+ describe '#each' do
5
+ subject(:iterator) { described_class.new(validator, scope, params) }
6
+ let(:scope) { Grape::Validations::ParamsScope.new(api: Class.new(Grape::API)) }
7
+ let(:validator) { double(attrs: %i[first second third]) }
8
+
9
+ context 'when params is a hash' do
10
+ let(:params) do
11
+ { first: 'string', second: 'string' }
12
+ end
13
+
14
+ it 'yields the whole params hash without the list of attrs' do
15
+ expect { |b| iterator.each(&b) }.to yield_with_args(params)
16
+ end
17
+ end
18
+
19
+ context 'when params is an array' do
20
+ let(:params) do
21
+ [{ first: 'string1', second: 'string1' }, { first: 'string2', second: 'string2' }]
22
+ end
23
+
24
+ it 'yields each element of the array without the list of attrs' do
25
+ expect { |b| iterator.each(&b) }.to yield_successive_args(params[0], params[1])
26
+ end
27
+ end
28
+ end
29
+ end