From 4800c9391cab4be6758a61309d675098d7d9f5e5 Mon Sep 17 00:00:00 2001 From: Julien Ammous Date: Sat, 7 Jul 2012 13:01:22 +0200 Subject: [PATCH] added coercion+validation support --- grape.gemspec | 1 + lib/grape.rb | 1 + lib/grape/api.rb | 5 ++ lib/grape/endpoint.rb | 5 ++ lib/grape/validations.rb | 118 +++++++++++++++++++++++++++++++++ spec/grape/validations_spec.rb | 88 ++++++++++++++++++++++++ 6 files changed, 218 insertions(+) create mode 100644 lib/grape/validations.rb create mode 100644 spec/grape/validations_spec.rb diff --git a/grape.gemspec b/grape.gemspec index 133f7cdba2..e5ea244feb 100644 --- a/grape.gemspec +++ b/grape.gemspec @@ -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' diff --git a/lib/grape.rb b/lib/grape.rb index 44d5bf8303..3b8e672bb9 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -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' diff --git a/lib/grape/api.rb b/lib/grape/api.rb index eec25f8e88..7fa53161be 100644 --- a/lib/grape/api.rb +++ b/lib/grape/api.rb @@ -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 @@ -32,6 +34,7 @@ def reset! @endpoints = [] @mountings = [] @routes = nil + reset_validations! end def compile @@ -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) diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 3ec4ad3eb0..d500fa18d2 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -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 diff --git a/lib/grape/validations.rb b/lib/grape/validations.rb new file mode 100644 index 0000000000..7f1a14c8da --- /dev/null +++ b/lib/grape/validations.rb @@ -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 < 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 diff --git a/spec/grape/validations_spec.rb b/spec/grape/validations_spec.rb new file mode 100644 index 0000000000..4260cf6b00 --- /dev/null +++ b/spec/grape/validations_spec.rb @@ -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