Permalink
Browse files

Merge pull request #236 from tim-vandecasteele/validation_nested_para…

…meters

Allow validation of nested parameters.
  • Loading branch information...
dblock committed Sep 5, 2012
2 parents 9bd4f23 + 3715500 commit 7e375f02145d41ca3490e621ce21b435aecaee29
View
@@ -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).
View
@@ -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
@@ -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.
View
@@ -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)
@@ -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
@@ -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
@@ -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) }
@@ -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)
@@ -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
@@ -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
View
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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

0 comments on commit 7e375f0

Please sign in to comment.