Skip to content

Commit 732d81e

Browse files
committed
Extended mutually_exclusive, exactly_one_of, at_least_one_of to work in group
change mutually_exclusive to work in scope change exactly_one_of to work in scope change at_least_one_of to work in scope all of those methods changed to work for Hash and Array all of those methods changed to work for requires and optional scope add and change specs
1 parent 43f25db commit 732d81e

11 files changed

+188
-44
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
0.9.1 (Next)
22
============
33

4+
* [#774](https://github.com/intridea/grape/pull/774): Extended `mutually_exclusive`, `exactly_one_of`, `at_least_one_of` to work inside any kind of group: `requires` or `optional`, `Hash` or `Array` - [@ShPakvel](https://github.com/ShPakvel).
45
* [#743](https://github.com/intridea/grape/pull/743): Added `allow_blank` parameter validator to validate non-empty strings - [@elado](https://github.com/elado).
56
* Your contribution here.
67
* [#745](https://github.com/intridea/grape/pull/745): Removed `atom+xml`, `rss+xml`, and `jsonapi` content-types - [@akabraham](https://github.com/akabraham).

README.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ module Twitter
110110
class API < Grape::API
111111
version 'v1', using: :header, vendor: 'twitter'
112112
format :json
113+
prefix :api
113114

114115
helpers do
115116
def current_user
@@ -620,6 +621,32 @@ params do
620621
end
621622
```
622623

624+
#### Nested `mutually_exclusive`, `exactly_one_of`, `at_least_one_of`
625+
626+
All of these methods can be used at any nested level.
627+
628+
```ruby
629+
params do
630+
requires :food do
631+
optional :meat
632+
optional :fish
633+
optional :rice
634+
at_least_one_of :meat, :fish, :rice
635+
end
636+
group :drink do
637+
optional :beer
638+
optional :wine
639+
optional :juice
640+
exactly_one_of :beer, :wine, :juice
641+
end
642+
optional :dessert do
643+
optional :cake
644+
optional :icecream
645+
mutually_exclusive :cake, :icecream
646+
end
647+
end
648+
```
649+
623650
### Namespace Validation and Coercion
624651

625652
Namespaces allow parameter definitions and apply to every method within the namespace.
@@ -1812,17 +1839,17 @@ describe Twitter::API do
18121839
end
18131840

18141841
describe Twitter::API do
1815-
describe "GET /api/v1/statuses" do
1842+
describe "GET /api/statuses/public_timeline" do
18161843
it "returns an empty array of statuses" do
1817-
get "/api/v1/statuses"
1844+
get "/api/statuses/public_timeline"
18181845
expect(last_response.status).to eq(200)
18191846
expect(JSON.parse(last_response.body)).to eq []
18201847
end
18211848
end
1822-
describe "GET /api/v1/statuses/:id" do
1849+
describe "GET /api/statuses/:id" do
18231850
it "returns a status by id" do
18241851
status = Status.create!
1825-
get "/api/v1/statuses/#{status.id}"
1852+
get "/api/statuses/#{status.id}"
18261853
expect(last_response.body).to eq status.to_json
18271854
end
18281855
end
@@ -1834,17 +1861,17 @@ end
18341861

18351862
```ruby
18361863
describe Twitter::API do
1837-
describe "GET /api/v1/statuses" do
1864+
describe "GET /api/statuses/public_timeline" do
18381865
it "returns an empty array of statuses" do
1839-
get "/api/v1/statuses"
1866+
get "/api/statuses/public_timeline"
18401867
expect(response.status).to eq(200)
18411868
expect(JSON.parse(response.body)).to eq []
18421869
end
18431870
end
1844-
describe "GET /api/v1/statuses/:id" do
1871+
describe "GET /api/statuses/:id" do
18451872
it "returns a status by id" do
18461873
status = Status.create!
1847-
get "/api/v1/statuses/#{status.id}"
1874+
get "/api/statuses/#{status.id}"
18481875
expect(response.body).to eq status.to_json
18491876
end
18501877
end

lib/grape/validations/params_scope.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ def root?
3333
!@parent
3434
end
3535

36+
def required?
37+
!@optional
38+
end
39+
3640
protected
3741

3842
def push_declared_params(attrs)

lib/grape/validations/validators/at_least_one_of.rb

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
module Grape
22
module Validations
3-
class AtLeastOneOfValidator < Base
4-
attr_reader :params
5-
3+
require 'grape/validations/validators/multiple_params_base'
4+
class AtLeastOneOfValidator < MultipleParamsBase
65
def validate!(params)
7-
@params = params
8-
if no_exclusive_params_are_present
9-
raise Grape::Exceptions::Validation, params: attrs.map(&:to_s), message_key: :at_least_one
6+
super
7+
if scope_requires_params && no_exclusive_params_are_present
8+
raise Grape::Exceptions::Validation, params: all_keys, message_key: :at_least_one
109
end
1110
params
1211
end
1312

1413
private
1514

1615
def no_exclusive_params_are_present
17-
keys_in_common.length == 0
18-
end
19-
20-
def keys_in_common
21-
(attrs.map(&:to_s) & params.stringify_keys.keys).map(&:to_s)
16+
scoped_params.any? { |resource_params| keys_in_common(resource_params).length == 0 }
2217
end
2318
end
2419
end

lib/grape/validations/validators/exactly_one_of.rb

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ module Grape
22
module Validations
33
require 'grape/validations/validators/mutual_exclusion'
44
class ExactlyOneOfValidator < MutualExclusionValidator
5-
attr_reader :params
6-
75
def validate!(params)
86
super
9-
if none_of_restricted_params_is_present
7+
if scope_requires_params && none_of_restricted_params_is_present
108
raise Grape::Exceptions::Validation, params: all_keys, message_key: :exactly_one
119
end
1210
params
@@ -15,11 +13,7 @@ def validate!(params)
1513
private
1614

1715
def none_of_restricted_params_is_present
18-
keys_in_common.length < 1
19-
end
20-
21-
def all_keys
22-
attrs.map(&:to_s)
16+
scoped_params.any? { |resource_params| keys_in_common(resource_params).length < 1 }
2317
end
2418
end
2519
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+
class MultipleParamsBase < Base
4+
attr_reader :scoped_params
5+
6+
def validate!(params)
7+
@scoped_params = [@scope.params(params)].flatten
8+
params
9+
end
10+
11+
private
12+
13+
def scope_requires_params
14+
@scope.required? || scoped_params.any? { |resource_params| resource_params.length > 0 }
15+
end
16+
17+
def keys_in_common(resource_params)
18+
(all_keys & resource_params.stringify_keys.keys).map(&:to_s)
19+
end
20+
21+
def all_keys
22+
attrs.map(&:to_s)
23+
end
24+
end
25+
end
26+
end

lib/grape/validations/validators/mutual_exclusion.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
module Grape
22
module Validations
3-
class MutualExclusionValidator < Base
4-
attr_reader :params
3+
require 'grape/validations/validators/multiple_params_base'
4+
class MutualExclusionValidator < MultipleParamsBase
5+
attr_reader :processing_keys_in_common
56

67
def validate!(params)
7-
@params = params
8+
super
89
if two_or_more_exclusive_params_are_present
9-
raise Grape::Exceptions::Validation, params: keys_in_common, message_key: :mutual_exclusion
10+
raise Grape::Exceptions::Validation, params: processing_keys_in_common, message_key: :mutual_exclusion
1011
end
1112
params
1213
end
1314

1415
private
1516

1617
def two_or_more_exclusive_params_are_present
17-
keys_in_common.length > 1
18-
end
19-
20-
def keys_in_common
21-
(attrs.map(&:to_s) & params.stringify_keys.keys).map(&:to_s)
18+
scoped_params.any? do |resource_params|
19+
@processing_keys_in_common = keys_in_common(resource_params)
20+
@processing_keys_in_common.length > 1
21+
end
2222
end
2323
end
2424
end

spec/grape/validations/validators/at_least_one_of_spec.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
describe '#validate!' do
55
let(:scope) do
66
Struct.new(:opts) do
7-
def params(arg); end
7+
def params(arg)
8+
arg
9+
end
10+
11+
def required?; end
812
end
913
end
1014
let(:at_least_one_of_params) { [:beer, :wine, :grapefruit] }

spec/grape/validations/validators/exactly_one_of_spec.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
describe '#validate!' do
55
let(:scope) do
66
Struct.new(:opts) do
7-
def params(arg); end
7+
def params(arg)
8+
arg
9+
end
10+
11+
def required?; end
812
end
913
end
1014
let(:exactly_one_of_params) { [:beer, :wine, :grapefruit] }

spec/grape/validations/validators/mutual_exclusion_spec.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
describe '#validate!' do
55
let(:scope) do
66
Struct.new(:opts) do
7-
def params(arg); end
7+
def params(arg)
8+
arg
9+
end
810
end
911
end
1012
let(:mutually_exclusive_params) { [:beer, :wine, :grapefruit] }

spec/grape/validations_spec.rb

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -980,17 +980,24 @@ module SharedParams
980980
optional :beer
981981
optional :wine
982982
mutually_exclusive :beer, :wine
983-
optional :scotch
984-
optional :aquavit
985-
mutually_exclusive :scotch, :aquavit
983+
optional :nested, type: Hash do
984+
optional :scotch
985+
optional :aquavit
986+
mutually_exclusive :scotch, :aquavit
987+
end
988+
optional :nested2, type: Array do
989+
optional :scotch2
990+
optional :aquavit2
991+
mutually_exclusive :scotch2, :aquavit2
992+
end
986993
end
987994
subject.get '/mutually_exclusive' do
988995
'mutually_exclusive works!'
989996
end
990997

991-
get '/mutually_exclusive', beer: 'true', wine: 'true', scotch: 'true', aquavit: 'true'
998+
get '/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }]
992999
expect(last_response.status).to eq(400)
993-
expect(last_response.body).to eq "beer, wine are mutually exclusive, scotch, aquavit are mutually exclusive"
1000+
expect(last_response.body).to eq "beer, wine are mutually exclusive, scotch, aquavit are mutually exclusive, scotch2, aquavit2 are mutually exclusive"
9941001
end
9951002
end
9961003
end
@@ -1027,6 +1034,46 @@ module SharedParams
10271034
expect(last_response.body).to eq "beer, wine are mutually exclusive"
10281035
end
10291036
end
1037+
1038+
context 'nested params' do
1039+
before :each do
1040+
subject.params do
1041+
requires :nested, type: Hash do
1042+
optional :beer_nested
1043+
optional :wine_nested
1044+
optional :juice_nested
1045+
exactly_one_of :beer_nested, :wine_nested, :juice_nested
1046+
end
1047+
optional :nested2, type: Array do
1048+
optional :beer_nested2
1049+
optional :wine_nested2
1050+
optional :juice_nested2
1051+
exactly_one_of :beer_nested2, :wine_nested2, :juice_nested2
1052+
end
1053+
end
1054+
subject.get '/exactly_one_of_nested' do
1055+
'exactly_one_of works!'
1056+
end
1057+
end
1058+
1059+
it 'errors when none are present' do
1060+
get '/exactly_one_of_nested'
1061+
expect(last_response.status).to eq(400)
1062+
expect(last_response.body).to eq "nested is missing, beer_nested, wine_nested, juice_nested are missing, exactly one parameter must be provided"
1063+
end
1064+
1065+
it 'succeeds when one is present' do
1066+
get '/exactly_one_of_nested', nested: { beer_nested: 'string' }
1067+
expect(last_response.status).to eq(200)
1068+
expect(last_response.body).to eq 'exactly_one_of works!'
1069+
end
1070+
1071+
it 'errors when two or more are present' do
1072+
get '/exactly_one_of_nested', nested: { beer_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'anotherstring' }]
1073+
expect(last_response.status).to eq(400)
1074+
expect(last_response.body).to eq "beer_nested2, wine_nested2 are mutually exclusive"
1075+
end
1076+
end
10301077
end
10311078

10321079
context 'at least one of' do
@@ -1061,6 +1108,46 @@ module SharedParams
10611108
expect(last_response.body).to eq 'at_least_one_of works!'
10621109
end
10631110
end
1111+
1112+
context 'nested params' do
1113+
before :each do
1114+
subject.params do
1115+
requires :nested, type: Hash do
1116+
optional :beer_nested
1117+
optional :wine_nested
1118+
optional :juice_nested
1119+
at_least_one_of :beer_nested, :wine_nested, :juice_nested
1120+
end
1121+
optional :nested2, type: Array do
1122+
optional :beer_nested2
1123+
optional :wine_nested2
1124+
optional :juice_nested2
1125+
at_least_one_of :beer_nested2, :wine_nested2, :juice_nested2
1126+
end
1127+
end
1128+
subject.get '/at_least_one_of_nested' do
1129+
'at_least_one_of works!'
1130+
end
1131+
end
1132+
1133+
it 'errors when none are present' do
1134+
get '/at_least_one_of_nested'
1135+
expect(last_response.status).to eq(400)
1136+
expect(last_response.body).to eq "nested is missing, beer_nested, wine_nested, juice_nested are missing, at least one parameter must be provided"
1137+
end
1138+
1139+
it 'does not error when one is present' do
1140+
get '/at_least_one_of_nested', nested: { beer_nested: 'string' }, nested2: [{ beer_nested2: 'string' }]
1141+
expect(last_response.status).to eq(200)
1142+
expect(last_response.body).to eq 'at_least_one_of works!'
1143+
end
1144+
1145+
it 'does not error when two are present' do
1146+
get '/at_least_one_of_nested', nested: { beer_nested: 'string', wine_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'string' }]
1147+
expect(last_response.status).to eq(200)
1148+
expect(last_response.body).to eq 'at_least_one_of works!'
1149+
end
1150+
end
10641151
end
10651152
end
10661153
end

0 commit comments

Comments
 (0)