Skip to content

Commit 212c96c

Browse files
author
Michael Bleigh
committed
Merge pull request ruby-grape#101 from springyweb/root_objects
Root keys for entity representations
2 parents f4db5f0 + f951738 commit 212c96c

File tree

2 files changed

+169
-7
lines changed

2 files changed

+169
-7
lines changed

lib/grape/entity.rb

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
require 'hashie'
22

33
module Grape
4-
# An Entity is a lightweight structure that allows you to easily
4+
# An Entity is a lightweight structure that allows you to easily
55
# represent data from your application in a consistent and abstracted
66
# way in your API.
77
#
@@ -19,7 +19,7 @@ module Grape
1919
# end
2020
# end
2121
#
22-
# Entities are not independent structures, rather, they create
22+
# Entities are not independent structures, rather, they create
2323
# **representations** of other Ruby objects using a number of methods
2424
# that are convenient for use in an API. Once you've defined an Entity,
2525
# you can use it in your API like this:
@@ -54,7 +54,7 @@ class Entity
5454
# it will yield the object being represented and the options passed to the
5555
# representation call. Return true to prevent exposure, false to allow it.
5656
# @option options :using This option allows you to map an attribute to another Grape
57-
# Entity. Pass it a Grape::Entity class and the attribute in question will
57+
# Entity. Pass it a Grape::Entity class and the attribute in question will
5858
# automatically be transformed into a representation that will receive the same
5959
# options as the parent entity when called. Note that arrays are fine here and
6060
# will automatically be detected and handled appropriately.
@@ -85,6 +85,50 @@ def self.exposures
8585
(@exposures ||= {})
8686
end
8787

88+
# This allows you to set a root element name for your representation.
89+
#
90+
# @param plural [String] the root key to use when representing
91+
# a collection of objects. If missing or nil, no root key will be used
92+
# when representing collections of objects.
93+
# @param singular [String] the root key to use when representing
94+
# a single object. If missing or nil, no root key will be used when
95+
# representing an individual object.
96+
#
97+
# @example Entity Definition
98+
#
99+
# module API
100+
# module Entities
101+
# class User < Grape::Entity
102+
# root 'users', 'user'
103+
# expose :id
104+
# end
105+
# end
106+
# end
107+
#
108+
# @example Usage in the API Layer
109+
#
110+
# module API
111+
# class Users < Grape::API
112+
# version 'v2'
113+
#
114+
# # this will render { "users": [ {"id":"1"}, {"id":"2"} ] }
115+
# get '/users' do
116+
# @users = User.all
117+
# present @users, :with => API::Entities::User
118+
# end
119+
#
120+
# # this will render { "user": {"id":"1"} }
121+
# get '/users/:id' do
122+
# @user = User.find(params[:id])
123+
# present @user, :with => API::Entities::User
124+
# end
125+
# end
126+
# end
127+
def self.root(plural, singular=nil)
128+
@collection_root = plural
129+
@root = singular
130+
end
131+
88132
# This convenience method allows you to instantiate one or more entities by
89133
# passing either a singular or collection of objects. Each object will be
90134
# initialized with the same options. If an array of objects is passed in,
@@ -94,12 +138,23 @@ def self.exposures
94138
# @param objects [Object or Array] One or more objects to be represented.
95139
# @param options [Hash] Options that will be passed through to each entity
96140
# representation.
141+
#
142+
# @option options :root [String] override the default root name set for the
143+
#  entity. Pass nil or false to represent the object or objects with no
144+
# root name even if one is defined for the entity.
97145
def self.represent(objects, options = {})
98-
if objects.is_a?(Array)
146+
inner = if objects.is_a?(Array)
99147
objects.map{|o| self.new(o, {:collection => true}.merge(options))}
100148
else
101149
self.new(objects, options)
102150
end
151+
152+
root_element = if options.has_key?(:root)
153+
options[:root]
154+
else
155+
objects.is_a?(Array) ? @collection_root : @root
156+
end
157+
root_element ? { root_element => inner } : inner
103158
end
104159

105160
def initialize(object, options = {})
@@ -140,7 +195,7 @@ def value_for(attribute, options = {})
140195
if exposure_options[:proc]
141196
exposure_options[:proc].call(object, options)
142197
elsif exposure_options[:using]
143-
exposure_options[:using].represent(object.send(attribute))
198+
exposure_options[:using].represent(object.send(attribute), :root => nil)
144199
else
145200
object.send(attribute)
146201
end

spec/grape/entity_spec.rb

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,98 @@
5959
end
6060
end
6161

62+
describe '.root' do
63+
context 'with singular and plural root keys' do
64+
before(:each) do
65+
subject.root 'things', 'thing'
66+
end
67+
68+
context 'with a single object' do
69+
it 'should allow a root element name to be specified' do
70+
representation = subject.represent(Object.new)
71+
representation.should be_kind_of(Hash)
72+
representation.should have_key('thing')
73+
representation['thing'].should be_kind_of(subject)
74+
end
75+
end
76+
77+
context 'with an array of objects' do
78+
it 'should allow a root element name to be specified' do
79+
representation = subject.represent(4.times.map{Object.new})
80+
representation.should be_kind_of(Hash)
81+
representation.should have_key('things')
82+
representation['things'].should be_kind_of(Array)
83+
representation['things'].size.should == 4
84+
representation['things'].reject{|r| r.kind_of?(subject)}.should be_empty
85+
end
86+
end
87+
88+
context 'it can be overridden' do
89+
it 'can be disabled' do
90+
representation = subject.represent(4.times.map{Object.new}, :root=>false)
91+
representation.should be_kind_of(Array)
92+
representation.size.should == 4
93+
representation.reject{|r| r.kind_of?(subject)}.should be_empty
94+
end
95+
it 'can use a different name' do
96+
representation = subject.represent(4.times.map{Object.new}, :root=>'others')
97+
representation.should be_kind_of(Hash)
98+
representation.should have_key('others')
99+
representation['others'].should be_kind_of(Array)
100+
representation['others'].size.should == 4
101+
representation['others'].reject{|r| r.kind_of?(subject)}.should be_empty
102+
end
103+
end
104+
end
105+
106+
context 'with singular root key' do
107+
before(:each) do
108+
subject.root nil, 'thing'
109+
end
110+
111+
context 'with a single object' do
112+
it 'should allow a root element name to be specified' do
113+
representation = subject.represent(Object.new)
114+
representation.should be_kind_of(Hash)
115+
representation.should have_key('thing')
116+
representation['thing'].should be_kind_of(subject)
117+
end
118+
end
119+
120+
context 'with an array of objects' do
121+
it 'should allow a root element name to be specified' do
122+
representation = subject.represent(4.times.map{Object.new})
123+
representation.should be_kind_of(Array)
124+
representation.size.should == 4
125+
representation.reject{|r| r.kind_of?(subject)}.should be_empty
126+
end
127+
end
128+
end
129+
130+
context 'with plural root key' do
131+
before(:each) do
132+
subject.root 'things'
133+
end
134+
135+
context 'with a single object' do
136+
it 'should allow a root element name to be specified' do
137+
subject.represent(Object.new).should be_kind_of(subject)
138+
end
139+
end
140+
141+
context 'with an array of objects' do
142+
it 'should allow a root element name to be specified' do
143+
representation = subject.represent(4.times.map{Object.new})
144+
representation.should be_kind_of(Hash)
145+
representation.should have_key('things')
146+
representation['things'].should be_kind_of(Array)
147+
representation['things'].size.should == 4
148+
representation['things'].reject{|r| r.kind_of?(subject)}.should be_empty
149+
end
150+
end
151+
end
152+
end
153+
62154
describe '#initialize' do
63155
it 'should take an object and an optional options hash' do
64156
expect{ subject.new(Object.new) }.not_to raise_error
@@ -77,10 +169,10 @@
77169
context 'instance methods' do
78170
let(:model){ mock(attributes) }
79171
let(:attributes){ {
80-
:name => 'Bob Bobson',
172+
:name => 'Bob Bobson',
81173
:email => '[email protected]',
82174
:friends => [
83-
mock(:name => "Friend 1", :email => '[email protected]', :friends => []),
175+
mock(:name => "Friend 1", :email => '[email protected]', :friends => []),
84176
mock(:name => "Friend 2", :email => '[email protected]', :friends => [])
85177
]
86178
} }
@@ -119,6 +211,21 @@
119211
rep.last.serializable_hash[:name].should == 'Friend 2'
120212
end
121213

214+
it 'should disable root key name for child representations' do
215+
class FriendEntity < Grape::Entity
216+
root 'friends', 'friend'
217+
expose :name, :email
218+
end
219+
fresh_class.class_eval do
220+
expose :friends, :using => FriendEntity
221+
end
222+
rep = subject.send(:value_for, :friends)
223+
rep.should be_kind_of(Array)
224+
rep.reject{|r| r.is_a?(FriendEntity)}.should be_empty
225+
rep.first.serializable_hash[:name].should == 'Friend 1'
226+
rep.last.serializable_hash[:name].should == 'Friend 2'
227+
end
228+
122229
it 'should call through to the proc if there is one' do
123230
subject.send(:value_for, :computed, :awesome => 123).should == 123
124231
end

0 commit comments

Comments
 (0)