Last active
May 21, 2018 15:23
-
-
Save hoffm/ed05817d28c261aa4dd078b30d61a7c9 to your computer and use it in GitHub Desktop.
Monkey patch to allow Grape's `present` method to represent list of objects with heterogeneous entities
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ### | |
# # OVERVIEW | |
# ### | |
# | |
# Monkey patch of Grape DSL to augment behavior in cases where | |
# we are presenting lists of objects of different types. The | |
# primary use case for this is the presentation of lists of | |
# records that inherit from an abstract parent, i.e. single- | |
# table inheritance. | |
# | |
# Grape's default behavior is to present each member of a list | |
# using the entity associated with the first member of the list. | |
# The new behavior presents each member of a heterogeneous list | |
# using that object's own associated entity. | |
# | |
# The original `present` method is defined here: | |
# | |
# https://github.com/ruby-grape/grape/blob/v1.0.2/lib/grape/dsl/inside_route.rb#L292-L333 | |
# | |
# ### | |
# # DETAILS | |
# ### | |
# | |
# More precisely, this code changes the behavior of the `present` | |
# macro when all of the following conditions are met: | |
# | |
# 1. The object to be presented is a list, i.e. it responds to | |
# `#map` | |
# 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. (Models | |
# specify an entity class by assigning it to an `Entity` | |
# constant namespaced to the model.) | |
# | |
# When any of these conditions fails, we fallback on the original | |
# behavior of `present`. However, when all three conditions obtain, | |
# we invoke a new method, `heterogeneous_present`. | |
# | |
# The new method creates a representation of each member of the | |
# list and combines them 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. | |
# | |
# ### | |
# # EXAMPLE | |
# ### | |
# | |
# The new behavior allows presentation of heterogeneous lists. | |
# For example, suppose you have `Dog` and `Cat` models that | |
# each inherit from an abstract `Pet` model. `Dog` and `Cat` | |
# each have fields that the other lacks. These type-specific | |
# fields are exposed in the models' respective entities. The | |
# code is this file allows us to render an index view of pets | |
# with output like the following: | |
# | |
# { | |
# "data" => [ | |
# {"_type" => "Dog", "name" => "meatball", "dog_field" => "ruff"}, | |
# {"_type" => "Cat", "name" => "puffers", "cat_field" => "purr"} | |
# ], | |
# "page" => 1 | |
# } | |
# | |
# Such a presentation is impossible using the Grape library alone. | |
module Grape | |
module DSL | |
module InsideRoute | |
alias_method :homogeneous_present, :present | |
def present(*args) | |
original_args = args.dup | |
options = args.count > 1 ? args.extract_options! : {} | |
key, object = if args.count == 2 && args.first.is_a?(Symbol) | |
args | |
else | |
[nil, args.first] | |
end | |
no_explicit_entity = options[:with].nil? | |
object_is_mappable_list = object.respond_to?(:map) && !object.is_a?(Hash) | |
if no_explicit_entity && object_is_mappable_list | |
entities = object.map{ |member| entity_class_for_obj(member, options) } | |
if entities.uniq.count > 1 | |
return heterogeneous_present(key, object, entities, options) | |
end | |
end | |
homogeneous_present(*original_args) | |
end | |
def heterogeneous_present(key, objects, entities, options) | |
root = options.delete(:root) | |
representation = objects.zip(entities).map do |object, entity| | |
entity.present? ? entity_representation_for(entity, object, options) : object | |
end | |
representation = { root => representation } if root | |
if key | |
representation = (@body || {}).merge(key => representation) | |
elsif entities.all(&:present?) && @body | |
raise ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?(:merge) | |
representation = @body.merge(representation) | |
end | |
body representation | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment