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). diff --git a/lib/grape_entity/entity.rb b/lib/grape_entity/entity.rb index 17e310ba..9fe12dce 100644 --- a/lib/grape_entity/entity.rb +++ b/lib/grape_entity/entity.rb @@ -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 diff --git a/lib/grape_entity/exposure/base.rb b/lib/grape_entity/exposure/base.rb index 94814f20..12414bda 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) } @@ -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) # rubocop:disable Lint/UnusedMethodArgument false end @@ -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 diff --git a/lib/grape_entity/exposure/nesting_exposure.rb b/lib/grape_entity/exposure/nesting_exposure.rb index 9186626b..9e76e254 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,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 diff --git a/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb b/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb index 0dca7aa6..0059b6f5 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 diff --git a/spec/grape_entity/exposure_spec.rb b/spec/grape_entity/exposure_spec.rb index f4696b68..b8d6b252 100644 --- a/spec/grape_entity/exposure_spec.rb +++ b/spec/grape_entity/exposure_spec.rb @@ -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