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

Allow validation of nested parameters. #236

Merged
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.markdown
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Next Release
============

* [#236](https://github.com/intridea/grape/pull/236): Allow validation of nested parameters. - [@tim-vandecasteele](https://github.com/tim-vandecasteele).
* [#201](https://github.com/intridea/grape/pull/201): Added custom exceptions to Grape. Updated validations to use ValidationError that can be rescued. - [@adamgotterer](https://github.com/adamgotterer).
* [#211](https://github.com/intridea/grape/pull/211): Updates to validation and coercion: Fix #211 and force order of operations for presence and coercion - [@adamgotterer](https://github.com/adamgotterer).
* [#210](https://github.com/intridea/grape/pull/210): Fix: `Endpoint#body_params` causing undefined method 'size' - [@adamgotterer](https://github.com/adamgotterer).
Expand Down
8 changes: 8 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ You can define validations and coercion options for your parameters using `param
params do
requires :id, type: Integer
optional :name, type: String, regexp: /^[a-z]+$/

group :user do
requires :first_name
requires :last_name
end
end
get ':id' do
# params[:id] is an Integer
Expand All @@ -229,6 +234,9 @@ end
When a type is specified an implicit validation is done after the coercion to ensure
the output type is the one declared.

Parameters can be nested using `group`. In the above example, this means both
`params[:user][:first_name]` and `params[:user][:last_name]` are required next to `params[:id]`.

### Namespace Validation and Coercion
Namespaces allow parameter definitions and apply to every method within the namespace.

Expand Down
47 changes: 35 additions & 12 deletions lib/grape/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ module Validations
# All validators must inherit from this class.
#
class Validator
def initialize(attrs, options, required)
def initialize(attrs, options, required, scope)
@attrs = Array(attrs)
@required = required
@scope = scope

if options.is_a?(Hash) && !options.empty?
raise "unknown options: #{options.keys}"
end
end

def validate!(params)
params = @scope.params(params)

@attrs.each do |attr_name|
if @required || params.has_key?(attr_name)
validate_param!(attr_name, params)
Expand All @@ -40,7 +43,7 @@ def self.convert_to_short_name(klass)
##
# Base class for all validators taking only one param.
class SingleOptionValidator < Validator
def initialize(attrs, options, required)
def initialize(attrs, options, required, scope)
@option = options
super
end
Expand All @@ -67,7 +70,11 @@ def self.register_validator(short_name, klass)
end

class ParamsScope
def initialize(api, &block)
attr_accessor :element, :parent

def initialize(api, element, parent, &block)
@element = element
@parent = parent
@api = api
instance_eval(&block)
end
Expand All @@ -89,7 +96,22 @@ def optional(*attrs)

validates(attrs, validations)
end


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

def params(params)
params = @parent.params(params) if @parent
params = params[@element] || {} if @element
params
end

def full_name(name)
return "#{@parent.full_name(@element)}[#{name}]" if @parent
name.to_s
end

private
def validates(attrs, validations)
doc_attrs = { :required => validations.keys.include?(:presence) }
Expand All @@ -106,9 +128,10 @@ def validates(attrs, validations)
if desc = validations.delete(:desc)
doc_attrs[:desc] = desc
end

@api.document_attribute(attrs, doc_attrs)


full_attrs = attrs.collect{ |name| { :name => name, :full_name => full_name(name)} }
@api.document_attribute(full_attrs, doc_attrs)

# Validate for presence before any other validators
if validations.has_key?(:presence) && validations[:presence]
validate('presence', validations[:presence], attrs, doc_attrs)
Expand All @@ -130,7 +153,7 @@ def validates(attrs, validations)
def validate(type, options, attrs, doc_attrs)
validator_class = Validations::validators[type.to_s]
if validator_class
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required])
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required], self)
else
raise "unknown validator: #{type}"
end
Expand All @@ -145,16 +168,16 @@ def reset_validations!
end

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

def document_attribute(names, opts)
if @last_description
@last_description[:params] ||= {}

Array(names).each do |name|
@last_description[:params][name.to_s] ||= {}
@last_description[:params][name.to_s].merge!(opts)
@last_description[:params][name[:name].to_s] ||= {}
@last_description[:params][name[:name].to_s].merge!(opts).merge!({:full_name => name[:full_name]})
end
end
end
Expand Down
19 changes: 17 additions & 2 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ class CommunicationError < RuntimeError; end
subject.routes.map { |route|
{ :description => route.route_description, :params => route.route_params }
}.should eq [
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "namespace parameter" }, "method_param" => { :required => false, :desc => "method parameter" } } }
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "namespace parameter", :full_name=>"ns_param" }, "method_param" => { :required => false, :desc => "method parameter", :full_name=>"method_param" } } }
]
end
it "should merge the parameters of nested namespaces" do
Expand All @@ -1055,7 +1055,22 @@ class CommunicationError < RuntimeError; end
subject.routes.map { |route|
{ :description => route.route_description, :params => route.route_params }
}.should eq [
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "ns param 2" }, "ns1_param" => { :required => true, :desc => "ns1 param" }, "ns2_param" => { :required => true, :desc => "ns2 param" }, "method_param" => { :required => false, :desc => "method param" } } }
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "ns param 2", :full_name=>"ns_param" }, "ns1_param" => { :required => true, :desc => "ns1 param", :full_name=>"ns1_param" }, "ns2_param" => { :required => true, :desc => "ns2 param", :full_name=>"ns2_param" }, "method_param" => { :required => false, :desc => "method param", :full_name=>"method_param" } } }
]
end
it "should provide a full_name for parameters in nested groups" do
subject.desc "nesting"
subject.params do
requires :root_param, :desc => "root param"
group :nested do
requires :nested_param, :desc => "nested param"
end
end
subject.get "method" do ; end
subject.routes.map { |route|
{ :description => route.route_description, :params => route.route_params }
}.should eq [
{ :description => "nesting", :params => { "root_param" => { :required => true, :desc => "root param", :full_name=>"root_param" }, "nested_param" => { :required => true, :desc => "nested param", :full_name=>"nested[nested_param]" } } }
]
end
it "should not symbolize params" do
Expand Down
13 changes: 13 additions & 0 deletions spec/grape/validations/coerce_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ class User
last_response.status.should == 201
last_response.body.should == File.basename(__FILE__).to_s
end

it 'Nests integers' do
subject.params do
group :integers do
requires :int, :coerce => Integer
end
end
subject.get '/int' do params[:integers][:int].class; end

get '/int', { :integers => { :int => "45" } }
last_response.status.should == 200
last_response.body.should == 'Fixnum'
end
end
end
end
69 changes: 68 additions & 1 deletion spec/grape/validations/presence_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ class API < Grape::API
get do
"Hello"
end

params do
group :user do
requires :first_name, :last_name
end
end
get '/nested' do
"Nested"
end

params do
group :admin do
requires :admin_name
group :super do
group :user do
requires :first_name, :last_name
end
end
end
end
get '/nested_triple' do
"Nested triple"
end
end
end
end
Expand Down Expand Up @@ -67,5 +90,49 @@ def app
last_response.status.should == 200
last_response.body.should == "Hello"
end


it 'validates nested parameters' do
get('/nested')
last_response.status.should == 400
last_response.body.should == "missing parameter: first_name"

get('/nested', :user => {:first_name => "Billy"})
last_response.status.should == 400
last_response.body.should == "missing parameter: last_name"

get('/nested', :user => {:first_name => "Billy", :last_name => "Bob"})
last_response.status.should == 200
last_response.body.should == "Nested"
end

it 'validates triple nested parameters' do
get('/nested_triple')
last_response.status.should == 400
last_response.body.should == "missing parameter: admin_name"

get('/nested_triple', :user => {:first_name => "Billy"})
last_response.status.should == 400
last_response.body.should == "missing parameter: admin_name"

get('/nested_triple', :admin => {:super => {:first_name => "Billy"}})
last_response.status.should == 400
last_response.body.should == "missing parameter: admin_name"

get('/nested_triple', :super => {:user => {:first_name => "Billy", :last_name => "Bob"}})
last_response.status.should == 400
last_response.body.should == "missing parameter: admin_name"

get('/nested_triple', :admin => {:super => {:user => {:first_name => "Billy"}}})
last_response.status.should == 400
last_response.body.should == "missing parameter: admin_name"

get('/nested_triple', :admin => { :admin_name => 'admin', :super => {:user => {:first_name => "Billy"}}})
last_response.status.should == 400
last_response.body.should == "missing parameter: last_name"

get('/nested_triple', :admin => { :admin_name => 'admin', :super => {:user => {:first_name => "Billy", :last_name => "Bob"}}})
last_response.status.should == 200
last_response.body.should == "Nested triple"
end

end