Skip to content

added exactly_one_of validation #637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 29, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

* [#637](https://github.com/intridea/grape/pull/637): Added 'exactly_one_of' validation - [@Morred](https://github.com/Morred).
* [#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).
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,16 @@ 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.

Parameters can be defined as 'exactly_one_of', ensuring that exactly one parameter gets selected.

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

### Namespace Validation and Coercion

Namespaces allow parameter definitions and apply to every method within the namespace.
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 @@ -29,3 +29,4 @@ en:
unknown_options: 'unknown options: %{options}'
incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
mutual_exclusion: 'are mutually exclusive'
exactly_one: "- exactly one parameter must be provided"
4 changes: 4 additions & 0 deletions lib/grape/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ def mutually_exclusive(*attrs)
validates(attrs, mutual_exclusion: true)
end

def exactly_one_of(*attrs)
validates(attrs, exactly_one_of: true)
end

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

def validate!(params)
super
if none_of_restricted_params_is_present
raise Grape::Exceptions::Validation, param: "#{all_keys}", message_key: :exactly_one
end
params
end

private

def none_of_restricted_params_is_present
keys_in_common.length < 1
end

def all_keys
attrs.map(&:to_sym)
end
end
end
end
71 changes: 71 additions & 0 deletions spec/grape/validations/exactly_one_of_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require 'spec_helper'

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

context 'when all restricted 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 restricted 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 none of the restricted params is selected' do
let(:params) { { somethingelse: true } }

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

context 'when exactly one of the restricted params is selected' do
let(:params) { { beer: true, somethingelse: true } }

it 'params' do
expect(validator.validate!(params)).to eql params
end
end
end
end
36 changes: 36 additions & 0 deletions spec/grape/validations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -878,5 +878,41 @@ module SharedParams
end
end
end

context 'exactly one of' do
context 'params' do
it 'errors when two or more are present' do
subject.params do
optional :beer
optional :wine
optional :juice
exactly_one_of :beer, :wine, :juice
end
subject.get '/exactly_one_of' do
'exactly_one_of works!'
end

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

it 'errors when none is selected' do
subject.params do
optional :beer
optional :wine
optional :juice
exactly_one_of :beer, :wine, :juice
end
subject.get '/exactly_one_of' do
'exactly_one_of works!'
end

get '/exactly_one_of'
expect(last_response.status).to eq(400)
expect(last_response.body).to eq("[:beer, :wine, :juice] - exactly one parameter must be provided")
end
end
end
end
end