Skip to content

Commit 4800c93

Browse files
committed
added coercion+validation support
1 parent 575e6fc commit 4800c93

File tree

6 files changed

+218
-0
lines changed

6 files changed

+218
-0
lines changed

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+
include Validations
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
@@ -285,6 +285,11 @@ def run(env)
285285

286286
self.extend helpers
287287
cookies.read(@request)
288+
289+
Array(settings[:validations]).each do |validator|
290+
validator.validate!(params)
291+
end
292+
288293
run_filters befores
289294
response_text = instance_eval &self.block
290295
run_filters afters

lib/grape/validations.rb

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
require 'virtus'
2+
Boolean = Virtus::Attribute::Boolean
3+
module Grape
4+
5+
class Validator
6+
def initialize(attrs, options)
7+
@attrs = Array(attrs)
8+
9+
if options.is_a?(Hash) && !options.empty?
10+
raise "unknown options: #{options.keys}"
11+
end
12+
end
13+
14+
def validate!(params)
15+
@attrs.each do |attr_name|
16+
validate_param!(attr_name, params)
17+
end
18+
end
19+
end
20+
21+
22+
class SingleOptionValidator < Validator
23+
def initialize(attrs, options)
24+
@option = options
25+
super
26+
end
27+
28+
end
29+
30+
31+
class PresenceValidator < Validator
32+
def validate_param!(attr_name, params)
33+
unless params.has_key?(attr_name)
34+
throw :error, :status => 400, :message => "missing parameter: #{attr_name}"
35+
end
36+
end
37+
38+
end
39+
40+
class CoerceValidator < SingleOptionValidator
41+
def validate_param!(attr_name, params)
42+
params[attr_name] = coerce_value(@option, params[attr_name])
43+
end
44+
45+
private
46+
def coerce_value(type, val)
47+
converter = Virtus::Attribute.build(:a, type)
48+
converter.coerce(val)
49+
end
50+
end
51+
52+
class RegExpValidator < SingleOptionValidator
53+
def validate_param!(attr_name, params)
54+
if params[attr_name] && !( params[attr_name].to_s =~ @option )
55+
throw :error, :status => 400, :message => "invalid parameter: #{attr_name}"
56+
end
57+
end
58+
end
59+
60+
61+
62+
module Validations
63+
64+
class <<self
65+
attr_accessor :validators
66+
end
67+
68+
self.validators = {}
69+
self.validators[:presence] = PresenceValidator
70+
self.validators[:regexp] = RegExpValidator
71+
self.validators[:coerce] = CoerceValidator
72+
73+
def self.included(klass)
74+
klass.instance_eval do
75+
extend ClassMethods
76+
end
77+
end
78+
79+
module ClassMethods
80+
def reset_validations!
81+
settings[:validations] = []
82+
end
83+
84+
def requires(*attrs)
85+
validations = {:presence => true}
86+
if attrs.last.is_a?(Hash)
87+
validations.merge!(attrs.pop)
88+
end
89+
90+
validates(attrs, validations)
91+
end
92+
93+
def optional(*attrs)
94+
validations = {}
95+
if attrs.last.is_a?(Hash)
96+
validations.merge!(attrs.pop)
97+
end
98+
99+
validates(attrs, validations)
100+
end
101+
102+
def validates(attrs, validations)
103+
validations.each do |type, options|
104+
validator_class = Validations::validators[type]
105+
if validator_class
106+
settings[:validations] << validator_class.new(attrs, options)
107+
else
108+
raise "unknown validator: #{type}"
109+
end
110+
end
111+
112+
end
113+
114+
115+
end
116+
117+
end
118+
end

spec/grape/validations_spec.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
require 'spec_helper'
2+
3+
describe Grape::Validations do
4+
def app; @app; end
5+
6+
before do
7+
@app = Class.new(Grape::API) do
8+
default_format :json
9+
10+
requires :id, :regexp => /^[0-9]+$/
11+
post do
12+
{:ret => params[:id]}
13+
end
14+
15+
requires :name, :company
16+
optional :a_number, :regexp => /^[0-9]+$/
17+
get do
18+
"Hello"
19+
end
20+
21+
requires :int, :coerce => Integer
22+
optional :arr, :coerce => Array[Integer]
23+
optional :bool, :coerce => Array[Boolean]
24+
get '/coerce' do
25+
{
26+
:int => params[:int].class,
27+
:arr => params[:arr] ? params[:arr][0].class : nil,
28+
:bool => params[:bool] ? (params[:bool][0] == true) && (params[:bool][1] == false) : nil
29+
}
30+
end
31+
32+
end
33+
34+
end
35+
36+
it 'validates id' do
37+
post('/')
38+
last_response.status.should == 400
39+
last_response.body.should == "missing parameter: id"
40+
41+
post('/', {}, 'rack.input' => StringIO.new('{"id" : "a56b"}'))
42+
last_response.body.should == 'invalid parameter: id'
43+
last_response.status.should == 400
44+
45+
post('/', {}, 'rack.input' => StringIO.new('{"id" : 56}'))
46+
last_response.body.should == '{"ret":56}'
47+
last_response.status.should == 201
48+
end
49+
50+
it 'validates name, company' do
51+
get('/')
52+
last_response.status.should == 400
53+
last_response.body.should == "missing parameter: name"
54+
55+
get('/', :name => "Bob")
56+
last_response.status.should == 400
57+
last_response.body.should == "missing parameter: company"
58+
59+
get('/', :name => "Bob", :company => "TestCorp")
60+
last_response.status.should == 200
61+
last_response.body.should == "Hello"
62+
end
63+
64+
it 'validates optional parameter if present' do
65+
get('/', :name => "Bob", :company => "TestCorp", :a_number => "string")
66+
last_response.status.should == 400
67+
last_response.body.should == "invalid parameter: a_number"
68+
69+
get('/', :name => "Bob", :company => "TestCorp", :a_number => 45)
70+
last_response.status.should == 200
71+
last_response.body.should == "Hello"
72+
end
73+
74+
it 'should coerce inputs' do
75+
get('/coerce', :int => "43")
76+
last_response.status.should == 200
77+
ret = MultiJson.load(last_response.body)
78+
ret["int"].should == "Fixnum"
79+
80+
get('/coerce', :int => "40", :arr => ["1","20","3"], :bool => [1, 0])
81+
last_response.status.should == 200
82+
ret = MultiJson.load(last_response.body)
83+
ret["int"].should == "Fixnum"
84+
ret["arr"].should == "Fixnum"
85+
ret["bool"].should == true
86+
end
87+
88+
end

0 commit comments

Comments
 (0)