Skip to content

Commit ba229f8

Browse files
james2mLeFnord
authored andcommitted
Allow dynamic keys via as: proc (#265)
* 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.
1 parent 0930f60 commit ba229f8

File tree

10 files changed

+64
-26
lines changed

10 files changed

+64
-26
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#### Features
44

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

lib/grape_entity/entity.rb

+16
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ def self.inherited(subclass)
127127
# should be exposed by the entity.
128128
#
129129
# @option options :as Declare an alias for the representation of this attribute.
130+
# If a proc is presented it is evaluated in the context of the entity so object
131+
# and the entity methods are available to it.
132+
#
133+
# @example as: a proc or lambda
134+
#
135+
# object = OpenStruct(awesomness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' )
136+
#
137+
# class MyEntity < Grape::Entity
138+
# expose :awesome, as: proc { object.awesomeness }
139+
# expose :awesomeness, as: ->(object, opts) { object.other }
140+
# end
141+
#
142+
# => { 'awesome_key': 'not-my-key', 'other-key': 'awesome_key' }
143+
#
144+
# Note the parameters passed in via the lambda syntax.
145+
#
130146
# @option options :if When passed a Hash, the attribute will only be exposed if the
131147
# runtime options match all the conditions passed in. When passed a lambda, the
132148
# lambda will execute with two arguments: the object being represented and the

lib/grape_entity/exposure/base.rb

+8-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Grape
44
class Entity
55
module Exposure
66
class Base
7-
attr_reader :attribute, :key, :is_safe, :documentation, :conditions, :for_merge
7+
attr_reader :attribute, :is_safe, :documentation, :conditions, :for_merge
88

99
def self.new(attribute, options, conditions, *args, &block)
1010
super(attribute, options, conditions).tap { |e| e.setup(*args, &block) }
@@ -13,7 +13,8 @@ def self.new(attribute, options, conditions, *args, &block)
1313
def initialize(attribute, options, conditions)
1414
@attribute = attribute.try(:to_sym)
1515
@options = options
16-
@key = (options[:as] || attribute).try(:to_sym)
16+
key = options[:as] || attribute
17+
@key = key.respond_to?(:to_sym) ? key.to_sym : key
1718
@is_safe = options[:safe]
1819
@for_merge = options[:merge]
1920
@attr_path_proc = options[:attr_path]
@@ -43,7 +44,7 @@ def nesting?
4344
end
4445

4546
# if we have any nesting exposures with the same name.
46-
def deep_complex_nesting?
47+
def deep_complex_nesting?(entity) # rubocop:disable Lint/UnusedMethodArgument
4748
false
4849
end
4950

@@ -104,6 +105,10 @@ def attr_path(entity, options)
104105
end
105106
end
106107

108+
def key(entity = nil)
109+
@key.respond_to?(:call) ? entity.exec_with_object(@options, &@key) : @key
110+
end
111+
107112
def with_attr_path(entity, options)
108113
path_part = attr_path(entity, options)
109114
options.with_attr_path(path_part) do

lib/grape_entity/exposure/nesting_exposure.rb

+12-10
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def valid?(entity)
3232

3333
def value(entity, options)
3434
new_options = nesting_options_for(options)
35-
output = OutputBuilder.new
35+
output = OutputBuilder.new(entity)
3636

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

4848
result = nil
49-
normalized_exposures(entity, new_options).select { |e| e.key == key }.each do |exposure|
49+
normalized_exposures(entity, new_options).select { |e| e.key(entity) == key }.each do |exposure|
5050
exposure.with_attr_path(entity, new_options) do
5151
result = exposure.valid_value(entity, new_options)
5252
end
@@ -56,7 +56,7 @@ def valid_value_for(key, entity, options)
5656

5757
def serializable_value(entity, options)
5858
new_options = nesting_options_for(options)
59-
output = OutputBuilder.new
59+
output = OutputBuilder.new(entity)
6060

6161
normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
6262
exposure.with_attr_path(entity, new_options) do
@@ -67,9 +67,9 @@ def serializable_value(entity, options)
6767
end
6868

6969
# if we have any nesting exposures with the same name.
70-
# delegate :deep_complex_nesting?, to: :nested_exposures
71-
def deep_complex_nesting?
72-
nested_exposures.deep_complex_nesting?
70+
# delegate :deep_complex_nesting?(entity), to: :nested_exposures
71+
def deep_complex_nesting?(entity)
72+
nested_exposures.deep_complex_nesting?(entity)
7373
end
7474

7575
private
@@ -92,15 +92,15 @@ def easy_normalized_exposures(entity, options)
9292

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

9797
table = nested_exposures.each_with_object({}) do |exposure, output|
9898
should_expose = exposure.with_attr_path(entity, options) do
9999
exposure.should_expose?(entity, options)
100100
end
101101
next unless should_expose
102-
output[exposure.key] ||= []
103-
output[exposure.key] << exposure
102+
output[exposure.key(entity)] ||= []
103+
output[exposure.key(entity)] << exposure
104104
end
105105
table.map do |key, exposures|
106106
last_exposure = exposures.last
@@ -113,7 +113,9 @@ def normalized_exposures(entity, options)
113113
end
114114
new_nested_exposures = nesting_tail.flat_map(&:nested_exposures)
115115
NestingExposure.new(key, {}, [], new_nested_exposures).tap do |new_exposure|
116-
new_exposure.instance_variable_set(:@deep_complex_nesting, true) if nesting_tail.any?(&:deep_complex_nesting?)
116+
if nesting_tail.any? { |exposure| exposure.deep_complex_nesting?(entity) }
117+
new_exposure.instance_variable_set(:@deep_complex_nesting, true)
118+
end
117119
end
118120
else
119121
last_exposure

lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,13 @@ def #{name}(*args, &block)
5353
end
5454

5555
# Determine if we have any nesting exposures with the same name.
56-
def deep_complex_nesting?
56+
def deep_complex_nesting?(entity)
5757
if @deep_complex_nesting.nil?
5858
all_nesting = select(&:nesting?)
59-
@deep_complex_nesting = all_nesting.group_by(&:key).any? { |_key, exposures| exposures.length > 1 }
59+
@deep_complex_nesting =
60+
all_nesting
61+
.group_by { |exposure| exposure.key(entity) }
62+
.any? { |_key, exposures| exposures.length > 1 }
6063
else
6164
@deep_complex_nesting
6265
end

lib/grape_entity/exposure/nesting_exposure/output_builder.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ class Entity
55
module Exposure
66
class NestingExposure
77
class OutputBuilder < SimpleDelegator
8-
def initialize
8+
def initialize(entity)
9+
@entity = entity
910
@output_hash = {}
1011
@output_collection = []
1112
end
@@ -20,7 +21,7 @@ def add(exposure, result)
2021
return unless result
2122
@output_hash.merge! result, &merge_strategy(exposure.for_merge)
2223
else
23-
@output_hash[exposure.key] = result
24+
@output_hash[exposure.key(@entity)] = result
2425
end
2526
end
2627

lib/grape_entity/exposure/represent_exposure.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def ==(other)
2323
end
2424

2525
def value(entity, options)
26-
new_options = options.for_nesting(key)
26+
new_options = options.for_nesting(key(entity))
2727
using_class.represent(@subexposure.value(entity, options), new_options)
2828
end
2929

spec/grape_entity/entity_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class BogusEntity < Grape::Entity
133133
expect(another_nested).to_not be_nil
134134
expect(another_nested.using_class_name).to eq('Awesome')
135135
expect(moar_nested).to_not be_nil
136-
expect(moar_nested.key).to eq(:weee)
136+
expect(moar_nested.key(subject)).to eq(:weee)
137137
end
138138

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

500500
exposure = subject.find_exposure(:awesome_thing)
501-
expect(exposure.key).to eq :extra_smooth
501+
expect(exposure.key(subject)).to eq :extra_smooth
502502
end
503503

504504
it 'merges nested :if option' do

spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
describe Grape::Entity::Exposure::NestingExposure::NestedExposures do
66
subject(:nested_exposures) { described_class.new([]) }
77

8-
describe '#deep_complex_nesting?' do
8+
describe '#deep_complex_nesting?(entity)' do
99
it 'is reset when additional exposure is added' do
1010
subject << Grape::Entity::Exposure.new(:x, {})
1111
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
12-
subject.deep_complex_nesting?
12+
subject.deep_complex_nesting?(subject)
1313
expect(subject.instance_variable_get(:@deep_complex_nesting)).to_not be_nil
1414
subject << Grape::Entity::Exposure.new(:y, {})
1515
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
@@ -18,7 +18,7 @@
1818
it 'is reset when exposure is deleted' do
1919
subject << Grape::Entity::Exposure.new(:x, {})
2020
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
21-
subject.deep_complex_nesting?
21+
subject.deep_complex_nesting?(subject)
2222
expect(subject.instance_variable_get(:@deep_complex_nesting)).to_not be_nil
2323
subject.delete_by(:x)
2424
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
@@ -27,7 +27,7 @@
2727
it 'is reset when exposures are cleared' do
2828
subject << Grape::Entity::Exposure.new(:x, {})
2929
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil
30-
subject.deep_complex_nesting?
30+
subject.deep_complex_nesting?(subject)
3131
expect(subject.instance_variable_get(:@deep_complex_nesting)).to_not be_nil
3232
subject.clear
3333
expect(subject.instance_variable_get(:@deep_complex_nesting)).to be_nil

spec/grape_entity/exposure_spec.rb

+12-2
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,22 @@
2626
describe '#key' do
2727
it 'returns the attribute if no :as is set' do
2828
fresh_class.expose :name
29-
expect(subject.key).to eq :name
29+
expect(subject.key(entity)).to eq :name
3030
end
3131

3232
it 'returns the :as alias if one exists' do
3333
fresh_class.expose :name, as: :nombre
34-
expect(subject.key).to eq :nombre
34+
expect(subject.key(entity)).to eq :nombre
35+
end
36+
37+
it 'returns the result if :as is a proc' do
38+
fresh_class.expose :name, as: proc { object.name.reverse }
39+
expect(subject.key(entity)).to eq(model.name.reverse)
40+
end
41+
42+
it 'returns the result if :as is a lambda' do
43+
fresh_class.expose :name, as: ->(obj, _opts) { obj.name.reverse }
44+
expect(subject.key(entity)).to eq(model.name.reverse)
3545
end
3646
end
3747

0 commit comments

Comments
 (0)