Skip to content
Merged
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