Grape: Allow presentation of lists using heterogeneous entities (e.g. for STI data models)

Created on 21 May 2018  路  5Comments  路  Source: ruby-grape/grape

Summary

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.

Example of the Problem

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.

Proposed Solution

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:

  1. The object responds to #map (this is a proxy for its being list).
  2. No entity is explicitly specified via the :with option.
  3. The members of the list are instances of models that collectively specify more than one entity class.

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.

Future Versions

In future versions of Grape, it may be desirable for the behavior described here become the _default_ behavior for present.

feature request

Most helpful comment

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?

All 5 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wxianfeng picture wxianfeng  路  8Comments

dmmcgee picture dmmcgee  路  6Comments

davidmccoy picture davidmccoy  路  3Comments

jellybob picture jellybob  路  4Comments

dblock picture dblock  路  7Comments