ruby-grape / ruby-grape/grape

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

Open
#1,760 5 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

feature request
Dominant language
Ruby
Stars
10k
Forks
1.2k
Avg merge
14h 38m
Merged PRs (30d)
92

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in lib/grape/dsl/inside_route.rb around lines 359-364 and trace the present entry point that infers one entity from the first list member. Review the linked monkey-patch for the proposed per-member behavior, then define and verify an explicit present opt-in that renders heterogeneous entities without changing existing behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
api
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.