grape 1.1.0 → 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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -43
  3. data/LICENSE +1 -1
  4. data/README.md +394 -47
  5. data/UPGRADING.md +111 -0
  6. data/grape.gemspec +3 -1
  7. data/lib/grape.rb +98 -66
  8. data/lib/grape/api.rb +136 -175
  9. data/lib/grape/api/instance.rb +280 -0
  10. data/lib/grape/config.rb +32 -0
  11. data/lib/grape/dsl/callbacks.rb +20 -0
  12. data/lib/grape/dsl/desc.rb +39 -7
  13. data/lib/grape/dsl/inside_route.rb +12 -6
  14. data/lib/grape/dsl/middleware.rb +7 -0
  15. data/lib/grape/dsl/parameters.rb +9 -4
  16. data/lib/grape/dsl/routing.rb +5 -1
  17. data/lib/grape/dsl/validations.rb +4 -3
  18. data/lib/grape/eager_load.rb +18 -0
  19. data/lib/grape/endpoint.rb +42 -26
  20. data/lib/grape/error_formatter.rb +1 -1
  21. data/lib/grape/exceptions/base.rb +9 -1
  22. data/lib/grape/exceptions/invalid_response.rb +9 -0
  23. data/lib/grape/exceptions/validation_errors.rb +4 -2
  24. data/lib/grape/formatter.rb +1 -1
  25. data/lib/grape/locale/en.yml +2 -0
  26. data/lib/grape/middleware/auth/base.rb +2 -4
  27. data/lib/grape/middleware/base.rb +2 -0
  28. data/lib/grape/middleware/error.rb +9 -4
  29. data/lib/grape/middleware/helpers.rb +10 -0
  30. data/lib/grape/middleware/stack.rb +1 -1
  31. data/lib/grape/middleware/versioner/header.rb +4 -4
  32. data/lib/grape/parser.rb +1 -1
  33. data/lib/grape/request.rb +1 -1
  34. data/lib/grape/router/attribute_translator.rb +2 -0
  35. data/lib/grape/router/route.rb +2 -2
  36. data/lib/grape/util/base_inheritable.rb +34 -0
  37. data/lib/grape/util/endpoint_configuration.rb +6 -0
  38. data/lib/grape/util/inheritable_values.rb +5 -25
  39. data/lib/grape/util/lazy_block.rb +25 -0
  40. data/lib/grape/util/lazy_value.rb +95 -0
  41. data/lib/grape/util/reverse_stackable_values.rb +7 -36
  42. data/lib/grape/util/stackable_values.rb +19 -22
  43. data/lib/grape/validations/attributes_iterator.rb +5 -3
  44. data/lib/grape/validations/multiple_attributes_iterator.rb +11 -0
  45. data/lib/grape/validations/params_scope.rb +20 -14
  46. data/lib/grape/validations/single_attribute_iterator.rb +13 -0
  47. data/lib/grape/validations/types/custom_type_coercer.rb +1 -1
  48. data/lib/grape/validations/types/file.rb +1 -1
  49. data/lib/grape/validations/validator_factory.rb +6 -11
  50. data/lib/grape/validations/validators/all_or_none.rb +6 -13
  51. data/lib/grape/validations/validators/as.rb +2 -3
  52. data/lib/grape/validations/validators/at_least_one_of.rb +5 -13
  53. data/lib/grape/validations/validators/base.rb +11 -10
  54. data/lib/grape/validations/validators/coerce.rb +4 -0
  55. data/lib/grape/validations/validators/default.rb +1 -1
  56. data/lib/grape/validations/validators/exactly_one_of.rb +6 -23
  57. data/lib/grape/validations/validators/multiple_params_base.rb +14 -10
  58. data/lib/grape/validations/validators/mutual_exclusion.rb +6 -18
  59. data/lib/grape/validations/validators/same_as.rb +23 -0
  60. data/lib/grape/version.rb +1 -1
  61. data/spec/grape/api/defines_boolean_in_params_spec.rb +37 -0
  62. data/spec/grape/api/routes_with_requirements_spec.rb +59 -0
  63. data/spec/grape/api_remount_spec.rb +466 -0
  64. data/spec/grape/api_spec.rb +379 -1
  65. data/spec/grape/config_spec.rb +17 -0
  66. data/spec/grape/dsl/desc_spec.rb +40 -16
  67. data/spec/grape/dsl/middleware_spec.rb +8 -0
  68. data/spec/grape/dsl/routing_spec.rb +10 -0
  69. data/spec/grape/endpoint_spec.rb +40 -4
  70. data/spec/grape/exceptions/base_spec.rb +65 -0
  71. data/spec/grape/exceptions/invalid_response_spec.rb +11 -0
  72. data/spec/grape/exceptions/validation_errors_spec.rb +6 -4
  73. data/spec/grape/integration/rack_spec.rb +22 -6
  74. data/spec/grape/middleware/auth/dsl_spec.rb +3 -3
  75. data/spec/grape/middleware/base_spec.rb +8 -0
  76. data/spec/grape/middleware/exception_spec.rb +1 -1
  77. data/spec/grape/middleware/formatter_spec.rb +15 -5
  78. data/spec/grape/middleware/versioner/header_spec.rb +6 -0
  79. data/spec/grape/named_api_spec.rb +19 -0
  80. data/spec/grape/request_spec.rb +24 -0
  81. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +29 -0
  82. data/spec/grape/validations/params_scope_spec.rb +184 -8
  83. data/spec/grape/validations/single_attribute_iterator_spec.rb +33 -0
  84. data/spec/grape/validations/validators/all_or_none_spec.rb +138 -30
  85. data/spec/grape/validations/validators/at_least_one_of_spec.rb +173 -29
  86. data/spec/grape/validations/validators/coerce_spec.rb +10 -2
  87. data/spec/grape/validations/validators/exactly_one_of_spec.rb +202 -38
  88. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +184 -27
  89. data/spec/grape/validations/validators/same_as_spec.rb +63 -0
  90. data/spec/grape/validations_spec.rb +33 -21
  91. data/spec/spec_helper.rb +4 -1
  92. metadata +35 -23
  93. data/Appraisals +0 -32
  94. data/Dangerfile +0 -2
  95. data/Gemfile +0 -33
  96. data/Gemfile.lock +0 -231
  97. data/Guardfile +0 -10
  98. data/RELEASING.md +0 -111
  99. data/Rakefile +0 -25
  100. data/benchmark/simple.rb +0 -27
  101. data/benchmark/simple_with_type_coercer.rb +0 -22
  102. data/gemfiles/multi_json.gemfile +0 -35
  103. data/gemfiles/multi_xml.gemfile +0 -35
  104. data/gemfiles/rack_1.5.2.gemfile +0 -35
  105. data/gemfiles/rack_edge.gemfile +0 -35
  106. data/gemfiles/rails_3.gemfile +0 -36
  107. data/gemfiles/rails_4.gemfile +0 -35
  108. data/gemfiles/rails_5.gemfile +0 -35
  109. data/gemfiles/rails_edge.gemfile +0 -35
  110. data/pkg/grape-0.17.0.gem +0 -0
  111. data/pkg/grape-0.19.0.gem +0 -0
@@ -2,65 +2,209 @@ require 'spec_helper'
2
2
 
3
3
  describe Grape::Validations::AtLeastOneOfValidator do
4
4
  describe '#validate!' do
5
- let(:scope) do
6
- Struct.new(:opts) do
7
- def params(arg)
8
- arg
9
- end
5
+ subject(:validate) { post path, params }
6
+
7
+ module ValidationsSpec
8
+ module AtLeastOneOfValidatorSpec
9
+ class API < Grape::API
10
+ rescue_from Grape::Exceptions::ValidationErrors do |e|
11
+ error!(e.errors.transform_keys! { |key| key.join(',') }, 400)
12
+ end
13
+
14
+ params do
15
+ optional :beer, :wine, :grapefruit
16
+ at_least_one_of :beer, :wine, :grapefruit
17
+ end
18
+ post do
19
+ end
20
+
21
+ params do
22
+ optional :beer, :wine, :grapefruit, :other
23
+ at_least_one_of :beer, :wine, :grapefruit
24
+ end
25
+ post 'mixed-params' do
26
+ end
27
+
28
+ params do
29
+ optional :beer, :wine, :grapefruit
30
+ at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something'
31
+ end
32
+ post '/custom-message' do
33
+ end
34
+
35
+ params do
36
+ requires :item, type: Hash do
37
+ optional :beer, :wine, :grapefruit
38
+ at_least_one_of :beer, :wine, :grapefruit, message: 'fail'
39
+ end
40
+ end
41
+ post '/nested-hash' do
42
+ end
10
43
 
11
- def required?; end
44
+ params do
45
+ requires :items, type: Array do
46
+ optional :beer, :wine, :grapefruit
47
+ at_least_one_of :beer, :wine, :grapefruit, message: 'fail'
48
+ end
49
+ end
50
+ post '/nested-array' do
51
+ end
52
+
53
+ params do
54
+ requires :items, type: Array do
55
+ requires :nested_items, type: Array do
56
+ optional :beer, :wine, :grapefruit
57
+ at_least_one_of :beer, :wine, :grapefruit, message: 'fail'
58
+ end
59
+ end
60
+ end
61
+ post '/deeply-nested-array' do
62
+ end
63
+ end
12
64
  end
13
65
  end
14
- let(:at_least_one_of_params) { %i[beer wine grapefruit] }
15
- let(:validator) { described_class.new(at_least_one_of_params, {}, false, scope.new) }
66
+
67
+ def app
68
+ ValidationsSpec::AtLeastOneOfValidatorSpec::API
69
+ end
16
70
 
17
71
  context 'when all restricted params are present' do
72
+ let(:path) { '/' }
18
73
  let(:params) { { beer: true, wine: true, grapefruit: true } }
19
74
 
20
- it 'does not raise a validation exception' do
21
- expect(validator.validate!(params)).to eql params
75
+ it 'does not return a validation error' do
76
+ validate
77
+ expect(last_response.status).to eq 201
22
78
  end
23
79
 
24
80
  context 'mixed with other params' do
25
- let(:mixed_params) { params.merge!(other: true, andanother: true) }
81
+ let(:path) { '/mixed-params' }
82
+ let(:params) { { beer: true, wine: true, grapefruit: true, other: true } }
26
83
 
27
- it 'does not raise a validation exception' do
28
- expect(validator.validate!(mixed_params)).to eql mixed_params
84
+ it 'does not return a validation error' do
85
+ validate
86
+ expect(last_response.status).to eq 201
29
87
  end
30
88
  end
31
89
  end
32
90
 
33
91
  context 'when a subset of restricted params are present' do
92
+ let(:path) { '/' }
34
93
  let(:params) { { beer: true, grapefruit: true } }
35
94
 
36
- it 'does not raise a validation exception' do
37
- expect(validator.validate!(params)).to eql params
95
+ it 'does not return a validation error' do
96
+ validate
97
+ expect(last_response.status).to eq 201
38
98
  end
39
99
  end
40
100
 
41
- context 'when params keys come as strings' do
42
- let(:params) { { 'beer' => true, 'grapefruit' => true } }
101
+ context 'when none of the restricted params is selected' do
102
+ let(:path) { '/' }
103
+ let(:params) { { other: true } }
104
+
105
+ it 'returns a validation error' do
106
+ validate
107
+ expect(last_response.status).to eq 400
108
+ expect(JSON.parse(last_response.body)).to eq(
109
+ 'beer,wine,grapefruit' => ['are missing, at least one parameter must be provided']
110
+ )
111
+ end
112
+
113
+ context 'when custom message is specified' do
114
+ let(:path) { '/custom-message' }
43
115
 
44
- it 'does not raise a validation exception' do
45
- expect(validator.validate!(params)).to eql params
116
+ it 'returns a validation error' do
117
+ validate
118
+ expect(last_response.status).to eq 400
119
+ expect(JSON.parse(last_response.body)).to eq(
120
+ 'beer,wine,grapefruit' => ['you should choose something']
121
+ )
122
+ end
46
123
  end
47
124
  end
48
125
 
49
- context 'when none of the restricted params is selected' do
50
- let(:params) { { somethingelse: true } }
126
+ context 'when exactly one of the restricted params is selected' do
127
+ let(:path) { '/' }
128
+ let(:params) { { beer: true } }
51
129
 
52
- it 'raises a validation exception' do
53
- expect do
54
- validator.validate! params
55
- end.to raise_error(Grape::Exceptions::Validation)
130
+ it 'does not return a validation error' do
131
+ validate
132
+ expect(last_response.status).to eq 201
56
133
  end
57
134
  end
58
135
 
59
- context 'when exactly one of the restricted params is selected' do
60
- let(:params) { { beer: true, somethingelse: true } }
136
+ context 'when restricted params are nested inside hash' do
137
+ let(:path) { '/nested-hash' }
138
+
139
+ context 'when at least one of them is present' do
140
+ let(:params) { { item: { beer: true, wine: true } } }
141
+
142
+ it 'does not return a validation error' do
143
+ validate
144
+ expect(last_response.status).to eq 201
145
+ end
146
+ end
61
147
 
62
- it 'does not raise a validation exception' do
63
- expect(validator.validate!(params)).to eql params
148
+ context 'when none of them are present' do
149
+ let(:params) { { item: { other: true } } }
150
+
151
+ it 'returns a validation error with full names of the params' do
152
+ validate
153
+ expect(last_response.status).to eq 400
154
+ expect(JSON.parse(last_response.body)).to eq(
155
+ 'item[beer],item[wine],item[grapefruit]' => ['fail']
156
+ )
157
+ end
158
+ end
159
+ end
160
+
161
+ context 'when restricted params are nested inside array' do
162
+ let(:path) { '/nested-array' }
163
+
164
+ context 'when at least one of them is present' do
165
+ let(:params) { { items: [{ beer: true, wine: true }, { grapefruit: true }] } }
166
+
167
+ it 'does not return a validation error' do
168
+ validate
169
+ expect(last_response.status).to eq 201
170
+ end
171
+ end
172
+
173
+ context 'when none of them are present' do
174
+ let(:params) { { items: [{ beer: true, other: true }, { other: true }] } }
175
+
176
+ it 'returns a validation error with full names of the params' do
177
+ validate
178
+ expect(last_response.status).to eq 400
179
+ expect(JSON.parse(last_response.body)).to eq(
180
+ 'items[1][beer],items[1][wine],items[1][grapefruit]' => ['fail']
181
+ )
182
+ end
183
+ end
184
+ end
185
+
186
+ context 'when restricted params are deeply nested' do
187
+ let(:path) { '/deeply-nested-array' }
188
+
189
+ context 'when at least one of them is present' do
190
+ let(:params) { { items: [{ nested_items: [{ wine: true }] }] } }
191
+
192
+ it 'does not return a validation error' do
193
+ validate
194
+ expect(last_response.status).to eq 201
195
+ end
196
+ end
197
+
198
+ context 'when none of them are present' do
199
+ let(:params) { { items: [{ nested_items: [{ other: true }] }] } }
200
+
201
+ it 'returns a validation error with full names of the params' do
202
+ validate
203
+ expect(last_response.status).to eq 400
204
+ expect(JSON.parse(last_response.body)).to eq(
205
+ 'items[0][nested_items][0][beer],items[0][nested_items][0][wine],items[0][nested_items][0][grapefruit]' => ['fail']
206
+ )
207
+ end
64
208
  end
65
209
  end
66
210
  end
@@ -20,10 +20,13 @@ describe Grape::Validations::CoerceValidator do
20
20
 
21
21
  context 'i18n' do
22
22
  after :each do
23
+ I18n.available_locales = %i[en]
23
24
  I18n.locale = :en
25
+ I18n.default_locale = :en
24
26
  end
25
27
 
26
28
  it 'i18n error on malformed input' do
29
+ I18n.available_locales = %i[en zh-CN]
27
30
  I18n.load_path << File.expand_path('../zh-CN.yml', __FILE__)
28
31
  I18n.reload!
29
32
  I18n.locale = 'zh-CN'.to_sym
@@ -40,6 +43,7 @@ describe Grape::Validations::CoerceValidator do
40
43
  end
41
44
 
42
45
  it 'gives an english fallback error when default locale message is blank' do
46
+ I18n.available_locales = %i[en pt-BR]
43
47
  I18n.locale = 'pt-BR'.to_sym
44
48
  subject.params do
45
49
  requires :age, type: Integer
@@ -397,6 +401,10 @@ describe Grape::Validations::CoerceValidator do
397
401
  post '/upload', file: 'not a file'
398
402
  expect(last_response.status).to eq(400)
399
403
  expect(last_response.body).to eq('file is invalid')
404
+
405
+ post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
406
+ expect(last_response.status).to eq(400)
407
+ expect(last_response.body).to eq('file is invalid')
400
408
  end
401
409
 
402
410
  it 'Nests integers' do
@@ -572,7 +580,7 @@ describe Grape::Validations::CoerceValidator do
572
580
  expect(last_response.status).to eq(200)
573
581
  expect(last_response.body).to eq('arrays work')
574
582
 
575
- get '/', splines: [{ x: 2, ints: [] }, { x: 3, ints: [4], obj: { y: 'quack' } }]
583
+ get '/', splines: [{ x: 2, ints: [5] }, { x: 3, ints: [4], obj: { y: 'quack' } }]
576
584
  expect(last_response.status).to eq(200)
577
585
  expect(last_response.body).to eq('arrays work')
578
586
 
@@ -588,7 +596,7 @@ describe Grape::Validations::CoerceValidator do
588
596
  expect(last_response.status).to eq(400)
589
597
  expect(last_response.body).to eq('splines[x] does not have a valid value')
590
598
 
591
- get '/', splines: [{ x: 1, ints: [] }, { x: 4, ints: [] }]
599
+ get '/', splines: [{ x: 1, ints: [5] }, { x: 4, ints: [6] }]
592
600
  expect(last_response.status).to eq(400)
593
601
  expect(last_response.body).to eq('splines[x] does not have a valid value')
594
602
  end
@@ -2,73 +2,237 @@ require 'spec_helper'
2
2
 
3
3
  describe Grape::Validations::ExactlyOneOfValidator do
4
4
  describe '#validate!' do
5
- let(:scope) do
6
- Struct.new(:opts) do
7
- def params(arg)
8
- arg
9
- end
5
+ subject(:validate) { post path, params }
6
+
7
+ module ValidationsSpec
8
+ module ExactlyOneOfValidatorSpec
9
+ class API < Grape::API
10
+ rescue_from Grape::Exceptions::ValidationErrors do |e|
11
+ error!(e.errors.transform_keys! { |key| key.join(',') }, 400)
12
+ end
13
+
14
+ params do
15
+ optional :beer
16
+ optional :wine
17
+ optional :grapefruit
18
+ exactly_one_of :beer, :wine, :grapefruit
19
+ end
20
+ post do
21
+ end
22
+
23
+ params do
24
+ optional :beer
25
+ optional :wine
26
+ optional :grapefruit
27
+ optional :other
28
+ exactly_one_of :beer, :wine, :grapefruit
29
+ end
30
+ post 'mixed-params' do
31
+ end
32
+
33
+ params do
34
+ optional :beer
35
+ optional :wine
36
+ optional :grapefruit
37
+ exactly_one_of :beer, :wine, :grapefruit, message: 'you should choose one'
38
+ end
39
+ post '/custom-message' do
40
+ end
10
41
 
11
- def required?; end
42
+ params do
43
+ requires :item, type: Hash do
44
+ optional :beer
45
+ optional :wine
46
+ optional :grapefruit
47
+ exactly_one_of :beer, :wine, :grapefruit
48
+ end
49
+ end
50
+ post '/nested-hash' do
51
+ end
52
+
53
+ params do
54
+ optional :item, type: Hash do
55
+ optional :beer
56
+ optional :wine
57
+ optional :grapefruit
58
+ exactly_one_of :beer, :wine, :grapefruit
59
+ end
60
+ end
61
+ post '/nested-optional-hash' do
62
+ end
63
+
64
+ params do
65
+ requires :items, type: Array do
66
+ optional :beer
67
+ optional :wine
68
+ optional :grapefruit
69
+ exactly_one_of :beer, :wine, :grapefruit
70
+ end
71
+ end
72
+ post '/nested-array' do
73
+ end
74
+
75
+ params do
76
+ requires :items, type: Array do
77
+ requires :nested_items, type: Array do
78
+ optional :beer, :wine, :grapefruit, type: Boolean
79
+ exactly_one_of :beer, :wine, :grapefruit
80
+ end
81
+ end
82
+ end
83
+ post '/deeply-nested-array' do
84
+ end
85
+ end
12
86
  end
13
87
  end
14
- let(:exactly_one_of_params) { %i[beer wine grapefruit] }
15
- let(:validator) { described_class.new(exactly_one_of_params, {}, false, scope.new) }
16
88
 
17
- context 'when all restricted params are present' do
89
+ def app
90
+ ValidationsSpec::ExactlyOneOfValidatorSpec::API
91
+ end
92
+
93
+ context 'when all params are present' do
94
+ let(:path) { '/' }
18
95
  let(:params) { { beer: true, wine: true, grapefruit: true } }
19
96
 
20
- it 'raises a validation exception' do
21
- expect do
22
- validator.validate! params
23
- end.to raise_error(Grape::Exceptions::Validation)
97
+ it 'returns a validation error' do
98
+ validate
99
+ expect(last_response.status).to eq 400
100
+ expect(JSON.parse(last_response.body)).to eq(
101
+ 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided']
102
+ )
24
103
  end
25
104
 
26
105
  context 'mixed with other params' do
27
- let(:mixed_params) { params.merge!(other: true, andanother: true) }
106
+ let(:path) { '/mixed-params' }
107
+ let(:params) { { beer: true, wine: true, grapefruit: true, other: true } }
28
108
 
29
- it 'still raises a validation exception' do
30
- expect do
31
- validator.validate! mixed_params
32
- end.to raise_error(Grape::Exceptions::Validation)
109
+ it 'returns a validation error' do
110
+ validate
111
+ expect(last_response.status).to eq 400
112
+ expect(JSON.parse(last_response.body)).to eq(
113
+ 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided']
114
+ )
33
115
  end
34
116
  end
35
117
  end
36
118
 
37
- context 'when a subset of restricted params are present' do
119
+ context 'when a subset of params are present' do
120
+ let(:path) { '/' }
38
121
  let(:params) { { beer: true, grapefruit: true } }
39
122
 
40
- it 'raises a validation exception' do
41
- expect do
42
- validator.validate! params
43
- end.to raise_error(Grape::Exceptions::Validation)
123
+ it 'returns a validation error' do
124
+ validate
125
+ expect(last_response.status).to eq 400
126
+ expect(JSON.parse(last_response.body)).to eq(
127
+ 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided']
128
+ )
44
129
  end
45
130
  end
46
131
 
47
- context 'when params keys come as strings' do
48
- let(:params) { { 'beer' => true, 'grapefruit' => true } }
132
+ context 'when custom message is specified' do
133
+ let(:path) { '/custom-message' }
134
+ let(:params) { { beer: true, wine: true } }
49
135
 
50
- it 'raises a validation exception' do
51
- expect do
52
- validator.validate! params
53
- end.to raise_error(Grape::Exceptions::Validation)
136
+ it 'returns a validation error' do
137
+ validate
138
+ expect(last_response.status).to eq 400
139
+ expect(JSON.parse(last_response.body)).to eq(
140
+ 'beer,wine,grapefruit' => ['you should choose one']
141
+ )
54
142
  end
55
143
  end
56
144
 
57
- context 'when none of the restricted params is selected' do
145
+ context 'when exacly one param is present' do
146
+ let(:path) { '/' }
147
+ let(:params) { { beer: true, somethingelse: true } }
148
+
149
+ it 'does not return a validation error' do
150
+ validate
151
+ expect(last_response.status).to eq 201
152
+ end
153
+ end
154
+
155
+ context 'when none of the params are present' do
156
+ let(:path) { '/' }
58
157
  let(:params) { { somethingelse: true } }
59
158
 
60
- it 'raises a validation exception' do
61
- expect do
62
- validator.validate! params
63
- end.to raise_error(Grape::Exceptions::Validation)
159
+ it 'returns a validation error' do
160
+ validate
161
+ expect(last_response.status).to eq 400
162
+ expect(JSON.parse(last_response.body)).to eq(
163
+ 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided']
164
+ )
64
165
  end
65
166
  end
66
167
 
67
- context 'when exactly one of the restricted params is selected' do
68
- let(:params) { { beer: true, somethingelse: true } }
168
+ context 'when params are nested inside required hash' do
169
+ let(:path) { '/nested-hash' }
170
+ let(:params) { { item: { beer: true, wine: true } } }
171
+
172
+ it 'returns a validation error with full names of the params' do
173
+ validate
174
+ expect(last_response.status).to eq 400
175
+ expect(JSON.parse(last_response.body)).to eq(
176
+ 'item[beer],item[wine],item[grapefruit]' => ['are missing, exactly one parameter must be provided']
177
+ )
178
+ end
179
+ end
180
+
181
+ context 'when params are nested inside optional hash' do
182
+ let(:path) { '/nested-optional-hash' }
183
+
184
+ context 'when params are passed' do
185
+ let(:params) { { item: { beer: true, wine: true } } }
186
+
187
+ it 'returns a validation error with full names of the params' do
188
+ validate
189
+ expect(last_response.status).to eq 400
190
+ expect(JSON.parse(last_response.body)).to eq(
191
+ 'item[beer],item[wine],item[grapefruit]' => ['are missing, exactly one parameter must be provided']
192
+ )
193
+ end
194
+ end
195
+
196
+ context 'when params are empty' do
197
+ let(:params) { { other: true } }
198
+
199
+ it 'does not return a validation error' do
200
+ validate
201
+ expect(last_response.status).to eq 201
202
+ end
203
+ end
204
+ end
205
+
206
+ context 'when params are nested inside array' do
207
+ let(:path) { '/nested-array' }
208
+ let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } }
209
+
210
+ it 'returns a validation error with full names of the params' do
211
+ validate
212
+ expect(last_response.status).to eq 400
213
+ expect(JSON.parse(last_response.body)).to eq(
214
+ 'items[0][beer],items[0][wine],items[0][grapefruit]' => [
215
+ 'are missing, exactly one parameter must be provided'
216
+ ],
217
+ 'items[1][beer],items[1][wine],items[1][grapefruit]' => [
218
+ 'are missing, exactly one parameter must be provided'
219
+ ]
220
+ )
221
+ end
222
+ end
223
+
224
+ context 'when params are deeply nested' do
225
+ let(:path) { '/deeply-nested-array' }
226
+ let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } }
69
227
 
70
- it 'does not raise a validation exception' do
71
- expect(validator.validate!(params)).to eql params
228
+ it 'returns a validation error with full names of the params' do
229
+ validate
230
+ expect(last_response.status).to eq 400
231
+ expect(JSON.parse(last_response.body)).to eq(
232
+ 'items[0][nested_items][0][beer],items[0][nested_items][0][wine],items[0][nested_items][0][grapefruit]' => [
233
+ 'are missing, exactly one parameter must be provided'
234
+ ]
235
+ )
72
236
  end
73
237
  end
74
238
  end