Skip to content

Commit

Permalink
Merge pull request #626 from oliverbarnes/mutually_exclusive
Browse files Browse the repository at this point in the history
Add mutually_exclusive params validation
  • Loading branch information
dblock committed Apr 15, 2014
2 parents c706c8a + 1b8263c commit 1e1b1f7
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Next Release

#### Features

* [#626](https://github.com/intridea/grape/pull/626): Mutually exclusive params - [@oliverbarnes](https://github.com/oliverbarnes).
* [#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).
* [#397](https://github.com/intridea/grape/pull/397): Adds `Grape::Endpoint.before_each` to allow easy helper stubbing - [@mbleigh](https://github.com/mbleigh).
* Your contribution here.
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ params do
optional :audio do
requires :format, type: Symbol, values: [:mp3, :wav, :aac, :ogg], default: :mp3
end
mutually_exclusive :media, :audio
end
put ':id' do
# params[:id] is an Integer
Expand Down Expand Up @@ -473,6 +474,31 @@ params do
end
```

Parameters can be defined as `mutually_exclusive`, ensuring that they aren't present at the same time in a request.

```ruby
params do
optional :beer
optional :wine
mutually_exclusive :beer, :wine
end
```

Multiple sets can be defined:

```ruby
params do
optional :beer
optional :wine
mutually_exclusive :beer, :wine
optional :scotch
optional :aquavit
mutually_exclusive :scotch, :aquavit
end
```

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

### Namespace Validation and Coercion

Namespaces allow parameter definitions and apply to every method within the namespace.
Expand Down
2 changes: 1 addition & 1 deletion lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ def run(env)

run_filters before_validations

# Retieve validations from this namespace and all parent namespaces.
# Retrieve validations from this namespace and all parent namespaces.
validation_errors = []
settings.gather(:validations).each do |validator|
begin
Expand Down
1 change: 1 addition & 0 deletions lib/grape/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ en:
unknown_validator: 'unknown validator: %{validator_type}'
unknown_options: 'unknown options: %{options}'
incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
mutual_exclusion: 'are mutually exclusive'
4 changes: 4 additions & 0 deletions lib/grape/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ def optional(*attrs, &block)
push_declared_params(attrs)
end

def mutually_exclusive(*attrs)
validates(attrs, mutual_exclusion: true)
end

def group(*attrs, &block)
requires(*attrs, &block)
end
Expand Down
25 changes: 25 additions & 0 deletions lib/grape/validations/mutual_exclusion.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Grape
module Validations
class MutualExclusionValidator < Validator
attr_reader :params

def validate!(params)
@params = params
if two_or_more_exclusive_params_are_present
raise Grape::Exceptions::Validation, param: "#{keys_in_common.map(&:to_sym)}", message_key: :mutual_exclusion
end
params
end

private

def two_or_more_exclusive_params_are_present
keys_in_common.length > 1
end

def keys_in_common
attrs.map(&:to_s) & params.stringify_keys.keys
end
end
end
end
2 changes: 1 addition & 1 deletion spec/grape/middleware/versioner/param_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
expect(subject.call(env)[1]["api.version"]).to eq('v1')
end

it 'cuts (only) the version out of the params', focus: true do
it 'cuts (only) the version out of the params' do
env = Rack::MockRequest.env_for("/awesome", params: { "apiver" => "v1", "other_param" => "5" })
env['rack.request.query_hash'] = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
expect(subject.call(env)[1]['rack.request.query_hash']["apiver"]).to be_nil
Expand Down
61 changes: 61 additions & 0 deletions spec/grape/validations/mutual_exclusion_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
require 'spec_helper'

describe Grape::Validations::MutualExclusionValidator do
describe '#validate!' do
let(:scope) do
Struct.new(:opts) do
def params(arg); end
end
end
let(:mutually_exclusive_params) { [:beer, :wine, :grapefruit] }
let(:validator) { described_class.new(mutually_exclusive_params, {}, false, scope.new) }

context 'when all mutually exclusive params are present' do
let(:params) { { beer: true, wine: true, grapefruit: true } }

it 'raises a validation exception' do
expect {
validator.validate! params
}.to raise_error(Grape::Exceptions::Validation)
end

context 'mixed with other params' do
let(:mixed_params) { params.merge!(other: true, andanother: true) }

it 'still raises a validation exception' do
expect {
validator.validate! mixed_params
}.to raise_error(Grape::Exceptions::Validation)
end
end
end

context 'when a subset of mutually exclusive params are present' do
let(:params) { { beer: true, grapefruit: true } }

it 'raises a validation exception' do
expect {
validator.validate! params
}.to raise_error(Grape::Exceptions::Validation)
end
end

context 'when params keys come as strings' do
let(:params) { { 'beer' => true, 'grapefruit' => true } }

it 'raises a validation exception' do
expect {
validator.validate! params
}.to raise_error(Grape::Exceptions::Validation)
end
end

context 'when no mutually exclusive params are present' do
let(:params) { { beer: true, somethingelse: true } }

it 'params' do
expect(validator.validate!(params)).to eql params
end
end
end
end
41 changes: 41 additions & 0 deletions spec/grape/validations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -837,5 +837,46 @@ module SharedParams
expect(subject.routes.first.route_params['first_name'][:documentation]).to eq(documentation)
end
end

context 'mutually exclusive' do
context 'optional params' do
it 'errors when two or more are present' do
subject.params do
optional :beer
optional :wine
optional :juice
mutually_exclusive :beer, :wine, :juice
end
subject.get '/mutually_exclusive' do
'mutually_exclusive works!'
end

get '/mutually_exclusive', beer: 'string', wine: 'anotherstring'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq("[:beer, :wine] are mutually exclusive")
end
end

context 'more than one set of mutually exclusive params' do
it 'errors for all sets' do
subject.params do
optional :beer
optional :wine
mutually_exclusive :beer, :wine
optional :scotch
optional :aquavit
mutually_exclusive :scotch, :aquavit
end
subject.get '/mutually_exclusive' do
'mutually_exclusive works!'
end

get '/mutually_exclusive', beer: 'true', wine: 'true', scotch: 'true', aquavit: 'true'
expect(last_response.status).to eq(400)
expect(last_response.body).to match(/\[:beer, :wine\] are mutually exclusive/)
expect(last_response.body).to match(/\[:scotch, :aquavit\] are mutually exclusive/)
end
end
end
end
end

0 comments on commit 1e1b1f7

Please sign in to comment.