Skip to content

Commit

Permalink
Merge branch 'master' of git://github.com/intridea/grape into validation
Browse files Browse the repository at this point in the history
  • Loading branch information
adamgotterer committed Sep 5, 2012
2 parents 7194fb2 + eca3327 commit 8aa3b42
Show file tree
Hide file tree
Showing 11 changed files with 345 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.markdown
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Next Release
============

* [#236](https://github.com/intridea/grape/pull/236): Allow validation of nested parameters. - [@tim-vandecasteele](https://github.com/tim-vandecasteele).
* [#201](https://github.com/intridea/grape/pull/201): Added custom exceptions to Grape. Updated validations to use ValidationError that can be rescued. - [@adamgotterer](https://github.com/adamgotterer).
* [#211](https://github.com/intridea/grape/pull/211): Updates to validation and coercion: Fix #211 and force order of operations for presence and coercion - [@adamgotterer](https://github.com/adamgotterer).
* [#210](https://github.com/intridea/grape/pull/210): Fix: `Endpoint#body_params` causing undefined method 'size' - [@adamgotterer](https://github.com/adamgotterer).
Expand All @@ -10,6 +11,7 @@ Next Release
* [#203](https://github.com/intridea/grape/pull/203): Added a check to `Entity#serializable_hash` that verifies an entity exists on an object - [@adamgotterer](https://github.com/adamgotterer).
* [#204](https://github.com/intridea/grape/pull/204): Added ability to declare shared parameters at namespace level - [@tim-vandecasteele](https://github.com/tim-vandecasteele).
* [#208](https://github.com/intridea/grape/pull/208): `Entity#serializable_hash` must also check if attribute is generated by a user supplied block - [@ppadron](https://github.com/ppadron).
* [#234](https://github.com/intridea/grape/pull/234): Adds a DSL for creating entities via mixin - [@mbleigh](https://github.com/mbleigh).

0.2.1 (7/11/2012)
=================
Expand Down
55 changes: 55 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ You can define validations and coercion options for your parameters using `param
params do
requires :id, type: Integer
optional :name, type: String, regexp: /^[a-z]+$/

group :user do
requires :first_name
requires :last_name
end
end
get ':id' do
# params[:id] is an Integer
Expand All @@ -229,6 +234,9 @@ end
When a type is specified an implicit validation is done after the coercion to ensure
the output type is the one declared.

Parameters can be nested using `group`. In the above example, this means both
`params[:user][:first_name]` and `params[:user][:last_name]` are required next to `params[:id]`.

### Namespace Validation and Coercion
Namespaces allow parameter definitions and apply to every method within the namespace.

Expand Down Expand Up @@ -604,6 +612,27 @@ module API
end
```

#### Using the Exposure DSL

Grape ships with a DSL to easily define entities within the context
of an existing class:

```ruby
class User
include Grape::Entity::DSL

entity :name, :email do
expose :advanced, if: :conditional
end
end
```

The above will automatically create a `User::Entity` class and
define properties on it according to the same rules as above. If
you only want to define simple exposures you don't have to supply
a block and can instead simply supply a list of comma-separated
symbols.

### Using Entities

Once an entity is defined, it can be used within endpoints, by calling #present. The #present
Expand All @@ -629,6 +658,32 @@ module API
end
```

### Entity Organization

In addition to separately organizing entities, it may be useful to
put them as namespaced classes underneath the model they represent.
For example:

```ruby
class User
def entity
Entity.new(self)
end

class Entity < Grape::Entity
expose :name, :email
end
end
```

If you organize your entities this way, Grape will automatically
detect the `Entity` class and use it to present your models. In
this example, if you added `present User.new` to your endpoint,
Grape would automatically detect that there is a `User::Entity`
class and use that as the representative entity. This can still
be overridden by using the `:with` option or an explicit
`represents` call.

### Caveats

Entities with duplicate exposure names and conditions will silently overwrite one another.
Expand Down
1 change: 1 addition & 0 deletions lib/grape/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ def nest(*blocks, &block)
instance_eval &block if block_given?
blocks.each{|b| instance_eval &b}
settings.pop # when finished, we pop the context
reset_validations!
else
instance_eval &block
end
Expand Down
2 changes: 2 additions & 0 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ def present(object, options = {})
entity_class ||= (settings[:representations] || {})[potential]
end

entity_class ||= object.class.const_get(:Entity) if object.class.const_defined?(:Entity)

root = options.delete(:root)

representation = if entity_class
Expand Down
56 changes: 55 additions & 1 deletion lib/grape/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,60 @@ module Grape
class Entity
attr_reader :object, :options

# The Entity DSL allows you to mix entity functionality into
# your existing classes.
module DSL
def self.included(base)
base.extend ClassMethods
ancestor_entity_class = base.ancestors.detect{|a| a.entity_class if a.respond_to?(:entity_class)}
base.const_set(:Entity, Class.new(ancestor_entity_class || Grape::Entity)) unless const_defined?(:Entity)
end

module ClassMethods
# Returns the automatically-created entity class for this
# Class.
def entity_class(search_ancestors=true)
klass = const_get(:Entity) if const_defined?(:Entity)
klass ||= ancestors.detect{|a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors
klass
end

# Call this to make exposures to the entity for this Class.
# Can be called with symbols for the attributes to expose,
# a block that yields the full Entity DSL (See Grape::Entity),
# or both.
#
# @example Symbols only.
#
# class User
# include Grape::Entity::DSL
#
# entity :name, :email
# end
#
# @example Mixed.
#
# class User
# include Grape::Entity::DSL
#
# entity :name, :email do
# expose :latest_status, using: Status::Entity, if: :include_status
# expose :new_attribute, :if => {:version => 'v2'}
# end
# end
def entity(*exposures, &block)
entity_class.expose *exposures if exposures.any?
entity_class.class_eval(&block) if block_given?
entity_class
end
end

# Instantiates an entity version of this object.
def entity
self.class.entity_class.new(self)
end
end

# This method is the primary means by which you will declare what attributes
# should be exposed by the entity.
#
Expand Down Expand Up @@ -259,7 +313,7 @@ def serializable_hash(runtime_options = {})
return nil if object.nil?
opts = options.merge(runtime_options || {})
exposures.inject({}) do |output, (attribute, exposure_options)|
if exposure_options.has_key?(:proc) || object.respond_to?(attribute) && conditions_met?(exposure_options, opts)
if (exposure_options.has_key?(:proc) || object.respond_to?(attribute)) && conditions_met?(exposure_options, opts)
partial_output = value_for(attribute, opts)
output[key_for(attribute)] =
if partial_output.respond_to? :serializable_hash
Expand Down
54 changes: 38 additions & 16 deletions lib/grape/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ module Validations
# All validators must inherit from this class.
#
class Validator
def initialize(attrs, options, required)
def initialize(attrs, options, required, scope)
@attrs = Array(attrs)
@required = required
@scope = scope

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

def validate!(params)
params = @scope.params(params)

@attrs.each do |attr_name|
if @required || params.has_key?(attr_name)
validate_param!(attr_name, params)
Expand All @@ -40,7 +43,7 @@ def self.convert_to_short_name(klass)
##
# Base class for all validators taking only one param.
class SingleOptionValidator < Validator
def initialize(attrs, options, required)
def initialize(attrs, options, required, scope)
@option = options
super
end
Expand All @@ -67,7 +70,11 @@ def self.register_validator(short_name, klass)
end

class ParamsScope
def initialize(api, &block)
attr_accessor :element, :parent

def initialize(api, element, parent, &block)
@element = element
@parent = parent
@api = api
instance_eval(&block)
end
Expand All @@ -89,7 +96,22 @@ def optional(*attrs)

validates(attrs, validations)
end


def group(element, &block)
scope = ParamsScope.new(@api, element, self, &block)
end

def params(params)
params = @parent.params(params) if @parent
params = params[@element] || {} if @element
params
end

def full_name(name)
return "#{@parent.full_name(@element)}[#{name}]" if @parent
name.to_s
end

private
def validates(attrs, validations)
doc_attrs = { :required => validations.keys.include?(:presence) }
Expand All @@ -106,9 +128,10 @@ def validates(attrs, validations)
if desc = validations.delete(:desc)
doc_attrs[:desc] = desc
end

@api.document_attribute(attrs, doc_attrs)


full_attrs = attrs.collect{ |name| { :name => name, :full_name => full_name(name)} }
@api.document_attribute(full_attrs, doc_attrs)

# Validate for presence before any other validators
if validations.has_key?(:presence) && validations[:presence]
validate('presence', validations[:presence], attrs, doc_attrs)
Expand All @@ -130,7 +153,7 @@ def validates(attrs, validations)
def validate(type, options, attrs, doc_attrs)
validator_class = Validations::validators[type.to_s]
if validator_class
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required])
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required], self)
else
raise "unknown validator: #{type}"
end
Expand All @@ -145,17 +168,16 @@ def reset_validations!
end

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

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

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

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

Expand Down
30 changes: 28 additions & 2 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ class CommunicationError < RuntimeError; end
subject.routes.map { |route|
{ :description => route.route_description, :params => route.route_params }
}.should eq [
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "namespace parameter" }, "method_param" => { :required => false, :desc => "method parameter" } } }
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "namespace parameter", :full_name=>"ns_param" }, "method_param" => { :required => false, :desc => "method parameter", :full_name=>"method_param" } } }
]
end
it "should merge the parameters of nested namespaces" do
Expand All @@ -1055,7 +1055,33 @@ class CommunicationError < RuntimeError; end
subject.routes.map { |route|
{ :description => route.route_description, :params => route.route_params }
}.should eq [
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "ns param 2" }, "ns1_param" => { :required => true, :desc => "ns1 param" }, "ns2_param" => { :required => true, :desc => "ns2 param" }, "method_param" => { :required => false, :desc => "method param" } } }
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "ns param 2", :full_name=>"ns_param" }, "ns1_param" => { :required => true, :desc => "ns1 param", :full_name=>"ns1_param" }, "ns2_param" => { :required => true, :desc => "ns2 param", :full_name=>"ns2_param" }, "method_param" => { :required => false, :desc => "method param", :full_name=>"method_param" } } }
]
end
it "should provide a full_name for parameters in nested groups" do
subject.desc "nesting"
subject.params do
requires :root_param, :desc => "root param"
group :nested do
requires :nested_param, :desc => "nested param"
end
end
subject.get "method" do ; end
subject.routes.map { |route|
{ :description => route.route_description, :params => route.route_params }
}.should eq [
{ :description => "nesting", :params => { "root_param" => { :required => true, :desc => "root param", :full_name=>"root_param" }, "nested_param" => { :required => true, :desc => "nested param", :full_name=>"nested[nested_param]" } } }
]
end
it "should parse parameters when no description is given" do
subject.params do
requires :one_param, :desc => "one param"
end
subject.get "method" do ; end
subject.routes.map { |route|
{ :description => route.route_description, :params => route.route_params }
}.should eq [
{ :description => nil, :params => { "one_param" => { :required => true, :desc => "one param", :full_name=>"one_param" } } }
]
end
it "should not symbolize params" do
Expand Down
14 changes: 14 additions & 0 deletions spec/grape/endpoint_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,20 @@ def memoized
last_response.body.should == 'Hiya'
end

it 'should automatically use Klass::Entity if that exists' do
some_model = Class.new
entity = Class.new(Grape::Entity)
entity.stub!(:represent).and_return("Auto-detect!")

some_model.const_set :Entity, entity

subject.get '/example' do
present some_model.new
end
get '/example'
last_response.body.should == 'Auto-detect!'
end

it 'should add a root key to the output if one is given' do
subject.get '/example' do
present({:abc => 'def'}, :root => :root)
Expand Down
Loading

0 comments on commit 8aa3b42

Please sign in to comment.