Skip to content

Commit 022cc38

Browse files
committed
Merge branch 'validations' of git://github.com/schmurfy/grape into pr
2 parents 470c865 + 908bc2d commit 022cc38

File tree

13 files changed

+484
-0
lines changed

13 files changed

+484
-0
lines changed

README.markdown

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,24 @@ post '/json_endpoint' do
192192
end
193193
```
194194

195+
## Validations
196+
197+
You can define validations and coercion option for your attributes:
198+
199+
```ruby
200+
params do
201+
required :id, type: Integer
202+
optional :name, type: String, regexp: /^[a-z]+$/
203+
end
204+
205+
get ':id' do
206+
# params[:id] is an Integer
207+
end
208+
```
209+
210+
When a type is specified an implicit validation is done after the coercion to ensure the output type is what you asked.
211+
212+
195213
## Headers
196214

197215
Headers are available through the `env` hash object.

grape.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
2020
s.add_runtime_dependency 'multi_json'
2121
s.add_runtime_dependency 'multi_xml'
2222
s.add_runtime_dependency 'hashie', '~> 1.2'
23+
s.add_runtime_dependency 'virtus'
2324

2425
s.add_development_dependency 'rake'
2526
s.add_development_dependency 'maruku'

lib/grape.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Grape
99
autoload :Route, 'grape/route'
1010
autoload :Entity, 'grape/entity'
1111
autoload :Cookies, 'grape/cookies'
12+
autoload :Validations, 'grape/validations'
1213

1314
module Middleware
1415
autoload :Base, 'grape/middleware/base'

lib/grape/api.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ module Grape
88
# creating Grape APIs.Users should subclass this
99
# class in order to build an API.
1010
class API
11+
extend Validations::ClassMethods
12+
1113
class << self
1214
attr_reader :route_set
1315
attr_reader :versions
@@ -32,6 +34,7 @@ def reset!
3234
@endpoints = []
3335
@mountings = []
3436
@routes = nil
37+
reset_validations!
3538
end
3639

3740
def compile
@@ -287,7 +290,9 @@ def route(methods, paths = ['/'], route_options = {}, &block)
287290
:route_options => (route_options || {}).merge(@last_description || {})
288291
}
289292
endpoints << Grape::Endpoint.new(settings.clone, endpoint_options, &block)
293+
290294
@last_description = nil
295+
reset_validations!
291296
end
292297

293298
def before(&block)

lib/grape/endpoint.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,11 @@ def run(env)
290290

291291
self.extend helpers
292292
cookies.read(@request)
293+
294+
Array(settings[:validations]).each do |validator|
295+
validator.validate!(params)
296+
end
297+
293298
run_filters befores
294299
response_text = instance_eval &self.block
295300
run_filters afters

lib/grape/validations.rb

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
require 'virtus'
2+
3+
module Grape
4+
5+
module Validations
6+
7+
##
8+
# All validators must inherit from this class.
9+
#
10+
class Validator
11+
def initialize(attrs, options)
12+
@attrs = Array(attrs)
13+
14+
if options.is_a?(Hash) && !options.empty?
15+
raise "unknown options: #{options.keys}"
16+
end
17+
end
18+
19+
def validate!(params)
20+
@attrs.each do |attr_name|
21+
validate_param!(attr_name, params)
22+
end
23+
end
24+
25+
private
26+
27+
def self.convert_to_short_name(klass)
28+
ret = klass.name.gsub(/::/, '/').
29+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
30+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
31+
tr("-", "_").
32+
downcase
33+
File.basename(ret, '_validator')
34+
end
35+
end
36+
37+
##
38+
# Base class for all validators taking only one param.
39+
class SingleOptionValidator < Validator
40+
def initialize(attrs, options)
41+
@option = options
42+
super
43+
end
44+
45+
end
46+
47+
# we define Validator::inherited here so SingleOptionValidator
48+
# will not be considered a validator.
49+
class Validator
50+
def self.inherited(klass)
51+
short_name = convert_to_short_name(klass)
52+
Validations::register_validator(short_name, klass)
53+
end
54+
end
55+
56+
57+
58+
class <<self
59+
attr_accessor :validators
60+
end
61+
62+
self.validators = {}
63+
64+
def self.register_validator(short_name, klass)
65+
validators[short_name] = klass
66+
end
67+
68+
69+
class ParamsScope
70+
def initialize(api, &block)
71+
@api = api
72+
instance_eval(&block)
73+
end
74+
75+
def requires(*attrs)
76+
validations = {:presence => true}
77+
if attrs.last.is_a?(Hash)
78+
validations.merge!(attrs.pop)
79+
end
80+
81+
validates(attrs, validations)
82+
end
83+
84+
def optional(*attrs)
85+
validations = {}
86+
if attrs.last.is_a?(Hash)
87+
validations.merge!(attrs.pop)
88+
end
89+
90+
validates(attrs, validations)
91+
end
92+
93+
private
94+
def validates(attrs, validations)
95+
doc_attrs = { :required => validations.keys.include?(:presence) }
96+
97+
# special case (type = coerce)
98+
if validations[:type]
99+
validations[:coerce] = validations.delete(:type)
100+
end
101+
102+
if coerce_type = validations[:coerce]
103+
doc_attrs[:type] = coerce_type.to_s
104+
end
105+
106+
if desc = validations.delete(:desc)
107+
doc_attrs[:desc] = desc
108+
end
109+
110+
@api.document_attribute(attrs, doc_attrs)
111+
112+
validations.each do |type, options|
113+
validator_class = Validations::validators[type.to_s]
114+
if validator_class
115+
@api.settings[:validations] << validator_class.new(attrs, options)
116+
else
117+
raise "unknown validator: #{type}"
118+
end
119+
end
120+
121+
end
122+
123+
end
124+
125+
# This module is mixed into the API Class.
126+
module ClassMethods
127+
def reset_validations!
128+
settings[:validations] = []
129+
end
130+
131+
def params(&block)
132+
ParamsScope.new(self, &block)
133+
end
134+
135+
def document_attribute(names, opts)
136+
if @last_description
137+
@last_description[:params] ||= {}
138+
139+
Array(names).each do |name|
140+
@last_description[:params][name.to_sym] ||= {}
141+
@last_description[:params][name.to_sym].merge!(opts)
142+
end
143+
end
144+
end
145+
146+
end
147+
148+
end
149+
end
150+
151+
# load all defined validations
152+
Dir[File.expand_path('../validations/*.rb', __FILE__)].each do |path|
153+
require(path)
154+
end

lib/grape/validations/coerce.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
module Grape
3+
class API
4+
Boolean = Virtus::Attribute::Boolean
5+
end
6+
7+
module Validations
8+
9+
class CoerceValidator < SingleOptionValidator
10+
def validate_param!(attr_name, params)
11+
new_value = coerce_value(@option, params[attr_name])
12+
if valid_type?(new_value)
13+
params[attr_name] = new_value
14+
else
15+
throw :error, :status => 400, :message => "invalid parameter: #{attr_name}"
16+
end
17+
end
18+
19+
private
20+
def _valid_array_type?(type, values)
21+
values.all? do |val|
22+
_valid_single_type?(type, val)
23+
end
24+
end
25+
26+
27+
def _valid_single_type?(klass, val)
28+
if klass == Virtus::Attribute::Boolean
29+
val.is_a?(TrueClass) || val.is_a?(FalseClass)
30+
else
31+
val.is_a?(klass)
32+
end
33+
end
34+
35+
def valid_type?(val)
36+
if @option.is_a?(Array)
37+
_valid_array_type?(@option[0], val)
38+
else
39+
_valid_single_type?(@option, val)
40+
end
41+
end
42+
43+
def coerce_value(type, val)
44+
converter = Virtus::Attribute.build(:a, type)
45+
converter.coerce(val)
46+
47+
# not the prettiest but some invalid coercion can currently trigger
48+
# errors in Virtus (see coerce_spec.rb)
49+
rescue => err
50+
nil
51+
end
52+
53+
end
54+
55+
end
56+
end

lib/grape/validations/presence.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Grape
2+
module Validations
3+
4+
class PresenceValidator < Validator
5+
def validate_param!(attr_name, params)
6+
unless params.has_key?(attr_name)
7+
throw :error, :status => 400, :message => "missing parameter: #{attr_name}"
8+
end
9+
end
10+
11+
end
12+
13+
end
14+
end

lib/grape/validations/regexp.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Grape
2+
module Validations
3+
4+
class RegexpValidator < SingleOptionValidator
5+
def validate_param!(attr_name, params)
6+
if params[attr_name] && !( params[attr_name].to_s =~ @option )
7+
throw :error, :status => 400, :message => "invalid parameter: #{attr_name}"
8+
end
9+
end
10+
end
11+
12+
end
13+
end

0 commit comments

Comments
 (0)