Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validation / coercion support #201

Merged
merged 12 commits into from
Jul 18, 2012
18 changes: 18 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,24 @@ post '/json_endpoint' do
end
```

## Validations

You can define validations and coercion option for your attributes:

```ruby
params do
required :id, type: Integer
optional :name, type: String, regexp: /^[a-z]+$/
end

get ':id' do
# params[:id] is an Integer
end
```

When a type is specified an implicit validation is done after the coercion to ensure the output type is what you asked.


## Headers

Headers are available through the `env` hash object.
Expand Down
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
extend Validations::ClassMethods

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
154 changes: 154 additions & 0 deletions lib/grape/validations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
require 'virtus'

module Grape

module Validations

##
# All validators must inherit from this class.
#
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

private

def self.convert_to_short_name(klass)
ret = klass.name.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
File.basename(ret, '_validator')
end
end

##
# Base class for all validators taking only one param.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want to create a validators folder and put all these validators into separate files.

Update: saw you did this further. I believe the SingleOptionValidator should move out too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept SingleOptionValidator there to clearly separate the foundation classes and the real validators but I can move it too if you prefer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly about it. Keep it.

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

end

# we define Validator::inherited here so SingleOptionValidator
# will not be considered a validator.
class Validator
def self.inherited(klass)
short_name = convert_to_short_name(klass)
Validations::register_validator(short_name, klass)
end
end



class <<self
attr_accessor :validators
end

self.validators = {}

def self.register_validator(short_name, klass)
validators[short_name] = klass
end


class ParamsScope
def initialize(api, &block)
@api = api
instance_eval(&block)
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

private
def validates(attrs, validations)
doc_attrs = { :required => validations.keys.include?(:presence) }

# special case (type = coerce)
if validations[:type]
validations[:coerce] = validations.delete(:type)
end

if coerce_type = validations[:coerce]
doc_attrs[:type] = coerce_type.to_s
end

if desc = validations.delete(:desc)
doc_attrs[:desc] = desc
end

@api.document_attribute(attrs, doc_attrs)

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

end

end

# This module is mixed into the API Class.
module ClassMethods
def reset_validations!
settings[:validations] = []
end

def params(&block)
ParamsScope.new(self, &block)
end

def document_attribute(names, opts)
if @last_description
@last_description[:params] ||= {}

Array(names).each do |name|
@last_description[:params][name.to_sym] ||= {}
@last_description[:params][name.to_sym].merge!(opts)
end
end
end

end

end
end

# load all defined validations
Dir[File.expand_path('../validations/*.rb', __FILE__)].each do |path|
require(path)
end
56 changes: 56 additions & 0 deletions lib/grape/validations/coerce.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

module Grape
class API
Boolean = Virtus::Attribute::Boolean
end

module Validations

class CoerceValidator < SingleOptionValidator
def validate_param!(attr_name, params)
new_value = coerce_value(@option, params[attr_name])
if valid_type?(new_value)
params[attr_name] = new_value
else
throw :error, :status => 400, :message => "invalid parameter: #{attr_name}"
end
end

private
def _valid_array_type?(type, values)
values.all? do |val|
_valid_single_type?(type, val)
end
end


def _valid_single_type?(klass, val)
if klass == Virtus::Attribute::Boolean
val.is_a?(TrueClass) || val.is_a?(FalseClass)
else
val.is_a?(klass)
end
end

def valid_type?(val)
if @option.is_a?(Array)
_valid_array_type?(@option[0], val)
else
_valid_single_type?(@option, val)
end
end

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

# not the prettiest but some invalid coercion can currently trigger
# errors in Virtus (see coerce_spec.rb)
rescue => err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do this without a begin? I learned something :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I was also glad when I found that, nice shortcut.

nil
end

end

end
end
14 changes: 14 additions & 0 deletions lib/grape/validations/presence.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Grape
module Validations

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

end
end
13 changes: 13 additions & 0 deletions lib/grape/validations/regexp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Grape
module Validations

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

end
end
Loading