Skip to content

Commit

Permalink
Allow dynamic keys via as: proc (#265)
Browse files Browse the repository at this point in the history
* Allow as: option to accept a proc.

* Fix the Ruboop errors.

* Remove overridden :key attribute. And test key method.

* Change as: proc to be run in the context of the entity.

* Add documentation to the as: option.

* Update spec and docs for lambda and proc syntax.

* Add CHANGELOG entry.
  • Loading branch information
james2m authored and LeFnord committed Apr 5, 2017
1 parent 0930f60 commit ba229f8
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 26 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#### Features

* [#265](https://github.com/ruby-grape/grape-entity/pull/265): Adds ability to provide a proc to as: - [@james2m](https://github.com/james2m).
* [#264](https://github.com/ruby-grape/grape-entity/pull/264): Adds Rubocop config and todo list - [@james2m](https://github.com/james2m).
* [#255](https://github.com/ruby-grape/grape-entity/pull/255): Adds code coverage w/ coveralls - [@LeFnord](https://github.com/LeFnord).

Expand Down
16 changes: 16 additions & 0 deletions lib/grape_entity/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ def self.inherited(subclass)
# should be exposed by the entity.
#
# @option options :as Declare an alias for the representation of this attribute.
# If a proc is presented it is evaluated in the context of the entity so object
# and the entity methods are available to it.
#
# @example as: a proc or lambda
#
# object = OpenStruct(awesomness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' )
#
# class MyEntity < Grape::Entity
# expose :awesome, as: proc { object.awesomeness }
# expose :awesomeness, as: ->(object, opts) { object.other }
# end
#
# => { 'awesome_key': 'not-my-key', 'other-key': 'awesome_key' }
#
# Note the parameters passed in via the lambda syntax.
#
# @option options :if When passed a Hash, the attribute will only be exposed if the
# runtime options match all the conditions passed in. When passed a lambda, the
# lambda will execute with two arguments: the object being represented and the
Expand Down
11 changes: 8 additions & 3 deletions lib/grape_entity/exposure/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Grape
class Entity
module Exposure
class Base
attr_reader :attribute, :key, :is_safe, :documentation, :conditions, :for_merge
attr_reader :attribute, :is_safe, :documentation, :conditions, :for_merge

def self.new(attribute, options, conditions, *args, &block)
super(attribute, options, conditions).tap { |e| e.setup(*args, &block) }
Expand All @@ -13,7 +13,8 @@ def self.new(attribute, options, conditions, *args, &block)
def initialize(attribute, options, conditions)
@attribute = attribute.try(:to_sym)
@options = options
@key = (options[:as] || attribute).try(:to_sym)
key = options[:as] || attribute
@key = key.respond_to?(:to_sym) ? key.to_sym : key
@is_safe = options[:safe]
@for_merge = options[:merge]
@attr_path_proc = options[:attr_path]
Expand Down Expand Up @@ -43,7 +44,7 @@ def nesting?
end

# if we have any nesting exposures with the same name.
def deep_complex_nesting?
def deep_complex_nesting?(entity) # rubocop:disable Lint/UnusedMethodArgument
false
end

Expand Down Expand Up @@ -104,6 +105,10 @@ def attr_path(entity, options)
end
end

def key(entity = nil)
@key.respond_to?(:call) ? entity.exec_with_object(@options, &@key) : @key
end

def with_attr_path(entity, options)
path_part = attr_path(entity, options)
options.with_attr_path(path_part) do
Expand Down
22 changes: 12 additions & 10 deletions lib/grape_entity/exposure/nesting_exposure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def valid?(entity)

def value(entity, options)
new_options = nesting_options_for(options)
output = OutputBuilder.new
output = OutputBuilder.new(entity)

normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
exposure.with_attr_path(entity, new_options) do
Expand All @@ -46,7 +46,7 @@ def valid_value_for(key, entity, options)
new_options = nesting_options_for(options)

result = nil
normalized_exposures(entity, new_options).select { |e| e.key == key }.each do |exposure|
normalized_exposures(entity, new_options).select { |e| e.key(entity) == key }.each do |exposure|
exposure.with_attr_path(entity, new_options) do
result = exposure.valid_value(entity, new_options)
end
Expand All @@ -56,7 +56,7 @@ def valid_value_for(key, entity, options)

def serializable_value(entity, options)
new_options = nesting_options_for(options)
output = OutputBuilder.new
output = OutputBuilder.new(entity)

normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
exposure.with_attr_path(entity, new_options) do
Expand All @@ -67,9 +67,9 @@ def serializable_value(entity, options)
end

# if we have any nesting exposures with the same name.
# delegate :deep_complex_nesting?, to: :nested_exposures
def deep_complex_nesting?
nested_exposures.deep_complex_nesting?
# delegate :deep_complex_nesting?(entity), to: :nested_exposures
def deep_complex_nesting?(entity)
nested_exposures.deep_complex_nesting?(entity)
end

private
Expand All @@ -92,15 +92,15 @@ def easy_normalized_exposures(entity, options)

# This method 'merges' subsequent nesting exposures with the same name if it's needed
def normalized_exposures(entity, options)
return easy_normalized_exposures(entity, options) unless deep_complex_nesting? # optimization
return easy_normalized_exposures(entity, options) unless deep_complex_nesting?(entity) # optimization

table = nested_exposures.each_with_object({}) do |exposure, output|
should_expose = exposure.with_attr_path(entity, options) do
exposure.should_expose?(entity, options)
end
next unless should_expose
output[exposure.key] ||= []
output[exposure.key] << exposure
output[exposure.key(entity)] ||= []
output[exposure.key(entity)] << exposure
end
table.map do |key, exposures|
last_exposure = exposures.last
Expand All @@ -113,7 +113,9 @@ def normalized_exposures(entity, options)
end
new_nested_exposures = nesting_tail.flat_map(&:nested_exposures)
NestingExposure.new(key, {}, [], new_nested_exposures).tap do |new_exposure|
new_exposure.instance_variable_set(:@deep_complex_nesting, true) if nesting_tail.any?(&:deep_complex_nesting?)
if nesting_tail.any? { |exposure| exposure.deep_complex_nesting?(entity) }
new_exposure.instance_variable_set(:@deep_complex_nesting, true)
end
end
else
last_exposure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,13 @@ def #{name}(*args, &block)
end

# Determine if we have any nesting exposures with the same name.
def deep_complex_nesting?
def deep_complex_nesting?(entity)
if @deep_complex_nesting.nil?
all_nesting = select(&:nesting?)
@deep_complex_nesting = all_nesting.group_by(&:key).any? { |_key, exposures| exposures.length > 1 }
@deep_complex_nesting =
all_nesting
.group_by { |exposure| exposure.key(entity) }
.any? { |_key, exposures| exposures.length > 1 }
else
@deep_complex_nesting
end
Expand Down
5 changes: 3 additions & 2 deletions lib/grape_entity/exposure/nesting_exposure/output_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ class Entity
module Exposure
class NestingExposure
class OutputBuilder < SimpleDelegator
def initialize
def initialize(entity)
@entity = entity
@output_hash = {}
@output_collection = []
end
Expand All @@ -20,7 +21,7 @@ def add(exposure, result)
return unless result
@output_hash.merge! result, &merge_strategy(exposure.for_merge)
else
@output_hash[exposure.key] = result
@output_hash[exposure.key(@entity)] = result
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/grape_entity/exposure/represent_exposure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def ==(other)
end

def value(entity, options)
new_options = options.for_nesting(key)
new_options = options.for_nesting(key(entity))
using_class.represent(@subexposure.value(entity, options), new_options)
end

Expand Down
4 changes: 2 additions & 2 deletions spec/grape_entity/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class BogusEntity < Grape::Entity
expect(another_nested).to_not be_nil
expect(another_nested.using_class_name).to eq('Awesome')
expect(moar_nested).to_not be_nil
expect(moar_nested.key).to eq(:weee)
expect(moar_nested.key(subject)).to eq(:weee)
end

it 'represents the exposure as a hash of its nested.root_exposures' do
Expand Down Expand Up @@ -498,7 +498,7 @@ class Parent < Person
end

exposure = subject.find_exposure(:awesome_thing)
expect(exposure.key).to eq :extra_smooth
expect(exposure.key(subject)).to eq :extra_smooth
end

it 'merges nested :if option' do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
describe Grape::Entity::Exposure::NestingExposure::NestedExposures do
subject(:nested_exposures) { described_class.new([]) }

describe '#deep_complex_nesting?' do
describe '#deep_complex_nesting?(entity)' do
it 'is reset when additional exposure is added' do
subject << Grape::Entity::Exposure.new(:x, {})
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
subject.deep_complex_nesting?
subject.deep_complex_nesting?(subject)
expect(subject.instance_variable_get(:@deep_complex_nesting)).to_not be_nil
subject << Grape::Entity::Exposure.new(:y, {})
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
Expand All @@ -18,7 +18,7 @@
it 'is reset when exposure is deleted' do
subject << Grape::Entity::Exposure.new(:x, {})
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
subject.deep_complex_nesting?
subject.deep_complex_nesting?(subject)
expect(subject.instance_variable_get(:@deep_complex_nesting)).to_not be_nil
subject.delete_by(:x)
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
Expand All @@ -27,7 +27,7 @@
it 'is reset when exposures are cleared' do
subject << Grape::Entity::Exposure.new(:x, {})
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
subject.deep_complex_nesting?
subject.deep_complex_nesting?(subject)
expect(subject.instance_variable_get(:@deep_complex_nesting)).to_not be_nil
subject.clear
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
Expand Down
14 changes: 12 additions & 2 deletions spec/grape_entity/exposure_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,22 @@
describe '#key' do
it 'returns the attribute if no :as is set' do
fresh_class.expose :name
expect(subject.key).to eq :name
expect(subject.key(entity)).to eq :name
end

it 'returns the :as alias if one exists' do
fresh_class.expose :name, as: :nombre
expect(subject.key).to eq :nombre
expect(subject.key(entity)).to eq :nombre
end

it 'returns the result if :as is a proc' do
fresh_class.expose :name, as: proc { object.name.reverse }
expect(subject.key(entity)).to eq(model.name.reverse)
end

it 'returns the result if :as is a lambda' do
fresh_class.expose :name, as: ->(obj, _opts) { obj.name.reverse }
expect(subject.key(entity)).to eq(model.name.reverse)
end
end

Expand Down

0 comments on commit ba229f8

Please sign in to comment.