Skip to content
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

Optional groups #443

Merged
merged 10 commits into from
Aug 6, 2013
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Next Release
* [#442](https://github.com/intridea/grape/issues/442): Enable incrementally building on top of a previous API version - [@dblock](https://github.com/dblock).
* [#442](https://github.com/intridea/grape/issues/442): API `version` can now take an array of multiple versions - [@dblock](https://github.com/dblock).
* [#444](https://github.com/intridea/grape/issues/444): Added :en as fallback locale for I18n - [@aew](https://github.com/aew).

* [#443](https://github.com/intridea/grape/pull/443): Let `requires` and `optional` take blocks that initialize new scopes - [@asross](https://github.com/asross).
* Your contribution here.

#### Fixes
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ params do
group :media do
requires :url
end
optional :audio do
requires :mp3
end
end
put ':id' do
# params[:id] is an Integer
Expand All @@ -351,8 +354,9 @@ params do
end
```

Parameters can be nested using `group`. In the above example, this means
`params[:media][:url]` is required along with `params[:id]`.
Parameters can be nested using `group` or by calling `requires` or `optional` with a block.
In the above example, this means `params[:media][:url]` is required along with `params[:id]`,
and `params[:audio][:mp3]` is required only if `params[:audio]` is present.

### Namespace Validation and Coercion

Expand Down
32 changes: 24 additions & 8 deletions lib/grape/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,27 @@ def self.register_validator(short_name, klass)
class ParamsScope
attr_accessor :element, :parent

def initialize(api, element, parent, &block)
@element = element
@parent = parent
@api = api
def initialize(opts, &block)
@element = opts[:element]
@parent = opts[:parent]
@api = opts[:api]
@optional = opts[:optional] || false
@declared_params = []

instance_eval(&block)

configure_declared_params
end

def requires(*attrs)
def should_validate?(parameters)
return false if @optional && params(parameters).blank?
return true if parent.nil?
parent.should_validate?(parameters)
end

def requires(*attrs, &block)
return new_scope(attrs, &block) if block_given?

validations = {:presence => true}
if attrs.last.is_a?(Hash)
validations.merge!(attrs.pop)
Expand All @@ -110,7 +119,9 @@ def requires(*attrs)
validates(attrs, validations)
end

def optional(*attrs)
def optional(*attrs, &block)
return new_scope(attrs, true, &block) if block_given?

validations = {}
if attrs.last.is_a?(Hash)
validations.merge!(attrs.pop)
Expand All @@ -121,7 +132,7 @@ def optional(*attrs)
end

def group(element, &block)
ParamsScope.new(@api, element, self, &block)
requires(element, &block)
end

def params(params)
Expand All @@ -143,6 +154,11 @@ def push_declared_params(attrs)

private

def new_scope(attrs, optional=false, &block)
raise ArgumentError unless attrs.size == 1
ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, &block)
end

# Pushes declared params to parent or settings
def configure_declared_params
if @parent
Expand Down Expand Up @@ -214,7 +230,7 @@ def reset_validations!
end

def params(&block)
ParamsScope.new(self, nil, nil, &block)
ParamsScope.new(api: self, &block)
end

def document_attribute(names, opts)
Expand Down
5 changes: 5 additions & 0 deletions lib/grape/validations/presence.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
module Grape
module Validations
class PresenceValidator < Validator
def validate!(params)
return unless @scope.should_validate?(params)
super
end

def validate_param!(attr_name, params)
unless params.has_key?(attr_name)
raise Grape::Exceptions::Validation, :status => 400,
Expand Down
130 changes: 130 additions & 0 deletions spec/grape/validations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,48 @@ def app; subject end
end
end

context 'required with a block' do
before do
subject.params {
requires :items do
requires :key
end
}
subject.get '/required' do 'required works'; end
end

it 'errors when param not present' do
get '/required'
last_response.status.should == 400
last_response.body.should == 'missing parameter: items[key]'
end

it "doesn't throw a missing param when param is present" do
get '/required', { :items => [:key => 'hello', :key => 'world'] }
last_response.status.should == 200
last_response.body.should == 'required works'
end

it "doesn't allow more than one parameter" do
expect {
subject.params {
requires(:items, desc: 'Foo') do
requires :key
end
}
}.to raise_error ArgumentError
end

it 'adds to declared parameters' do
subject.params {
requires :items do
requires :key
end
}
subject.settings[:declared_params].should == [:items => [:key]]
end
end

context 'group' do
before do
subject.params {
Expand Down Expand Up @@ -90,6 +132,94 @@ def app; subject end
end
end

context 'optional with a block' do
before do
subject.params {
optional :items do
requires :key
end
}
subject.get '/optional_group' do 'optional group works'; end
end

it "doesn't throw a missing param when the group isn't present" do
get '/optional_group'
last_response.status.should == 200
last_response.body.should == 'optional group works'
end

it "doesn't throw a missing param when both group and param are given" do
get '/optional_group', { :items => {:key => 'foo'} }
last_response.status.should == 200
last_response.body.should == 'optional group works'
end

it "errors when group is present, but required param is not" do
get '/optional_group', { :items => {:NOT_key => 'foo'} }
last_response.status.should == 400
last_response.body.should == 'missing parameter: items[key]'
end

it 'adds to declared parameters' do
subject.params {
optional :items do
requires :key
end
}
subject.settings[:declared_params].should == [:items => [:key]]
end
end

context 'nested optional blocks' do
before do
subject.params {
optional :items do
requires :key
optional(:optional_subitems) { requires :value }
requires(:required_subitems) { requires :value }
end
}
subject.get('/nested_optional_group') { 'nested optional group works' }
end

it 'does no internal validations if the outer group is blank' do
get '/nested_optional_group'
last_response.status.should == 200
last_response.body.should == 'nested optional group works'
end

it 'does internal validations if the outer group is present' do
get '/nested_optional_group', { :items => {:key => 'foo' }}
last_response.status.should == 400
last_response.body.should == 'missing parameter: items[required_subitems][value]'

get '/nested_optional_group', { :items => { :key => 'foo', :required_subitems => {:value => 'bar'}}}
last_response.status.should == 200
last_response.body.should == 'nested optional group works'
end

it 'handles deep nesting' do
get '/nested_optional_group', { :items => { :key => 'foo', :required_subitems => {:value => 'bar'}, :optional_subitems => {:NOT_value => 'baz'}}}
last_response.status.should == 400
last_response.body.should == 'missing parameter: items[optional_subitems][value]'

get '/nested_optional_group', { :items => { :key => 'foo', :required_subitems => {:value => 'bar'}, :optional_subitems => {:value => 'baz'}}}
last_response.status.should == 200
last_response.body.should == 'nested optional group works'
end

it 'adds to declared parameters' do
subject.params {
optional :items do
requires :key
optional(:optional_subitems) { requires :value }
requires(:required_subitems) { requires :value }
end
}
subject.settings[:declared_params].should == [:items => [:key, {:optional_subitems => [:value]}, {:required_subitems => [:value]}]]
end
end

context 'custom validation' do
module CustomValidations
class Customvalidator < Grape::Validations::Validator
Expand Down