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

Allow dynamic keys via as: proc #265

Merged
merged 7 commits into from
Apr 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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