In my last blog post I presented in general the features I like the most about the grape gem. Now I will present the grape entities which are related to the presentation of the returned data. I like these entities a lot because they are an OO way to present data and they help a lot to keep my presentation layer DRY.
The last statement of every grape call is returned to the caller as JSON.
Despite this simple efficiency I like to have more control over the returned data and I use the grape entities to format the returned data wherever I can. I can also rspec them and I am doing it extensively because I am a Test Driven Guy ;)
Let me present some examples of entities their usage and how do I spec them:
Here is the spec for the entity:
describe GameEntity do
describe 'fields' do
subject(:subject) { GameEntity }
it { is_expected.to represent(:id) }
it { is_expected.to represent(:user_id).as(:owner_id) }
...
Here is the entity itself:
class GameEntity < Grape::Entity
expose :id
expose :user_id, as: :owner_id
...
The part of the grape API:
namespace :games do
desc "Retrieve all the games"
params do
optional :include_reviews, type: Boolean, default: true, desc: 'Accepts: true/false.'
end
get do
games = Game.actual.limit(500)
present games, with: GameEntity
This was simple so far. But I can use a more sophisticated GameEntity in case I want to present the included reviews. In all other cases I will use my old simple GameEntity to present the Game.
Let's see that case too:
I modify the grape API to this:
...
if params[:include_reviews]
games = games.includes(:reviews)
present games, with: DetailedGameEntity
else
present games, with: GameEntity
else
present games, with: GameEntity
end
I also spec the new entity:
describe DetailedGameEntity do
describe 'special fields' do
subject(:detailed_game) { DetailedGameEntity(game).as_json }
specify { expect(detailed_game[:reviews]).to be_an(Array) }
end
describe DetailedGameEntity do
describe 'special fields' do
subject(:detailed_game) { DetailedGameEntity(game).as_json }
specify { expect(detailed_game[:reviews]).to be_an(Array) }
end
Of course the above was a very simple example.
In real world scenarios we can find ourselves that we are reusing the entities like this:
class DetailedGameEntity < Grape::Entity
expose :game, using: GameEntity do |game, options|
game
end
expose :venue, using: VenueEntity do |game, options|
game.the_venue
end
expose :visitor_team, using: TeamEntity do |game, options|
game.visitor_team
end
expose :host_team, using: TeamEntity do |game, options|
game.host_team
end
end
As you can see we are keeping our entity codebase DRY by calling the TeamEntity twice in the DetailedGameEntity. This is a typical use of delegation of the Entities.
Let me show a more complex example of our entities:
The usage of the entities:
desc "Retrieve all the reviews belonging to a team"
get do
...
present reviews, :with => ReviewPresenter, user: current_user
end
The entity itself:
class ReviewPresenter < Grape::Entity
expose :current_user_likes do |review, options|
review.likes.pluck(:user_id).include? options[:user].id if options[:user].present?
end
end
And finally the spec for the entity:
describe ReviewPresenter do
describe 'special fields' do
context 'without passing a user' do
subject(:presented_review) { ReviewPresenter.new(image.review).as_json }
specify { expect(presented_review[:current_user_likes]).to be_nil }
end
subject(:presented_review) { ReviewPresenter.new(image.review, user: current_user).as_json }
...
context 'user is the current_user' do
specify { expect(presented_review[:current_user_likes]).to eq(true) }
end
end
...
end
end
What I was doing here is to populate the field 'current_user_likes'
to true or false depending if the current_user likes or not the presented review.
As you can see the presentation layer of any grape API could became more DRY and versatile by using the grape entities.
Enjoy using them ;)