Skip to content

Commit b54bf68

Browse files
Dieter Späthdblock
authored andcommitted
Refactored .represent and #delegate_attribute.
Hashes can now be presented. Added an option to present an collection of objects with a single entity.
1 parent 2929f45 commit b54bf68

File tree

4 files changed

+150
-14
lines changed

4 files changed

+150
-14
lines changed

CHANGELOG.md

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

4+
* [#85](https://github.com/intridea/grape-entity/pull/85): Added `present_collection` to indicate that an `Entity` presents an entire Collection - [@dspaeth-faber](https://github.com/dspaeth-faber).
5+
* [#85](https://guthub.com/intridea/grape-entity/pull/85): Hashes can now be passed as object to be presented and the `Hash` keys can be referenced by expose - [@dspaeth-faber](https://github.com/dspaeth-faber).
46
* Your contribution here.
57

68
0.4.3 (2014-06-12)

README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ Define a list of fields that will always be exposed.
6363
expose :user_name, :ip
6464
```
6565

66+
The field lookup takes several steps
67+
68+
* first try `entity-instance.exposure`
69+
* next try `object.exposure`
70+
* next try `object.fetch(exposure)`
71+
* last raise an Exception
72+
6673
#### Exposing with a Presenter
6774

6875
Don't derive your model classes from `Grape::Entity`, expose them using a presenter.
@@ -119,6 +126,20 @@ root 'users', 'user'
119126
expose :id, :name, ...
120127
```
121128

129+
By default every object of a collection is wrapped into an instance of your `Entity` class.
130+
You can override this behavior and wrapp the hole collection into one instance of your `Entity`
131+
class.
132+
133+
As example:
134+
135+
```ruby
136+
137+
present_collection true, :collection_name # `collection_name` is optional and defaults to `items`
138+
expose :collection_name, using: API:Items
139+
140+
141+
```
142+
122143
#### Runtime Exposure
123144

124145
Use a block or a `Proc` to evaluate exposure at runtime. The supplied block or
@@ -151,6 +172,20 @@ private
151172
end
152173
```
153174

175+
You have always access to the presented instance with `object`
176+
177+
```ruby
178+
class ExampleEntity < Grape::Entity
179+
expose :formatted_value
180+
# ...
181+
private
182+
183+
def formatted_value
184+
"+ X #{object.value}"
185+
end
186+
end
187+
```
188+
154189
#### Aliases
155190

156191
Expose under a different name with `:as`.
@@ -216,8 +251,7 @@ The above will automatically create a `Status::Entity` class and define properti
216251

217252
### Using Entities
218253

219-
With Grape, once an entity is defined, it can be used within endpoints, by calling `present`. The `present` method accepts two arguments, the object to be presented and the options associated with it. The options hash must always include `:with`, which defines the entity to expose.
220-
254+
With Grape, once an entity is defined, it can be used within endpoints, by calling `present`. The `present` method accepts two arguments, the `object` to be presented and the `options` associated with it. The options hash must always include `:with`, which defines the entity to expose.
221255
If the entity includes documentation it can be included in an endpoint's description.
222256

223257
```ruby

lib/grape_entity/entity.rb

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,62 @@ def self.root(plural, singular = nil)
329329
@root = singular
330330
end
331331

332+
# This allows you to present a collection of objects.
333+
#
334+
# @param present_collection [true or false] when true all objects will be available as
335+
# items in your presenter instead of wrapping each object in an instance of your presenter.
336+
# When false (default) every object in a collection to present will be wrapped separately
337+
# into an instance of your presenter.
338+
# @param collection_name [Symbol] the name of the collection accessor in your entity object.
339+
# Default :items
340+
#
341+
# @example Entity Definition
342+
#
343+
# module API
344+
# module Entities
345+
# class User < Grape::Entity
346+
# expose :id
347+
# end
348+
#
349+
# class Users < Grape::Entity
350+
# present_collection true
351+
# expose :items, as: 'users', using: API::Entities::Users
352+
# expose :version, documentation: { type: 'string',
353+
# desc: 'actual api version',
354+
# required: true }
355+
#
356+
# def version
357+
# options[:version]
358+
# end
359+
# end
360+
# end
361+
# end
362+
#
363+
# @example Usage in the API Layer
364+
#
365+
# module API
366+
# class Users < Grape::API
367+
# version 'v2'
368+
#
369+
# # this will render { "users" : [ { "id" : "1" }, { "id" : "2" } ], "version" : "v2" }
370+
# get '/users' do
371+
# @users = User.all
372+
# present @users, with: API::Entities::Users
373+
# end
374+
#
375+
# # this will render { "user" : { "id" : "1" } }
376+
# get '/users/:id' do
377+
# @user = User.find(params[:id])
378+
# present @user, with: API::Entities::User
379+
# end
380+
# end
381+
# end
382+
#
383+
def self.present_collection(present_collection = false, collection_name = :items)
384+
@present_collection = present_collection
385+
@collection_name = collection_name
386+
end
387+
332388
# This convenience method allows you to instantiate one or more entities by
333389
# passing either a singular or collection of objects. Each object will be
334390
# initialized with the same options. If an array of objects is passed in,
@@ -339,27 +395,34 @@ def self.root(plural, singular = nil)
339395
# @param options [Hash] Options that will be passed through to each entity
340396
# representation.
341397
#
342-
# @option options :root [String] override the default root name set for the entity.
398+
# @option options :root [String or false] override the default root name set for the entity.
343399
# Pass nil or false to represent the object or objects with no root name
344400
# even if one is defined for the entity.
401+
# @option options :serializable [true or false] when true a serializable Hash will be returned
402+
#
345403
def self.represent(objects, options = {})
346-
if objects.respond_to?(:to_ary)
347-
inner = objects.to_ary.map { |object| new(object, { collection: true }.merge(options)) }
348-
inner = inner.map(&:serializable_hash) if options[:serializable]
404+
if objects.respond_to?(:to_ary) && ! @present_collection
405+
root_element = @collection_root
406+
inner = objects.to_ary.map { |object| new(object, { collection: true }.merge(options)).presented }
349407
else
350-
inner = new(objects, options)
351-
inner = inner.serializable_hash if options[:serializable]
408+
objects = { @collection_name => objects } if @present_collection
409+
root_element = @root
410+
inner = new(objects, options).presented
352411
end
353412

354-
root_element = if options.key?(:root)
355-
options[:root]
356-
else
357-
objects.respond_to?(:to_ary) ? @collection_root : @root
358-
end
413+
root_element = options[:root] if options.key?(:root)
359414

360415
root_element ? { root_element => inner } : inner
361416
end
362417

418+
def presented
419+
if options[:serializable]
420+
serializable_hash
421+
else
422+
self
423+
end
424+
end
425+
363426
def initialize(object, options = {})
364427
@object, @options = object, options
365428
end
@@ -486,7 +549,13 @@ def delegate_attribute(attribute)
486549
if respond_to?(name, true)
487550
send(name)
488551
else
489-
object.send(name)
552+
if object.respond_to?(name, true)
553+
object.send(name)
554+
elsif object.respond_to?(:fetch, true)
555+
object.fetch(name)
556+
else
557+
raise ArgumentError
558+
end
490559
end
491560
end
492561

spec/grape_entity/entity_spec.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,37 @@ class Parent < Person
436436
representation = subject.represent(2.times.map { Object.new }, serializable: true)
437437
representation.should == [{ awesome: true }, { awesome: true }]
438438
end
439+
440+
it 'returns a serialized hash of a hash' do
441+
subject.expose(:awesome)
442+
representation = subject.represent({ awesome: true }, serializable: true)
443+
representation.should == { awesome: true }
444+
end
445+
end
446+
447+
describe '.present_collection' do
448+
it 'make the objects accessible' do
449+
subject.present_collection true
450+
subject.expose :items
451+
452+
representation = subject.represent(4.times.map { Object.new })
453+
representation.should be_kind_of(subject)
454+
representation.object.should be_kind_of(Hash)
455+
representation.object.should have_key :items
456+
representation.object[:items].should be_kind_of Array
457+
representation.object[:items].size.should be 4
458+
end
459+
460+
it 'serializes items with my root name' do
461+
subject.present_collection true, :my_items
462+
subject.expose :my_items
463+
464+
representation = subject.represent(4.times.map { Object.new }, serializable: true)
465+
representation.should be_kind_of(Hash)
466+
representation.should have_key :my_items
467+
representation[:my_items].should be_kind_of Array
468+
representation[:my_items].size.should be 4
469+
end
439470
end
440471

441472
describe '.root' do

0 commit comments

Comments
 (0)