Skip to content

Commit 8aa3b42

Browse files
committed
Merge branch 'master' of git://github.com/intridea/grape into validation
2 parents 7194fb2 + eca3327 commit 8aa3b42

File tree

11 files changed

+345
-23
lines changed

11 files changed

+345
-23
lines changed

CHANGELOG.markdown

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Next Release
22
============
33

4+
* [#236](https://github.com/intridea/grape/pull/236): Allow validation of nested parameters. - [@tim-vandecasteele](https://github.com/tim-vandecasteele).
45
* [#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).
56
* [#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).
67
* [#210](https://github.com/intridea/grape/pull/210): Fix: `Endpoint#body_params` causing undefined method 'size' - [@adamgotterer](https://github.com/adamgotterer).
@@ -10,6 +11,7 @@ Next Release
1011
* [#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).
1112
* [#204](https://github.com/intridea/grape/pull/204): Added ability to declare shared parameters at namespace level - [@tim-vandecasteele](https://github.com/tim-vandecasteele).
1213
* [#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).
14+
* [#234](https://github.com/intridea/grape/pull/234): Adds a DSL for creating entities via mixin - [@mbleigh](https://github.com/mbleigh).
1315

1416
0.2.1 (7/11/2012)
1517
=================

README.markdown

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ You can define validations and coercion options for your parameters using `param
220220
params do
221221
requires :id, type: Integer
222222
optional :name, type: String, regexp: /^[a-z]+$/
223+
224+
group :user do
225+
requires :first_name
226+
requires :last_name
227+
end
223228
end
224229
get ':id' do
225230
# params[:id] is an Integer
@@ -229,6 +234,9 @@ end
229234
When a type is specified an implicit validation is done after the coercion to ensure
230235
the output type is the one declared.
231236

237+
Parameters can be nested using `group`. In the above example, this means both
238+
`params[:user][:first_name]` and `params[:user][:last_name]` are required next to `params[:id]`.
239+
232240
### Namespace Validation and Coercion
233241
Namespaces allow parameter definitions and apply to every method within the namespace.
234242

@@ -604,6 +612,27 @@ module API
604612
end
605613
```
606614

615+
#### Using the Exposure DSL
616+
617+
Grape ships with a DSL to easily define entities within the context
618+
of an existing class:
619+
620+
```ruby
621+
class User
622+
include Grape::Entity::DSL
623+
624+
entity :name, :email do
625+
expose :advanced, if: :conditional
626+
end
627+
end
628+
```
629+
630+
The above will automatically create a `User::Entity` class and
631+
define properties on it according to the same rules as above. If
632+
you only want to define simple exposures you don't have to supply
633+
a block and can instead simply supply a list of comma-separated
634+
symbols.
635+
607636
### Using Entities
608637

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

661+
### Entity Organization
662+
663+
In addition to separately organizing entities, it may be useful to
664+
put them as namespaced classes underneath the model they represent.
665+
For example:
666+
667+
```ruby
668+
class User
669+
def entity
670+
Entity.new(self)
671+
end
672+
673+
class Entity < Grape::Entity
674+
expose :name, :email
675+
end
676+
end
677+
```
678+
679+
If you organize your entities this way, Grape will automatically
680+
detect the `Entity` class and use it to present your models. In
681+
this example, if you added `present User.new` to your endpoint,
682+
Grape would automatically detect that there is a `User::Entity`
683+
class and use that as the representative entity. This can still
684+
be overridden by using the `:with` option or an explicit
685+
`represents` call.
686+
632687
### Caveats
633688

634689
Entities with duplicate exposure names and conditions will silently overwrite one another.

lib/grape/api.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ def nest(*blocks, &block)
386386
instance_eval &block if block_given?
387387
blocks.each{|b| instance_eval &b}
388388
settings.pop # when finished, we pop the context
389+
reset_validations!
389390
else
390391
instance_eval &block
391392
end

lib/grape/endpoint.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ def present(object, options = {})
255255
entity_class ||= (settings[:representations] || {})[potential]
256256
end
257257

258+
entity_class ||= object.class.const_get(:Entity) if object.class.const_defined?(:Entity)
259+
258260
root = options.delete(:root)
259261

260262
representation = if entity_class

lib/grape/entity.rb

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,60 @@ module Grape
4343
class Entity
4444
attr_reader :object, :options
4545

46+
# The Entity DSL allows you to mix entity functionality into
47+
# your existing classes.
48+
module DSL
49+
def self.included(base)
50+
base.extend ClassMethods
51+
ancestor_entity_class = base.ancestors.detect{|a| a.entity_class if a.respond_to?(:entity_class)}
52+
base.const_set(:Entity, Class.new(ancestor_entity_class || Grape::Entity)) unless const_defined?(:Entity)
53+
end
54+
55+
module ClassMethods
56+
# Returns the automatically-created entity class for this
57+
# Class.
58+
def entity_class(search_ancestors=true)
59+
klass = const_get(:Entity) if const_defined?(:Entity)
60+
klass ||= ancestors.detect{|a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors
61+
klass
62+
end
63+
64+
# Call this to make exposures to the entity for this Class.
65+
# Can be called with symbols for the attributes to expose,
66+
# a block that yields the full Entity DSL (See Grape::Entity),
67+
# or both.
68+
#
69+
# @example Symbols only.
70+
#
71+
# class User
72+
# include Grape::Entity::DSL
73+
#
74+
# entity :name, :email
75+
# end
76+
#
77+
# @example Mixed.
78+
#
79+
# class User
80+
# include Grape::Entity::DSL
81+
#
82+
# entity :name, :email do
83+
# expose :latest_status, using: Status::Entity, if: :include_status
84+
# expose :new_attribute, :if => {:version => 'v2'}
85+
# end
86+
# end
87+
def entity(*exposures, &block)
88+
entity_class.expose *exposures if exposures.any?
89+
entity_class.class_eval(&block) if block_given?
90+
entity_class
91+
end
92+
end
93+
94+
# Instantiates an entity version of this object.
95+
def entity
96+
self.class.entity_class.new(self)
97+
end
98+
end
99+
46100
# This method is the primary means by which you will declare what attributes
47101
# should be exposed by the entity.
48102
#
@@ -259,7 +313,7 @@ def serializable_hash(runtime_options = {})
259313
return nil if object.nil?
260314
opts = options.merge(runtime_options || {})
261315
exposures.inject({}) do |output, (attribute, exposure_options)|
262-
if exposure_options.has_key?(:proc) || object.respond_to?(attribute) && conditions_met?(exposure_options, opts)
316+
if (exposure_options.has_key?(:proc) || object.respond_to?(attribute)) && conditions_met?(exposure_options, opts)
263317
partial_output = value_for(attribute, opts)
264318
output[key_for(attribute)] =
265319
if partial_output.respond_to? :serializable_hash

lib/grape/validations.rb

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@ module Validations
88
# All validators must inherit from this class.
99
#
1010
class Validator
11-
def initialize(attrs, options, required)
11+
def initialize(attrs, options, required, scope)
1212
@attrs = Array(attrs)
1313
@required = required
14+
@scope = scope
1415

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

2021
def validate!(params)
22+
params = @scope.params(params)
23+
2124
@attrs.each do |attr_name|
2225
if @required || params.has_key?(attr_name)
2326
validate_param!(attr_name, params)
@@ -40,7 +43,7 @@ def self.convert_to_short_name(klass)
4043
##
4144
# Base class for all validators taking only one param.
4245
class SingleOptionValidator < Validator
43-
def initialize(attrs, options, required)
46+
def initialize(attrs, options, required, scope)
4447
@option = options
4548
super
4649
end
@@ -67,7 +70,11 @@ def self.register_validator(short_name, klass)
6770
end
6871

6972
class ParamsScope
70-
def initialize(api, &block)
73+
attr_accessor :element, :parent
74+
75+
def initialize(api, element, parent, &block)
76+
@element = element
77+
@parent = parent
7178
@api = api
7279
instance_eval(&block)
7380
end
@@ -89,7 +96,22 @@ def optional(*attrs)
8996

9097
validates(attrs, validations)
9198
end
92-
99+
100+
def group(element, &block)
101+
scope = ParamsScope.new(@api, element, self, &block)
102+
end
103+
104+
def params(params)
105+
params = @parent.params(params) if @parent
106+
params = params[@element] || {} if @element
107+
params
108+
end
109+
110+
def full_name(name)
111+
return "#{@parent.full_name(@element)}[#{name}]" if @parent
112+
name.to_s
113+
end
114+
93115
private
94116
def validates(attrs, validations)
95117
doc_attrs = { :required => validations.keys.include?(:presence) }
@@ -106,9 +128,10 @@ def validates(attrs, validations)
106128
if desc = validations.delete(:desc)
107129
doc_attrs[:desc] = desc
108130
end
109-
110-
@api.document_attribute(attrs, doc_attrs)
111-
131+
132+
full_attrs = attrs.collect{ |name| { :name => name, :full_name => full_name(name)} }
133+
@api.document_attribute(full_attrs, doc_attrs)
134+
112135
# Validate for presence before any other validators
113136
if validations.has_key?(:presence) && validations[:presence]
114137
validate('presence', validations[:presence], attrs, doc_attrs)
@@ -130,7 +153,7 @@ def validates(attrs, validations)
130153
def validate(type, options, attrs, doc_attrs)
131154
validator_class = Validations::validators[type.to_s]
132155
if validator_class
133-
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required])
156+
@api.settings[:validations] << validator_class.new(attrs, options, doc_attrs[:required], self)
134157
else
135158
raise "unknown validator: #{type}"
136159
end
@@ -145,17 +168,16 @@ def reset_validations!
145168
end
146169

147170
def params(&block)
148-
ParamsScope.new(self, &block)
171+
ParamsScope.new(self, nil, nil, &block)
149172
end
150173

151174
def document_attribute(names, opts)
152-
if @last_description
153-
@last_description[:params] ||= {}
154-
155-
Array(names).each do |name|
156-
@last_description[:params][name.to_s] ||= {}
157-
@last_description[:params][name.to_s].merge!(opts)
158-
end
175+
@last_description ||= {}
176+
@last_description[:params] ||= {}
177+
178+
Array(names).each do |name|
179+
@last_description[:params][name[:name].to_s] ||= {}
180+
@last_description[:params][name[:name].to_s].merge!(opts).merge!({:full_name => name[:full_name]})
159181
end
160182
end
161183

spec/grape/api_spec.rb

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,7 @@ class CommunicationError < RuntimeError; end
10291029
subject.routes.map { |route|
10301030
{ :description => route.route_description, :params => route.route_params }
10311031
}.should eq [
1032-
{ :description => "method", :params => { "ns_param" => { :required => true, :desc => "namespace parameter" }, "method_param" => { :required => false, :desc => "method parameter" } } }
1032+
{ :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" } } }
10331033
]
10341034
end
10351035
it "should merge the parameters of nested namespaces" do
@@ -1055,7 +1055,33 @@ class CommunicationError < RuntimeError; end
10551055
subject.routes.map { |route|
10561056
{ :description => route.route_description, :params => route.route_params }
10571057
}.should eq [
1058-
{ :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" } } }
1058+
{ :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" } } }
1059+
]
1060+
end
1061+
it "should provide a full_name for parameters in nested groups" do
1062+
subject.desc "nesting"
1063+
subject.params do
1064+
requires :root_param, :desc => "root param"
1065+
group :nested do
1066+
requires :nested_param, :desc => "nested param"
1067+
end
1068+
end
1069+
subject.get "method" do ; end
1070+
subject.routes.map { |route|
1071+
{ :description => route.route_description, :params => route.route_params }
1072+
}.should eq [
1073+
{ :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]" } } }
1074+
]
1075+
end
1076+
it "should parse parameters when no description is given" do
1077+
subject.params do
1078+
requires :one_param, :desc => "one param"
1079+
end
1080+
subject.get "method" do ; end
1081+
subject.routes.map { |route|
1082+
{ :description => route.route_description, :params => route.route_params }
1083+
}.should eq [
1084+
{ :description => nil, :params => { "one_param" => { :required => true, :desc => "one param", :full_name=>"one_param" } } }
10591085
]
10601086
end
10611087
it "should not symbolize params" do

spec/grape/endpoint_spec.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,20 @@ def memoized
347347
last_response.body.should == 'Hiya'
348348
end
349349

350+
it 'should automatically use Klass::Entity if that exists' do
351+
some_model = Class.new
352+
entity = Class.new(Grape::Entity)
353+
entity.stub!(:represent).and_return("Auto-detect!")
354+
355+
some_model.const_set :Entity, entity
356+
357+
subject.get '/example' do
358+
present some_model.new
359+
end
360+
get '/example'
361+
last_response.body.should == 'Auto-detect!'
362+
end
363+
350364
it 'should add a root key to the output if one is given' do
351365
subject.get '/example' do
352366
present({:abc => 'def'}, :root => :root)

0 commit comments

Comments
 (0)