grape 1.3.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/LICENSE +1 -1
  4. data/README.md +104 -21
  5. data/UPGRADING.md +243 -39
  6. data/lib/grape.rb +4 -5
  7. data/lib/grape/api.rb +4 -4
  8. data/lib/grape/api/instance.rb +32 -31
  9. data/lib/grape/content_types.rb +34 -0
  10. data/lib/grape/dsl/helpers.rb +2 -1
  11. data/lib/grape/dsl/inside_route.rb +76 -42
  12. data/lib/grape/dsl/parameters.rb +4 -4
  13. data/lib/grape/dsl/routing.rb +8 -8
  14. data/lib/grape/dsl/validations.rb +18 -1
  15. data/lib/grape/eager_load.rb +1 -1
  16. data/lib/grape/endpoint.rb +8 -6
  17. data/lib/grape/exceptions/base.rb +0 -4
  18. data/lib/grape/exceptions/validation_errors.rb +11 -12
  19. data/lib/grape/http/headers.rb +26 -0
  20. data/lib/grape/middleware/base.rb +3 -4
  21. data/lib/grape/middleware/error.rb +10 -12
  22. data/lib/grape/middleware/formatter.rb +3 -3
  23. data/lib/grape/middleware/stack.rb +19 -5
  24. data/lib/grape/middleware/versioner/header.rb +4 -4
  25. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  26. data/lib/grape/middleware/versioner/path.rb +1 -1
  27. data/lib/grape/namespace.rb +12 -2
  28. data/lib/grape/path.rb +13 -3
  29. data/lib/grape/request.rb +13 -8
  30. data/lib/grape/router.rb +26 -30
  31. data/lib/grape/router/attribute_translator.rb +25 -4
  32. data/lib/grape/router/pattern.rb +17 -16
  33. data/lib/grape/router/route.rb +5 -24
  34. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  35. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  36. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  37. data/lib/grape/util/base_inheritable.rb +15 -8
  38. data/lib/grape/util/cache.rb +20 -0
  39. data/lib/grape/util/lazy_object.rb +43 -0
  40. data/lib/grape/util/lazy_value.rb +1 -0
  41. data/lib/grape/util/reverse_stackable_values.rb +2 -0
  42. data/lib/grape/util/stackable_values.rb +7 -20
  43. data/lib/grape/validations/params_scope.rb +6 -5
  44. data/lib/grape/validations/types.rb +6 -5
  45. data/lib/grape/validations/types/array_coercer.rb +14 -5
  46. data/lib/grape/validations/types/build_coercer.rb +5 -8
  47. data/lib/grape/validations/types/custom_type_coercer.rb +14 -2
  48. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  49. data/lib/grape/validations/types/file.rb +15 -12
  50. data/lib/grape/validations/types/json.rb +40 -36
  51. data/lib/grape/validations/types/primitive_coercer.rb +15 -6
  52. data/lib/grape/validations/types/set_coercer.rb +6 -4
  53. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  54. data/lib/grape/validations/validators/as.rb +1 -1
  55. data/lib/grape/validations/validators/base.rb +2 -4
  56. data/lib/grape/validations/validators/coerce.rb +4 -11
  57. data/lib/grape/validations/validators/default.rb +3 -5
  58. data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
  59. data/lib/grape/validations/validators/except_values.rb +1 -1
  60. data/lib/grape/validations/validators/regexp.rb +1 -1
  61. data/lib/grape/validations/validators/values.rb +1 -1
  62. data/lib/grape/version.rb +1 -1
  63. data/spec/grape/api/instance_spec.rb +50 -0
  64. data/spec/grape/api_spec.rb +82 -6
  65. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  66. data/spec/grape/endpoint/declared_spec.rb +590 -0
  67. data/spec/grape/endpoint_spec.rb +0 -521
  68. data/spec/grape/entity_spec.rb +6 -0
  69. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  70. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  71. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  72. data/spec/grape/middleware/error_spec.rb +1 -1
  73. data/spec/grape/middleware/formatter_spec.rb +3 -3
  74. data/spec/grape/middleware/stack_spec.rb +12 -1
  75. data/spec/grape/path_spec.rb +4 -4
  76. data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
  77. data/spec/grape/validations/params_scope_spec.rb +26 -0
  78. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  79. data/spec/grape/validations/types/primitive_coercer_spec.rb +135 -0
  80. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  81. data/spec/grape/validations/types_spec.rb +1 -1
  82. data/spec/grape/validations/validators/coerce_spec.rb +329 -77
  83. data/spec/grape/validations/validators/default_spec.rb +170 -0
  84. data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
  85. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  86. data/spec/grape/validations/validators/values_spec.rb +1 -1
  87. data/spec/grape/validations_spec.rb +30 -30
  88. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  89. data/spec/spec_helper.rb +3 -10
  90. data/spec/support/chunks.rb +14 -0
  91. data/spec/support/eager_load.rb +19 -0
  92. data/spec/support/versioned_helpers.rb +3 -5
  93. metadata +121 -105
  94. data/lib/grape/util/content_types.rb +0 -28
@@ -181,6 +181,7 @@ describe Grape::Entity do
181
181
  subject.get '/example' do
182
182
  c = Class.new do
183
183
  attr_reader :id
184
+
184
185
  def initialize(id)
185
186
  @id = id
186
187
  end
@@ -202,6 +203,7 @@ describe Grape::Entity do
202
203
  subject.get '/examples' do
203
204
  c = Class.new do
204
205
  attr_reader :id
206
+
205
207
  def initialize(id)
206
208
  @id = id
207
209
  end
@@ -226,6 +228,7 @@ describe Grape::Entity do
226
228
  subject.get '/example' do
227
229
  c = Class.new do
228
230
  attr_reader :name
231
+
229
232
  def initialize(args)
230
233
  @name = args[:name] || 'no name set'
231
234
  end
@@ -255,6 +258,7 @@ XML
255
258
  subject.get '/example' do
256
259
  c = Class.new do
257
260
  attr_reader :name
261
+
258
262
  def initialize(args)
259
263
  @name = args[:name] || 'no name set'
260
264
  end
@@ -284,6 +288,7 @@ XML
284
288
  subject.get '/example' do
285
289
  c = Class.new do
286
290
  attr_reader :name
291
+
287
292
  def initialize(args)
288
293
  @name = args[:name] || 'no name set'
289
294
  end
@@ -302,6 +307,7 @@ XML
302
307
  it 'present with multiple entities using optional symbol' do
303
308
  user = Class.new do
304
309
  attr_reader :name
310
+
305
311
  def initialize(args)
306
312
  @name = args[:name] || 'no name set'
307
313
  end
@@ -81,8 +81,8 @@ describe Grape::Exceptions::ValidationErrors do
81
81
  expect(last_response.status).to eq(400)
82
82
  expect(JSON.parse(last_response.body)).to eq(
83
83
  [
84
- 'params' => %w[beer wine juice],
85
- 'messages' => ['are missing, exactly one parameter must be provided']
84
+ 'params' => %w[beer wine],
85
+ 'messages' => ['are mutually exclusive']
86
86
  ]
87
87
  )
88
88
  end
@@ -4,12 +4,16 @@ require 'spec_helper'
4
4
 
5
5
  describe Rack::Sendfile do
6
6
  subject do
7
- send_file = file_streamer
7
+ content_object = file_object
8
8
  app = Class.new(Grape::API) do
9
9
  use Rack::Sendfile
10
10
  format :json
11
11
  get do
12
- file send_file
12
+ if content_object.is_a?(String)
13
+ sendfile content_object
14
+ else
15
+ stream content_object
16
+ end
13
17
  end
14
18
  end
15
19
 
@@ -22,9 +26,9 @@ describe Rack::Sendfile do
22
26
  app.call(env)
23
27
  end
24
28
 
25
- context do
26
- let(:file_streamer) do
27
- double(:file_streamer, to_path: '/accel/mapping/some/path')
29
+ context 'when calling sendfile' do
30
+ let(:file_object) do
31
+ '/accel/mapping/some/path'
28
32
  end
29
33
 
30
34
  it 'contains Sendfile headers' do
@@ -33,9 +37,9 @@ describe Rack::Sendfile do
33
37
  end
34
38
  end
35
39
 
36
- context do
37
- let(:file_streamer) do
38
- double(:file_streamer)
40
+ context 'when streaming non file content' do
41
+ let(:file_object) do
42
+ double(:file_object, each: nil)
39
43
  end
40
44
 
41
45
  it 'not contains Sendfile headers' do
@@ -36,7 +36,7 @@ describe Grape::Middleware::Auth::Strategies do
36
36
  RSpec::Matchers.define :be_challenge do
37
37
  match do |actual_response|
38
38
  actual_response.status == 401 &&
39
- actual_response['WWW-Authenticate'] =~ /^Digest / &&
39
+ actual_response['WWW-Authenticate'].start_with?('Digest ') &&
40
40
  actual_response.body.empty?
41
41
  end
42
42
  end
@@ -30,7 +30,7 @@ describe Grape::Middleware::Error do
30
30
  opts = options
31
31
  Rack::Builder.app do
32
32
  use Spec::Support::EndpointFaker
33
- use Grape::Middleware::Error, opts
33
+ use Grape::Middleware::Error, **opts
34
34
  run ErrorSpec::ErrApp
35
35
  end
36
36
  end
@@ -380,7 +380,7 @@ describe Grape::Middleware::Formatter do
380
380
 
381
381
  context 'send file' do
382
382
  let(:file) { double(File) }
383
- let(:file_body) { Grape::ServeFile::FileResponse.new(file) }
383
+ let(:file_body) { Grape::ServeStream::StreamResponse.new(file) }
384
384
  let(:app) { ->(_env) { [200, {}, file_body] } }
385
385
 
386
386
  it 'returns a file response' do
@@ -402,10 +402,10 @@ describe Grape::Middleware::Formatter do
402
402
  let(:app) { ->(_env) { [200, {}, ['']] } }
403
403
  before do
404
404
  Grape::Formatter.register :invalid, InvalidFormatter
405
- Grape::ContentTypes::CONTENT_TYPES[:invalid] = 'application/x-invalid'
405
+ Grape::ContentTypes.register :invalid, 'application/x-invalid'
406
406
  end
407
407
  after do
408
- Grape::ContentTypes::CONTENT_TYPES.delete(:invalid)
408
+ Grape::ContentTypes.default_elements.delete(:invalid)
409
409
  Grape::Formatter.default_elements.delete(:invalid)
410
410
  end
411
411
 
@@ -8,6 +8,7 @@ describe Grape::Middleware::Stack do
8
8
  class BarMiddleware; end
9
9
  class BlockMiddleware
10
10
  attr_reader :block
11
+
11
12
  def initialize(&block)
12
13
  @block = block
13
14
  end
@@ -34,7 +35,8 @@ describe Grape::Middleware::Stack do
34
35
  expect { subject.use StackSpec::BarMiddleware, false, my_arg: 42 }
35
36
  .to change { subject.size }.by(1)
36
37
  expect(subject.last).to eq(StackSpec::BarMiddleware)
37
- expect(subject.last.args).to eq([false, { my_arg: 42 }])
38
+ expect(subject.last.args).to eq([false])
39
+ expect(subject.last.opts).to eq(my_arg: 42)
38
40
  end
39
41
 
40
42
  it 'pushes a middleware class with block arguments onto the stack' do
@@ -111,6 +113,15 @@ describe Grape::Middleware::Stack do
111
113
  expect(subject[1]).to eq(StackSpec::BlockMiddleware)
112
114
  expect(subject[2]).to eq(StackSpec::BarMiddleware)
113
115
  end
116
+
117
+ context 'middleware spec with proc declaration exists' do
118
+ let(:middleware_spec_with_proc) { [:use, StackSpec::FooMiddleware, proc] }
119
+
120
+ it 'properly forwards spec arguments' do
121
+ expect(subject).to receive(:use).with(StackSpec::FooMiddleware)
122
+ subject.merge_with([middleware_spec_with_proc])
123
+ end
124
+ end
114
125
  end
115
126
 
116
127
  describe '#build' do
@@ -87,12 +87,12 @@ module Grape
87
87
  describe '#namespace?' do
88
88
  it 'is false when the namespace is nil' do
89
89
  path = Path.new(anything, nil, anything)
90
- expect(path.namespace?).to be nil
90
+ expect(path.namespace?).to be_falsey
91
91
  end
92
92
 
93
93
  it 'is false when the namespace starts with whitespace' do
94
94
  path = Path.new(anything, ' /foo', anything)
95
- expect(path.namespace?).to be nil
95
+ expect(path.namespace?).to be_falsey
96
96
  end
97
97
 
98
98
  it 'is false when the namespace is the root path' do
@@ -109,12 +109,12 @@ module Grape
109
109
  describe '#path?' do
110
110
  it 'is false when the path is nil' do
111
111
  path = Path.new(nil, anything, anything)
112
- expect(path.path?).to be nil
112
+ expect(path.path?).to be_falsey
113
113
  end
114
114
 
115
115
  it 'is false when the path starts with whitespace' do
116
116
  path = Path.new(' /foo', anything, anything)
117
- expect(path.path?).to be nil
117
+ expect(path.path?).to be_falsey
118
118
  end
119
119
 
120
120
  it 'is false when the path is the root path' do
@@ -6,7 +6,7 @@ describe 'Validator with instance variables' do
6
6
  let(:validator_type) do
7
7
  Class.new(Grape::Validations::Base) do
8
8
  def validate_param!(_attr_name, _params)
9
- if @instance_variable
9
+ if instance_variable_defined?(:@instance_variable) && @instance_variable
10
10
  raise Grape::Exceptions::Validation.new(params: ['params'],
11
11
  message: 'This should never happen')
12
12
  end
@@ -633,6 +633,32 @@ describe Grape::Validations::ParamsScope do
633
633
  expect(last_response.status).to eq(200)
634
634
  end
635
635
 
636
+ it 'detect unmet nested dependency' do
637
+ subject.params do
638
+ requires :a, type: String, allow_blank: false, values: %w[x y z]
639
+ given a: ->(val) { val == 'z' } do
640
+ requires :inner3, type: Array, allow_blank: false do
641
+ requires :bar, type: String, allow_blank: false
642
+ given bar: ->(val) { val == 'b' } do
643
+ requires :baz, type: Array do
644
+ optional :baz_category, type: String
645
+ end
646
+ end
647
+ given bar: ->(val) { val == 'c' } do
648
+ requires :baz, type: Array do
649
+ requires :baz_category, type: String
650
+ end
651
+ end
652
+ end
653
+ end
654
+ end
655
+ subject.get('/nested-dependency') { declared(params).to_json }
656
+
657
+ get '/nested-dependency', a: 'z', inner3: [{ bar: 'c', baz: [{ unrelated: 'nope' }] }]
658
+ expect(last_response.status).to eq(400)
659
+ expect(last_response.body).to eq 'inner3[0][baz][0][baz_category] is missing'
660
+ end
661
+
636
662
  it 'includes the parameter within #declared(params)' do
637
663
  get '/test', a: true, b: true
638
664
 
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Grape::Validations::Types::ArrayCoercer do
6
+ subject { described_class.new(type) }
7
+
8
+ describe '#call' do
9
+ context 'an array of primitives' do
10
+ let(:type) { Array[String] }
11
+
12
+ it 'coerces elements in the array' do
13
+ expect(subject.call([10, 20])).to eq(%w[10 20])
14
+ end
15
+ end
16
+
17
+ context 'an array of arrays' do
18
+ let(:type) { Array[Array[Integer]] }
19
+
20
+ it 'coerces elements in the nested array' do
21
+ expect(subject.call([%w[10 20]])).to eq([[10, 20]])
22
+ expect(subject.call([['10'], ['20']])).to eq([[10], [20]])
23
+ end
24
+ end
25
+
26
+ context 'an array of sets' do
27
+ let(:type) { Array[Set[Integer]] }
28
+
29
+ it 'coerces elements in the nested set' do
30
+ expect(subject.call([%w[10 20]])).to eq([Set[10, 20]])
31
+ expect(subject.call([['10'], ['20']])).to eq([Set[10], Set[20]])
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Grape::Validations::Types::PrimitiveCoercer do
6
+ let(:strict) { false }
7
+
8
+ subject { described_class.new(type, strict) }
9
+
10
+ describe '#call' do
11
+ context 'BigDecimal' do
12
+ let(:type) { BigDecimal }
13
+
14
+ it 'coerces to BigDecimal' do
15
+ expect(subject.call(5)).to eq(BigDecimal(5))
16
+ end
17
+
18
+ it 'coerces an empty string to nil' do
19
+ expect(subject.call('')).to be_nil
20
+ end
21
+ end
22
+
23
+ context 'Boolean' do
24
+ let(:type) { Grape::API::Boolean }
25
+
26
+ [true, 'true', 1].each do |val|
27
+ it "coerces '#{val}' to true" do
28
+ expect(subject.call(val)).to eq(true)
29
+ end
30
+ end
31
+
32
+ [false, 'false', 0].each do |val|
33
+ it "coerces '#{val}' to false" do
34
+ expect(subject.call(val)).to eq(false)
35
+ end
36
+ end
37
+
38
+ it 'returns an error when the given value cannot be coerced' do
39
+ expect(subject.call(123)).to be_instance_of(Grape::Validations::Types::InvalidValue)
40
+ end
41
+
42
+ it 'coerces an empty string to nil' do
43
+ expect(subject.call('')).to be_nil
44
+ end
45
+ end
46
+
47
+ context 'DateTime' do
48
+ let(:type) { DateTime }
49
+
50
+ it 'coerces an empty string to nil' do
51
+ expect(subject.call('')).to be_nil
52
+ end
53
+ end
54
+
55
+ context 'Float' do
56
+ let(:type) { Float }
57
+
58
+ it 'coerces an empty string to nil' do
59
+ expect(subject.call('')).to be_nil
60
+ end
61
+ end
62
+
63
+ context 'Integer' do
64
+ let(:type) { Integer }
65
+
66
+ it 'coerces an empty string to nil' do
67
+ expect(subject.call('')).to be_nil
68
+ end
69
+ end
70
+
71
+ context 'Numeric' do
72
+ let(:type) { Numeric }
73
+
74
+ it 'coerces an empty string to nil' do
75
+ expect(subject.call('')).to be_nil
76
+ end
77
+ end
78
+
79
+ context 'Time' do
80
+ let(:type) { Time }
81
+
82
+ it 'coerces an empty string to nil' do
83
+ expect(subject.call('')).to be_nil
84
+ end
85
+ end
86
+
87
+ context 'String' do
88
+ let(:type) { String }
89
+
90
+ it 'coerces to String' do
91
+ expect(subject.call(10)).to eq('10')
92
+ end
93
+
94
+ it 'does not coerce an empty string to nil' do
95
+ expect(subject.call('')).to eq('')
96
+ end
97
+ end
98
+
99
+ context 'Symbol' do
100
+ let(:type) { Symbol }
101
+
102
+ it 'coerces an empty string to nil' do
103
+ expect(subject.call('')).to be_nil
104
+ end
105
+ end
106
+
107
+ context 'the strict mode' do
108
+ let(:strict) { true }
109
+
110
+ context 'Boolean' do
111
+ let(:type) { Grape::API::Boolean }
112
+
113
+ it 'returns an error when the given value is not Boolean' do
114
+ expect(subject.call(1)).to be_instance_of(Grape::Validations::Types::InvalidValue)
115
+ end
116
+
117
+ it 'returns a value as it is when the given value is Boolean' do
118
+ expect(subject.call(true)).to eq(true)
119
+ end
120
+ end
121
+
122
+ context 'BigDecimal' do
123
+ let(:type) { BigDecimal }
124
+
125
+ it 'returns an error when the given value is not BigDecimal' do
126
+ expect(subject.call(1)).to be_instance_of(Grape::Validations::Types::InvalidValue)
127
+ end
128
+
129
+ it 'returns a value as it is when the given value is BigDecimal' do
130
+ expect(subject.call(BigDecimal(0))).to eq(BigDecimal(0))
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Grape::Validations::Types::SetCoercer do
6
+ subject { described_class.new(type) }
7
+
8
+ describe '#call' do
9
+ context 'a set of primitives' do
10
+ let(:type) { Set[String] }
11
+
12
+ it 'coerces elements to the set' do
13
+ expect(subject.call([10, 20])).to eq(Set['10', '20'])
14
+ end
15
+ end
16
+
17
+ context 'a set of sets' do
18
+ let(:type) { Set[Set[Integer]] }
19
+
20
+ it 'coerces elements in the nested set' do
21
+ expect(subject.call([%w[10 20]])).to eq(Set[Set[10, 20]])
22
+ expect(subject.call([['10'], ['20']])).to eq(Set[Set[10], Set[20]])
23
+ end
24
+ end
25
+
26
+ context 'a set of sets of arrays' do
27
+ let(:type) { Set[Set[Array[Integer]]] }
28
+
29
+ it 'coerces elements in the nested set' do
30
+ expect(subject.call([[['10'], ['20']]])).to eq(Set[Set[Array[10], Array[20]]])
31
+ end
32
+ end
33
+ end
34
+ end