From 958e23c5af3213ba7812601e7ae9dfc8dd0aa1a5 Mon Sep 17 00:00:00 2001 From: James McCarthy Date: Wed, 5 Apr 2017 10:09:45 +0700 Subject: [PATCH 1/7] Allow as: option to accept a proc. --- lib/grape_entity/exposure/base.rb | 9 +++++++-- lib/grape_entity/exposure/nesting_exposure.rb | 20 +++++++++---------- .../nesting_exposure/nested_exposures.rb | 7 +++++-- .../nesting_exposure/output_builder.rb | 5 +++-- .../exposure/represent_exposure.rb | 2 +- spec/grape_entity/entity_spec.rb | 4 ++-- .../nesting_exposure/nested_exposures_spec.rb | 8 ++++---- 7 files changed, 32 insertions(+), 23 deletions(-) diff --git a/lib/grape_entity/exposure/base.rb b/lib/grape_entity/exposure/base.rb index 94814f20..ed706d62 100644 --- a/lib/grape_entity/exposure/base.rb +++ b/lib/grape_entity/exposure/base.rb @@ -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] @@ -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) false end @@ -104,6 +105,10 @@ def attr_path(entity, options) end end + def key(entity=nil) + @key.respond_to?(:call) ? @key.call(entity).try(:to_sym) : @key + end + def with_attr_path(entity, options) path_part = attr_path(entity, options) options.with_attr_path(path_part) do diff --git a/lib/grape_entity/exposure/nesting_exposure.rb b/lib/grape_entity/exposure/nesting_exposure.rb index 9186626b..997b78ff 100644 --- a/lib/grape_entity/exposure/nesting_exposure.rb +++ b/lib/grape_entity/exposure/nesting_exposure.rb @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -113,7 +113,7 @@ 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?) + new_exposure.instance_variable_set(:@deep_complex_nesting, true) if nesting_tail.any? { |exposure| exposure.deep_complex_nesting?(entity) } end else last_exposure diff --git a/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb b/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb index 0dca7aa6..45d5c26e 100644 --- a/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +++ b/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb @@ -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 diff --git a/lib/grape_entity/exposure/nesting_exposure/output_builder.rb b/lib/grape_entity/exposure/nesting_exposure/output_builder.rb index 86b6b4ab..3e1e3a8e 100644 --- a/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +++ b/lib/grape_entity/exposure/nesting_exposure/output_builder.rb @@ -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 @@ -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 diff --git a/lib/grape_entity/exposure/represent_exposure.rb b/lib/grape_entity/exposure/represent_exposure.rb index 0c4b93f6..b63aae27 100644 --- a/lib/grape_entity/exposure/represent_exposure.rb +++ b/lib/grape_entity/exposure/represent_exposure.rb @@ -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 diff --git a/spec/grape_entity/entity_spec.rb b/spec/grape_entity/entity_spec.rb index 50ac4731..3f0d109c 100644 --- a/spec/grape_entity/entity_spec.rb +++ b/spec/grape_entity/entity_spec.rb @@ -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 @@ -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 diff --git a/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb b/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb index 7d9c2afb..f5e5ff3a 100644 --- a/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +++ b/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb @@ -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 @@ -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 @@ -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 From cd38ec08cce9427c89c8b22d5c6754fe116017ef Mon Sep 17 00:00:00 2001 From: James McCarthy Date: Wed, 5 Apr 2017 11:23:12 +0700 Subject: [PATCH 2/7] Fix the Ruboop errors. --- lib/grape_entity/exposure/base.rb | 4 ++-- lib/grape_entity/exposure/nesting_exposure.rb | 4 +++- .../exposure/nesting_exposure/nested_exposures.rb | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/grape_entity/exposure/base.rb b/lib/grape_entity/exposure/base.rb index ed706d62..99d34f5c 100644 --- a/lib/grape_entity/exposure/base.rb +++ b/lib/grape_entity/exposure/base.rb @@ -44,7 +44,7 @@ def nesting? end # if we have any nesting exposures with the same name. - def deep_complex_nesting?(entity) + def deep_complex_nesting?(entity) # rubocop:disable Lint/UnusedMethodArgument false end @@ -105,7 +105,7 @@ def attr_path(entity, options) end end - def key(entity=nil) + def key(entity = nil) @key.respond_to?(:call) ? @key.call(entity).try(:to_sym) : @key end diff --git a/lib/grape_entity/exposure/nesting_exposure.rb b/lib/grape_entity/exposure/nesting_exposure.rb index 997b78ff..9e76e254 100644 --- a/lib/grape_entity/exposure/nesting_exposure.rb +++ b/lib/grape_entity/exposure/nesting_exposure.rb @@ -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? { |exposure| exposure.deep_complex_nesting?(entity) } + if nesting_tail.any? { |exposure| exposure.deep_complex_nesting?(entity) } + new_exposure.instance_variable_set(:@deep_complex_nesting, true) + end end else last_exposure diff --git a/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb b/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb index 45d5c26e..0059b6f5 100644 --- a/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +++ b/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb @@ -58,8 +58,8 @@ def deep_complex_nesting?(entity) all_nesting = select(&:nesting?) @deep_complex_nesting = all_nesting - .group_by { |exposure| exposure.key(entity) } - .any? { |_key, exposures| exposures.length > 1 } + .group_by { |exposure| exposure.key(entity) } + .any? { |_key, exposures| exposures.length > 1 } else @deep_complex_nesting end From 98f8ba2322124f5abb1684ab7156ba22fccaf6e9 Mon Sep 17 00:00:00 2001 From: James McCarthy Date: Wed, 5 Apr 2017 14:23:58 +0700 Subject: [PATCH 3/7] Remove overridden :key attribute. And test key method. --- lib/grape_entity/exposure/base.rb | 4 ++-- spec/grape_entity/exposure_spec.rb | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/grape_entity/exposure/base.rb b/lib/grape_entity/exposure/base.rb index 99d34f5c..0220d3b0 100644 --- a/lib/grape_entity/exposure/base.rb +++ b/lib/grape_entity/exposure/base.rb @@ -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) } @@ -106,7 +106,7 @@ def attr_path(entity, options) end def key(entity = nil) - @key.respond_to?(:call) ? @key.call(entity).try(:to_sym) : @key + @key.respond_to?(:call) ? @key.call(entity) : @key end def with_attr_path(entity, options) diff --git a/spec/grape_entity/exposure_spec.rb b/spec/grape_entity/exposure_spec.rb index f4696b68..0efad239 100644 --- a/spec/grape_entity/exposure_spec.rb +++ b/spec/grape_entity/exposure_spec.rb @@ -26,12 +26,17 @@ 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: -> (entity) { entity.object.name.reverse } + expect(subject.key(entity)).to eq(model.name.reverse) end end From 9b63f85507c424cdf9415f4b8a141fec1eec85cb Mon Sep 17 00:00:00 2001 From: James McCarthy Date: Wed, 5 Apr 2017 15:05:03 +0700 Subject: [PATCH 4/7] Change as: proc to be run in the context of the entity. --- lib/grape_entity/exposure/base.rb | 2 +- spec/grape_entity/exposure_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/grape_entity/exposure/base.rb b/lib/grape_entity/exposure/base.rb index 0220d3b0..12414bda 100644 --- a/lib/grape_entity/exposure/base.rb +++ b/lib/grape_entity/exposure/base.rb @@ -106,7 +106,7 @@ def attr_path(entity, options) end def key(entity = nil) - @key.respond_to?(:call) ? @key.call(entity) : @key + @key.respond_to?(:call) ? entity.exec_with_object(@options, &@key) : @key end def with_attr_path(entity, options) diff --git a/spec/grape_entity/exposure_spec.rb b/spec/grape_entity/exposure_spec.rb index 0efad239..a3071815 100644 --- a/spec/grape_entity/exposure_spec.rb +++ b/spec/grape_entity/exposure_spec.rb @@ -35,7 +35,7 @@ end it 'returns the result if :as is a proc' do - fresh_class.expose :name, as: -> (entity) { entity.object.name.reverse } + fresh_class.expose :name, as: -> { object.name.reverse } expect(subject.key(entity)).to eq(model.name.reverse) end end From 4c991ea1871db511e6dda677523137858e7efd71 Mon Sep 17 00:00:00 2001 From: James McCarthy Date: Wed, 5 Apr 2017 15:05:22 +0700 Subject: [PATCH 5/7] Add documentation to the as: option. --- lib/grape_entity/entity.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index 17e310ba..2345f0f9 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -127,6 +127,19 @@ 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 + # + # object = OpenStruct(awesomness: 'awesome_key', awesome: 'not-my-key' ) + # + # class MyEntity < Grape::Entity + # expose :awesome, as: -> { object.awesomness } + # end + # + # => { 'awesome_key': 'not-my-key' } + # # @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 From dcba3a2735192f06bf88886731debac2aea6d203 Mon Sep 17 00:00:00 2001 From: James McCarthy Date: Wed, 5 Apr 2017 15:57:11 +0700 Subject: [PATCH 6/7] Update spec and docs for lambda and proc syntax. --- lib/grape_entity/entity.rb | 11 +++++++---- spec/grape_entity/exposure_spec.rb | 7 ++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index 2345f0f9..9fe12dce 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -130,15 +130,18 @@ def self.inherited(subclass) # 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 + # @example as: a proc or lambda # - # object = OpenStruct(awesomness: 'awesome_key', awesome: 'not-my-key' ) + # object = OpenStruct(awesomness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' ) # # class MyEntity < Grape::Entity - # expose :awesome, as: -> { object.awesomness } + # expose :awesome, as: proc { object.awesomeness } + # expose :awesomeness, as: ->(object, opts) { object.other } # end # - # => { 'awesome_key': 'not-my-key' } + # => { '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 diff --git a/spec/grape_entity/exposure_spec.rb b/spec/grape_entity/exposure_spec.rb index a3071815..b8d6b252 100644 --- a/spec/grape_entity/exposure_spec.rb +++ b/spec/grape_entity/exposure_spec.rb @@ -35,7 +35,12 @@ end it 'returns the result if :as is a proc' do - fresh_class.expose :name, as: -> { object.name.reverse } + 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 From 68baab0b9c8ca76b6ee899d5ec095053c2a47451 Mon Sep 17 00:00:00 2001 From: James McCarthy Date: Wed, 5 Apr 2017 16:38:12 +0700 Subject: [PATCH 7/7] Add CHANGELOG entry. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a64428e1..cc28327c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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).