grape 1.2.4 → 1.2.5

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.
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