Skip to content

Commit 90e0437

Browse files
author
Jon Evans
committed
root element name support for entities
You can specify root 'thing', 'things' in your Entity class, and your object or objects will be represented wrapped in a hash, with the appropriate top-level key.
1 parent 3e56f4d commit 90e0437

File tree

2 files changed

+124
-6
lines changed

2 files changed

+124
-6
lines changed

lib/grape/entity.rb

Lines changed: 47 additions & 4 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,46 @@ def self.exposures
8585
(@exposures ||= {})
8686
end
8787

88+
# This allows you to set a root element name for your representation.
89+
#
90+
# @param singular [String] the root key to use when representing a single object
91+
# @param plural [String] the root key to use when representing a collection of objects
92+
#
93+
# @example Entity Definition
94+
#
95+
# module API
96+
# module Entities
97+
# class User < Grape::Entity
98+
# root 'user', 'users'
99+
# expose :id
100+
# end
101+
# end
102+
# end
103+
#
104+
# @example Usage in the API Layer
105+
#
106+
# module API
107+
# class Users < Grape::API
108+
# version 'v2'
109+
#
110+
# # this will render { "users": [ {"id":"1"}, {"id":"2"} ] }
111+
# get '/users' do
112+
# @users = User.all
113+
# present @users, :with => API::Entities::User
114+
# end
115+
#
116+
# # this will render { "user": {"id":"1"} }
117+
# get '/users/:id' do
118+
# @user = User.find(params[:id])
119+
# present @user, :with => API::Entities::User
120+
# end
121+
# end
122+
# end
123+
def self.root(singular, plural=nil)
124+
@root = singular
125+
@collection_root = plural
126+
end
127+
88128
# This convenience method allows you to instantiate one or more entities by
89129
# passing either a singular or collection of objects. Each object will be
90130
# initialized with the same options. If an array of objects is passed in,
@@ -95,11 +135,14 @@ def self.exposures
95135
# @param options [Hash] Options that will be passed through to each entity
96136
# representation.
97137
def self.represent(objects, options = {})
98-
if objects.is_a?(Array)
138+
inner = if objects.is_a?(Array)
99139
objects.map{|o| self.new(o, {:collection => true}.merge(options))}
100140
else
101141
self.new(objects, options)
102142
end
143+
144+
root_element = objects.is_a?(Array) ? @collection_root : @root
145+
root_element ? { root_element => inner } : inner
103146
end
104147

105148
def initialize(object, options = {})

spec/grape/entity_spec.rb

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,81 @@
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 'thing', 'things'
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+
end
88+
89+
context 'with singular root key' do
90+
before(:each) do
91+
subject.root 'thing'
92+
end
93+
94+
context 'with a single object' do
95+
it 'should allow a root element name to be specified' do
96+
representation = subject.represent(Object.new)
97+
representation.should be_kind_of(Hash)
98+
representation.should have_key('thing')
99+
representation['thing'].should be_kind_of(subject)
100+
end
101+
end
102+
103+
context 'with an array of objects' do
104+
it 'should allow a root element name to be specified' do
105+
representation = subject.represent(4.times.map{Object.new})
106+
representation.should be_kind_of(Array)
107+
representation.size.should == 4
108+
representation.reject{|r| r.kind_of?(subject)}.should be_empty
109+
end
110+
end
111+
end
112+
113+
context 'with plural root key' do
114+
before(:each) do
115+
subject.root nil, 'things'
116+
end
117+
118+
context 'with a single object' do
119+
it 'should allow a root element name to be specified' do
120+
subject.represent(Object.new).should be_kind_of(subject)
121+
end
122+
end
123+
124+
context 'with an array of objects' do
125+
it 'should allow a root element name to be specified' do
126+
representation = subject.represent(4.times.map{Object.new})
127+
representation.should be_kind_of(Hash)
128+
representation.should have_key('things')
129+
representation['things'].should be_kind_of(Array)
130+
representation['things'].size.should == 4
131+
representation['things'].reject{|r| r.kind_of?(subject)}.should be_empty
132+
end
133+
end
134+
end
135+
end
136+
62137
describe '#initialize' do
63138
it 'should take an object and an optional options hash' do
64139
expect{ subject.new(Object.new) }.not_to raise_error
@@ -77,10 +152,10 @@
77152
context 'instance methods' do
78153
let(:model){ mock(attributes) }
79154
let(:attributes){ {
80-
:name => 'Bob Bobson',
155+
:name => 'Bob Bobson',
81156
:email => '[email protected]',
82157
:friends => [
83-
mock(:name => "Friend 1", :email => '[email protected]', :friends => []),
158+
mock(:name => "Friend 1", :email => '[email protected]', :friends => []),
84159
mock(:name => "Friend 2", :email => '[email protected]', :friends => [])
85160
]
86161
} }

0 commit comments

Comments
 (0)