Skip to content

Commit

Permalink
added coercion+validation support
Browse files Browse the repository at this point in the history
  • Loading branch information
schmurfy committed Jul 7, 2012
1 parent 575e6fc commit 4800c93
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 0 deletions.
1 change: 1 addition & 0 deletions grape.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'multi_json'
s.add_runtime_dependency 'multi_xml'
s.add_runtime_dependency 'hashie', '~> 1.2'
s.add_runtime_dependency 'virtus'

s.add_development_dependency 'rake'
s.add_development_dependency 'maruku'
Expand Down
1 change: 1 addition & 0 deletions lib/grape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module Grape
autoload :Route, 'grape/route'
autoload :Entity, 'grape/entity'
autoload :Cookies, 'grape/cookies'
autoload :Validations, 'grape/validations'

module Middleware
autoload :Base, 'grape/middleware/base'
Expand Down
5 changes: 5 additions & 0 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module Grape
# creating Grape APIs.Users should subclass this
# class in order to build an API.
class API
include Validations

class << self
attr_reader :route_set
attr_reader :versions
Expand All @@ -32,6 +34,7 @@ def reset!
@endpoints = []
@mountings = []
@routes = nil
reset_validations!
end

def compile
Expand Down Expand Up @@ -287,7 +290,9 @@ def route(methods, paths = ['/'], route_options = {}, &block)
:route_options => (route_options || {}).merge(@last_description || {})
}
endpoints << Grape::Endpoint.new(settings.clone, endpoint_options, &block)

@last_description = nil
reset_validations!
end

def before(&block)
Expand Down
5 changes: 5 additions & 0 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ def run(env)

self.extend helpers
cookies.read(@request)

Array(settings[:validations]).each do |validator|
validator.validate!(params)
end

run_filters befores
response_text = instance_eval &self.block
run_filters afters
Expand Down
118 changes: 118 additions & 0 deletions lib/grape/validations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
require 'virtus'
Boolean = Virtus::Attribute::Boolean
module Grape

class Validator
def initialize(attrs, options)
@attrs = Array(attrs)

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

def validate!(params)
@attrs.each do |attr_name|
validate_param!(attr_name, params)
end
end
end


class SingleOptionValidator < Validator
def initialize(attrs, options)
@option = options
super
end

end


class PresenceValidator < Validator
def validate_param!(attr_name, params)
unless params.has_key?(attr_name)
throw :error, :status => 400, :message => "missing parameter: #{attr_name}"
end
end

end

class CoerceValidator < SingleOptionValidator
def validate_param!(attr_name, params)
params[attr_name] = coerce_value(@option, params[attr_name])
end

private
def coerce_value(type, val)
converter = Virtus::Attribute.build(:a, type)
converter.coerce(val)
end
end

class RegExpValidator < SingleOptionValidator
def validate_param!(attr_name, params)
if params[attr_name] && !( params[attr_name].to_s =~ @option )
throw :error, :status => 400, :message => "invalid parameter: #{attr_name}"
end
end
end



module Validations

class <<self
attr_accessor :validators
end

self.validators = {}
self.validators[:presence] = PresenceValidator
self.validators[:regexp] = RegExpValidator
self.validators[:coerce] = CoerceValidator

def self.included(klass)
klass.instance_eval do
extend ClassMethods
end
end

module ClassMethods
def reset_validations!
settings[:validations] = []
end

def requires(*attrs)
validations = {:presence => true}
if attrs.last.is_a?(Hash)
validations.merge!(attrs.pop)
end

validates(attrs, validations)
end

def optional(*attrs)
validations = {}
if attrs.last.is_a?(Hash)
validations.merge!(attrs.pop)
end

validates(attrs, validations)
end

def validates(attrs, validations)
validations.each do |type, options|
validator_class = Validations::validators[type]
if validator_class
settings[:validations] << validator_class.new(attrs, options)
else
raise "unknown validator: #{type}"
end
end

end


end

end
end
88 changes: 88 additions & 0 deletions spec/grape/validations_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require 'spec_helper'

describe Grape::Validations do
def app; @app; end

before do
@app = Class.new(Grape::API) do
default_format :json

requires :id, :regexp => /^[0-9]+$/
post do
{:ret => params[:id]}
end

requires :name, :company
optional :a_number, :regexp => /^[0-9]+$/
get do
"Hello"
end

requires :int, :coerce => Integer
optional :arr, :coerce => Array[Integer]
optional :bool, :coerce => Array[Boolean]
get '/coerce' do
{
:int => params[:int].class,
:arr => params[:arr] ? params[:arr][0].class : nil,
:bool => params[:bool] ? (params[:bool][0] == true) && (params[:bool][1] == false) : nil
}
end

end

end

it 'validates id' do
post('/')
last_response.status.should == 400
last_response.body.should == "missing parameter: id"

post('/', {}, 'rack.input' => StringIO.new('{"id" : "a56b"}'))
last_response.body.should == 'invalid parameter: id'
last_response.status.should == 400

post('/', {}, 'rack.input' => StringIO.new('{"id" : 56}'))
last_response.body.should == '{"ret":56}'
last_response.status.should == 201
end

it 'validates name, company' do
get('/')
last_response.status.should == 400
last_response.body.should == "missing parameter: name"

get('/', :name => "Bob")
last_response.status.should == 400
last_response.body.should == "missing parameter: company"

get('/', :name => "Bob", :company => "TestCorp")
last_response.status.should == 200
last_response.body.should == "Hello"
end

it 'validates optional parameter if present' do
get('/', :name => "Bob", :company => "TestCorp", :a_number => "string")
last_response.status.should == 400
last_response.body.should == "invalid parameter: a_number"

get('/', :name => "Bob", :company => "TestCorp", :a_number => 45)
last_response.status.should == 200
last_response.body.should == "Hello"
end

it 'should coerce inputs' do
get('/coerce', :int => "43")
last_response.status.should == 200
ret = MultiJson.load(last_response.body)
ret["int"].should == "Fixnum"

get('/coerce', :int => "40", :arr => ["1","20","3"], :bool => [1, 0])
last_response.status.should == 200
ret = MultiJson.load(last_response.body)
ret["int"].should == "Fixnum"
ret["arr"].should == "Fixnum"
ret["bool"].should == true
end

end

0 comments on commit 4800c93

Please sign in to comment.