Currently, when presenting multiple objects, Grape infers the entity it should use from the first member of the list (ref). This behavior is problematic for use cases that present heterogeneous lists of objects, i.e. those that differ with respect the entity intended for their presentation. A common such use case is family of models that implement single-table inheritance.
I propose constructing a variant of present that presents each member object using its own entity. I have monkey-patch solution currently running in production that I would like to implement within Grape itself.
Suppose you are modeling pets. All pets have names, but dogs have some attributes that cats lack, and visa versa. (I'll use Mongoid here, but any ORM that allows single table/collection inheritance could be used to illustrate this behavior.)
module Models
class Pet
include Mongoid::Document
store_in collection: 'pets'
field :name, type: String
end
class Dog < Pet
field :dog_field, type: String
end
class Cat < Pet
field :cat_field, type: String
end
end
Let's define Grape entities for these models:
module Entities
class Pet < Grape::Entity
expose :name
end
class Dog < Pet
Models::Dog::Entity = self
expose :dog_field
end
class Cat < Pet
Models::Cat::Entity = self
expose :cat_field
end
end
And finally define a /pets endpoint:
class Pets < Grape::API
namespace 'pets' do
present :data, Pet.all
end
end
Suppose we have one dog and one cat in our database. When we make a request to /pets, Grape will use the first object in our list to infer the entity it will use to represent all the objects. Let's suppose the cat is first, and so Grape will find the Entities::Cat entity. Grape will build a represenentation of the cat using the Cat entity. Then Grape will attempt to the Cat entity to represent the dog. In doing so, Grape will call cat_field on the dog, which will result in a NoMethodError.
I have implemented a solution to this problem as a monkey patch to the Grape gem. This file runs as part of the app's initialization: https://gist.github.com/hoffm/ed05817d28c261aa4dd078b30d61a7c9
This code changes the behavior of present when all of these conditions are met:
#map (this is a proxy for its being list).:with option.When all three conditions are met, the new code creates a representation of each member of the list and combines these representations into the overall representation. This allows each member to be rendered using its own entity, and therefore to be represented using its type's specific shape.
This solution is currently running in a high-throughput production environment and seems to be functioning as intended. I propose implementing something similar within Grape, except that in order to avoid a breaking change, developers should have to explicitly opt into this behavior. This opt-in could be implemented via a new option passed to present.
In future versions of Grape, it may be desirable for the behavior described here become the _default_ behavior for present.
I think it's a good idea, although I generally think that present should be simpler, rather than more complex. I wonder if we can make this behavior somehow either pluggable or maybe even move it out into custom classes where these decisions can be made, much like formatters and error formatters?
@dblock Sorry for my delay; I'm still intending to build this. I looked a little bit into formatters, and I'm not sure exactly what you have in mind. Can you briefly explain the kind of API you'd like to see?
I think you could try adding something like a API/class level presenter Presenters::Root and moving the current implementation into ::Root, then implementing an array variant.
@dblock When you refer to Presenters::Root are you thinking of a new class under the existing Grape::Presenters namespace or some new structure altogether?
No strong preferences on namespacing.
Most helpful comment
I think it's a good idea, although I generally think that
presentshould be simpler, rather than more complex. I wonder if we can make this behavior somehow either pluggable or maybe even move it out into custom classes where these decisions can be made, much like formatters and error formatters?