Skip to content

Commit 7e375f0

Browse files
committed
Merge pull request ruby-grape#236 from tim-vandecasteele/validation_nested_parameters
Allow validation of nested parameters.
2 parents 9bd4f23 + 3715500 commit 7e375f0

File tree

6 files changed

+142
-15
lines changed

6 files changed

+142
-15
lines changed

CHANGELOG.markdown

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Next Release
22
============
33

4+
* [#236](https://github.com/intridea/grape/pull/236): Allow validation of nested parameters. - [@tim-vandecasteele](https://github.com/tim-vandecasteele).
45
* [#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).
56
* [#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).
67
* [#210](https://github.com/intridea/grape/pull/210): Fix: `Endpoint#body_params` causing undefined method 'size' - [@adamgotterer](https://github.com/adamgotterer).

README.markdown

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ You can define validations and coercion options for your parameters using `param
220220
params do
221221
requires :id, type: Integer
222222
optional :name, type: String, regexp: /^[a-z]+$/
223+
224+
group :user do
225+
requires :first_name
226+
requires :last_name
227+
end
223228
end
224229
get ':id' do
225230
# params[:id] is an Integer
@@ -229,6 +234,9 @@ end
229234
When a type is specified an implicit validation is done after the coercion to ensure
230235
the output type is the one declared.
231236

237+
Parameters can be nested using `group`. In the above example, this means both
238+
`params[:user][:first_name]` and `params[:user][:last_name]` are required next to `params[:id]`.
239+
232240
### Namespace Validation and Coercion
233241
Namespaces allow parameter definitions and apply to every method within the namespace.
234242

lib/grape/validations.rb

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@ module Validations
88
# All validators must inherit from this class.
99
#
1010
class Validator
11-
def initialize(attrs, options, required)
11+
def initialize(attrs, options, required, scope)
1212
@attrs = Array(attrs)
1313
@required = required
14+
@scope = scope
1415

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

2021
def validate!(params)
22+
params = @scope.params(params)
23+
2124
@attrs.each do |attr_name|
2225
if @required || params.has_key?(attr_name)
2326
validate_param!(attr_name, params)
@@ -40,7 +43,7 @@ def self.convert_to_short_name(klass)
4043
##
4144
# Base class for all validators taking only one param.
4245
class SingleOptionValidator < Validator
43-
def initialize(attrs, options, required)
46+
def initialize(attrs, options, required, scope)
4447
@option = options
4548
super
4649
end
@@ -67,7 +70,11 @@ def self.register_validator(short_name, klass)
6770
end
6871

6972
class ParamsScope
70-
def initialize(api, &block)
73+
attr_accessor :element, :parent
74+
75+
def initialize(api, element, parent, &block)
76+
@element = element
77+
@parent = parent
7178
@api = api
7279
instance_eval(&block)
7380
end
@@ -89,7 +96,22 @@ def optional(*attrs)
8996

9097
validates(attrs, validations)
9198
end
92-
99+
100+
def group(element, &block)
101+
scope = ParamsScope.new(@api, element, self, &block)
102+
end
103+
104+
def params(params)
105+
params = @parent.params(params) if @parent
106+
params = params[@element] || {} if @element
107+
params
108+
end
109+
110+
def full_name(name)
111+
return "#{@parent.full_name(@element)}[#{name}]" if @parent
112+
name.to_s
113+
end
114+
93115
private
94116
def validates(attrs, validations)
95117
doc_attrs = { :required => validations.keys.include?(:presence) }
@@ -106,9 +128,10 @@ def validates(attrs, validations)
106128
if desc = validations.delete(:desc)
107129
doc_attrs[:desc] = desc
108130
end
109-
110-
@api.document_attribute(attrs, doc_attrs)
111-
131+
132+
full_attrs = attrs.collect{ |name| { :name => name, :full_name => full_name(name)} }
133+
@api.document_attribute(full_attrs, doc_attrs)
134+
112135
# Validate for presence before any other validators
113136
if validations.has_key?(:presence) && validations[:presence]
114137
validate('presence', validations[:presence], attrs, doc_attrs)
@@ -130,7 +153,7 @@ def validates(attrs, validations)
130153
def validate(type, options, attrs, doc_attrs)
131154
validator_class = Validations::validators[type.to_s]
132155
if validator_class
133-
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required])
156+
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required], self)
134157
else
135158
raise "unknown validator: #{type}"
136159
end
@@ -145,16 +168,16 @@ def reset_validations!
145168
end
146169

147170
def params(&block)
148-
ParamsScope.new(self, &block)
171+
ParamsScope.new(self, nil, nil, &block)
149172
end
150173

151174
def document_attribute(names, opts)
152175
if @last_description
153176
@last_description[:params] ||= {}
154-
177+
155178
Array(names).each do |name|
156-
@last_description[:params][name.to_s] ||= {}
157-
@last_description[:params][name.to_s].merge!(opts)
179+
@last_description[:params][name[:name].to_s] ||= {}
180+
@last_description[:params][name[:name].to_s].merge!(opts).merge!({:full_name => name[:full_name]})
158181
end
159182
end
160183
end

spec/grape/api_spec.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,7 @@ class CommunicationError < RuntimeError; end
10291029
subject.routes.map { |route|
10301030
{ :description => route.route_description, :params => route.route_params }
10311031
}.should eq [
1032-
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "namespace parameter" }, "method_param" => { :required => false, :desc => "method parameter" } } }
1032+
{ :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" } } }
10331033
]
10341034
end
10351035
it "should merge the parameters of nested namespaces" do
@@ -1055,7 +1055,22 @@ class CommunicationError < RuntimeError; end
10551055
subject.routes.map { |route|
10561056
{ :description => route.route_description, :params => route.route_params }
10571057
}.should eq [
1058-
{ :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" } } }
1058+
{ :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" } } }
1059+
]
1060+
end
1061+
it "should provide a full_name for parameters in nested groups" do
1062+
subject.desc "nesting"
1063+
subject.params do
1064+
requires :root_param, :desc => "root param"
1065+
group :nested do
1066+
requires :nested_param, :desc => "nested param"
1067+
end
1068+
end
1069+
subject.get "method" do ; end
1070+
subject.routes.map { |route|
1071+
{ :description => route.route_description, :params => route.route_params }
1072+
}.should eq [
1073+
{ :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]" } } }
10591074
]
10601075
end
10611076
it "should not symbolize params" do

spec/grape/validations/coerce_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ class User
111111
last_response.status.should == 201
112112
last_response.body.should == File.basename(__FILE__).to_s
113113
end
114+
115+
it 'Nests integers' do
116+
subject.params do
117+
group :integers do
118+
requires :int, :coerce => Integer
119+
end
120+
end
121+
subject.get '/int' do params[:integers][:int].class; end
122+
123+
get '/int', { :integers => { :int => "45" } }
124+
last_response.status.should == 200
125+
last_response.body.should == 'Fixnum'
126+
end
114127
end
115128
end
116129
end

spec/grape/validations/presence_spec.rb

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ class API < Grape::API
2626
get do
2727
"Hello"
2828
end
29+
30+
params do
31+
group :user do
32+
requires :first_name, :last_name
33+
end
34+
end
35+
get '/nested' do
36+
"Nested"
37+
end
38+
39+
params do
40+
group :admin do
41+
requires :admin_name
42+
group :super do
43+
group :user do
44+
requires :first_name, :last_name
45+
end
46+
end
47+
end
48+
end
49+
get '/nested_triple' do
50+
"Nested triple"
51+
end
2952
end
3053
end
3154
end
@@ -67,5 +90,49 @@ def app
6790
last_response.status.should == 200
6891
last_response.body.should == "Hello"
6992
end
70-
93+
94+
it 'validates nested parameters' do
95+
get('/nested')
96+
last_response.status.should == 400
97+
last_response.body.should == "missing parameter: first_name"
98+
99+
get('/nested', :user => {:first_name => "Billy"})
100+
last_response.status.should == 400
101+
last_response.body.should == "missing parameter: last_name"
102+
103+
get('/nested', :user => {:first_name => "Billy", :last_name => "Bob"})
104+
last_response.status.should == 200
105+
last_response.body.should == "Nested"
106+
end
107+
108+
it 'validates triple nested parameters' do
109+
get('/nested_triple')
110+
last_response.status.should == 400
111+
last_response.body.should == "missing parameter: admin_name"
112+
113+
get('/nested_triple', :user => {:first_name => "Billy"})
114+
last_response.status.should == 400
115+
last_response.body.should == "missing parameter: admin_name"
116+
117+
get('/nested_triple', :admin => {:super => {:first_name => "Billy"}})
118+
last_response.status.should == 400
119+
last_response.body.should == "missing parameter: admin_name"
120+
121+
get('/nested_triple', :super => {:user => {:first_name => "Billy", :last_name => "Bob"}})
122+
last_response.status.should == 400
123+
last_response.body.should == "missing parameter: admin_name"
124+
125+
get('/nested_triple', :admin => {:super => {:user => {:first_name => "Billy"}}})
126+
last_response.status.should == 400
127+
last_response.body.should == "missing parameter: admin_name"
128+
129+
get('/nested_triple', :admin => { :admin_name => 'admin', :super => {:user => {:first_name => "Billy"}}})
130+
last_response.status.should == 400
131+
last_response.body.should == "missing parameter: last_name"
132+
133+
get('/nested_triple', :admin => { :admin_name => 'admin', :super => {:user => {:first_name => "Billy", :last_name => "Bob"}}})
134+
last_response.status.should == 200
135+
last_response.body.should == "Nested triple"
136+
end
137+
71138
end

0 commit comments

Comments
 (0)