Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to "flatten" nested entities into parent (e.g. for CSV) #45

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
79 changes: 72 additions & 7 deletions lib/grape_entity/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,58 @@ def self.with_options(options)
yield
@block_options.pop
end

# Merge exposures from another entity into the current entity
# as a way to "flatten" multiple models for use in formats such as "CSV".
#
# @example Merge child entity into parent
#
# class Address < Grape::Entity
# expose :street, :city, :state, :zip
# end
#
# class Contact < Grape::Entity
# expose :name
# expose :addresses, using: Address, unless: { format: :csv }
# merge_with Address, if: { format: :csv } do
# object.addresses.first
# end
# end
def self.merge_with(*entity_classes, &block)
merge_options = entity_classes.last.is_a?(Hash) ? entity_classes.pop.dup : {}
except_attributes = [merge_options.delete(:except)].compact
only_attributes = [merge_options.delete(:only)].compact
prefix = merge_options.delete(:prefix)
suffix = merge_options.delete(:suffix)

merge_options[:object] = block if block_given?

entity_classes.each do |entity_class|
raise ArgumentError, "#{entity_class} must be a Grape::Entity" unless entity_class < Grape::Entity

merged_entities[entity_class] = merge_options

entity_class.exposures.each_pair do |attribute, expose_options|
next if except_attributes.include?(attribute)
next if only_attributes.any? && !only_attributes.include?(attribute)

options = expose_options.dup.merge(merge_options)

[:if, :unless].each do |condition|
if merge_options.has_key?(condition) && expose_options.has_key?(condition)
options[condition] = Proc.new{|object, instance_options| conditions_met?(merge_options, instance_options) && conditions_met?(expose_options, instance_options)}
end
end

expose :"#{prefix}#{attribute}#{suffix}", options
end

end
end

def self.merged_entities
@merged_entities ||= superclass.respond_to?(:merged_entities) ? superclass.exposures.dup : {}
end

# Returns a hash of exposures that have been declared for this Entity or ancestors. The keys
# are symbolized references to methods on the containing object, the values are
Expand Down Expand Up @@ -388,27 +440,40 @@ def value_for(attribute, options = {})
using_options = options.dup
using_options.delete(:collection)
using_options[:root] = nil
exposure_options[:using].represent(delegate_attribute(attribute), using_options)
exposure_options[:using].represent(delegate_attribute(attribute, exposure_options[:object]), using_options)
elsif exposure_options[:format_with]
format_with = exposure_options[:format_with]

if format_with.is_a?(Symbol) && formatters[format_with]
instance_exec(delegate_attribute(attribute), &formatters[format_with])
instance_exec(delegate_attribute(attribute, exposure_options[:object]), &formatters[format_with])
elsif format_with.is_a?(Symbol)
send(format_with, delegate_attribute(attribute))
send(format_with, delegate_attribute(attribute, exposure_options[:object]))
elsif format_with.respond_to? :call
instance_exec(delegate_attribute(attribute), &format_with)
instance_exec(delegate_attribute(attribute, exposure_options[:object]), &format_with)
end
else
delegate_attribute(attribute)
delegate_attribute(attribute, exposure_options[:object])
end
end

def delegate_attribute(attribute)
def delegate_attribute(attribute, alternate_object = nil)
target_object = case alternate_object
when Symbol
send(alternate_object)
when Proc
instance_exec(&alternate_object)
else
object
end

if respond_to?(attribute, true)
send(attribute)
elsif target_object.respond_to?(attribute, true)
target_object.send(attribute)
elsif target_object.respond_to?(:[], true)
target_object.send(:[], attribute)
else
object.send(attribute)
raise ArgumentError, ":attribute was unable to be found anywhere"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be specialized as an AttributeNotFoundException, with the attribute name as a field, then you don't need an English explanation.

end
end

Expand Down