Skip to content

Commit 1eee799

Browse files
committed
Merge pull request #637 from Morred/exactly_one_of
added exactly_one_of validation
2 parents 597fabf + f9f61e5 commit 1eee799

File tree

7 files changed

+149
-0
lines changed

7 files changed

+149
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Next Release
33

44
#### Features
55

6+
* [#637](https://github.com/intridea/grape/pull/637): Added 'exactly_one_of' validation - [@Morred](https://github.com/Morred).
67
* [#626](https://github.com/intridea/grape/pull/626): Mutually exclusive params - [@oliverbarnes](https://github.com/oliverbarnes).
78
* [#617](https://github.com/intridea/grape/pull/617): Running tests on Ruby 2.1.1, Rubinius 2.1 and 2.2, Ruby and JRuby HEAD - [@dblock](https://github.com/dblock).
89
* [#397](https://github.com/intridea/grape/pull/397): Adds `Grape::Endpoint.before_each` to allow easy helper stubbing - [@mbleigh](https://github.com/mbleigh).

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,16 @@ end
500500

501501
**Warning**: Never define mutually exclusive sets with any required params. Two mutually exclusive required params will mean params are never valid, thus making the endpoint useless. One required param mutually exclusive with an optional param will mean the latter is never valid.
502502

503+
Parameters can be defined as 'exactly_one_of', ensuring that exactly one parameter gets selected.
504+
505+
```ruby
506+
params do
507+
optional :beer
508+
optional :wine
509+
exactly_one_of :beer, :wine
510+
end
511+
```
512+
503513
### Namespace Validation and Coercion
504514

505515
Namespaces allow parameter definitions and apply to every method within the namespace.

lib/grape/locale/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ en:
2929
unknown_options: 'unknown options: %{options}'
3030
incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
3131
mutual_exclusion: 'are mutually exclusive'
32+
exactly_one: "- exactly one parameter must be provided"

lib/grape/validations.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ def mutually_exclusive(*attrs)
134134
validates(attrs, mutual_exclusion: true)
135135
end
136136

137+
def exactly_one_of(*attrs)
138+
validates(attrs, exactly_one_of: true)
139+
end
140+
137141
def group(*attrs, &block)
138142
requires(*attrs, &block)
139143
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module Grape
2+
module Validations
3+
require 'grape/validations/mutual_exclusion'
4+
class ExactlyOneOfValidator < MutualExclusionValidator
5+
attr_reader :params
6+
7+
def validate!(params)
8+
super
9+
if none_of_restricted_params_is_present
10+
raise Grape::Exceptions::Validation, param: "#{all_keys}", message_key: :exactly_one
11+
end
12+
params
13+
end
14+
15+
private
16+
17+
def none_of_restricted_params_is_present
18+
keys_in_common.length < 1
19+
end
20+
21+
def all_keys
22+
attrs.map(&:to_sym)
23+
end
24+
end
25+
end
26+
end
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
require 'spec_helper'
2+
3+
describe Grape::Validations::ExactlyOneOfValidator do
4+
describe '#validate!' do
5+
let(:scope) do
6+
Struct.new(:opts) do
7+
def params(arg); end
8+
end
9+
end
10+
let(:exactly_one_of_params) { [:beer, :wine, :grapefruit] }
11+
let(:validator) { described_class.new(exactly_one_of_params, {}, false, scope.new) }
12+
13+
context 'when all restricted params are present' do
14+
let(:params) { { beer: true, wine: true, grapefruit: true } }
15+
16+
it 'raises a validation exception' do
17+
expect {
18+
validator.validate! params
19+
}.to raise_error(Grape::Exceptions::Validation)
20+
end
21+
22+
context 'mixed with other params' do
23+
let(:mixed_params) { params.merge!(other: true, andanother: true) }
24+
25+
it 'still raises a validation exception' do
26+
expect {
27+
validator.validate! mixed_params
28+
}.to raise_error(Grape::Exceptions::Validation)
29+
end
30+
end
31+
end
32+
33+
context 'when a subset of restricted params are present' do
34+
let(:params) { { beer: true, grapefruit: true } }
35+
36+
it 'raises a validation exception' do
37+
expect {
38+
validator.validate! params
39+
}.to raise_error(Grape::Exceptions::Validation)
40+
end
41+
end
42+
43+
context 'when params keys come as strings' do
44+
let(:params) { { 'beer' => true, 'grapefruit' => true } }
45+
46+
it 'raises a validation exception' do
47+
expect {
48+
validator.validate! params
49+
}.to raise_error(Grape::Exceptions::Validation)
50+
end
51+
end
52+
53+
context 'when none of the restricted params is selected' do
54+
let(:params) { { somethingelse: true } }
55+
56+
it 'raises a validation exception' do
57+
expect {
58+
validator.validate! params
59+
}.to raise_error(Grape::Exceptions::Validation)
60+
end
61+
end
62+
63+
context 'when exactly one of the restricted params is selected' do
64+
let(:params) { { beer: true, somethingelse: true } }
65+
66+
it 'params' do
67+
expect(validator.validate!(params)).to eql params
68+
end
69+
end
70+
end
71+
end

spec/grape/validations_spec.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,5 +878,41 @@ module SharedParams
878878
end
879879
end
880880
end
881+
882+
context 'exactly one of' do
883+
context 'params' do
884+
it 'errors when two or more are present' do
885+
subject.params do
886+
optional :beer
887+
optional :wine
888+
optional :juice
889+
exactly_one_of :beer, :wine, :juice
890+
end
891+
subject.get '/exactly_one_of' do
892+
'exactly_one_of works!'
893+
end
894+
895+
get '/exactly_one_of', beer: 'string', wine: 'anotherstring'
896+
expect(last_response.status).to eq(400)
897+
expect(last_response.body).to eq("[:beer, :wine] are mutually exclusive")
898+
end
899+
900+
it 'errors when none is selected' do
901+
subject.params do
902+
optional :beer
903+
optional :wine
904+
optional :juice
905+
exactly_one_of :beer, :wine, :juice
906+
end
907+
subject.get '/exactly_one_of' do
908+
'exactly_one_of works!'
909+
end
910+
911+
get '/exactly_one_of'
912+
expect(last_response.status).to eq(400)
913+
expect(last_response.body).to eq("[:beer, :wine, :juice] - exactly one parameter must be provided")
914+
end
915+
end
916+
end
881917
end
882918
end

0 commit comments

Comments
 (0)