Skip to content

Commit a4aa641

Browse files
committed
Merge pull request #147 from marshall-lee/delegators
Refactoring: extract delegating logic.
2 parents 78b141b + 3274bb4 commit a4aa641

File tree

10 files changed

+142
-34
lines changed

10 files changed

+142
-34
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
* [#134](https://github.com/intridea/grape-entity/pull/134): Subclasses no longer affected in all cases by `unexpose` in parent - [@etehtsea](https://github.com/etehtsea).
88
* [#135](https://github.com/intridea/grape-entity/pull/135): Added `except` option - [@dan-corneanu](https://github.com/dan-corneanu).
99
* [#136](https://github.com/intridea/grape-entity/pull/136): Allow for strings in `only` and `except` options - [@bswinnerton](https://github.com/bswinnerton).
10+
* [#147](https://github.com/intridea/grape-entity/pull/147): Expose `safe` attributes as `nil` if they cannot be evaluated: [#140](https://github.com/intridea/grape-entity/issues/140).
11+
* [#147](https://github.com/intridea/grape-entity/pull/147): Fix: private method values were not exposed with `safe` option: [#142](https://github.com/intridea/grape-entity/pull/142).
12+
* [#147](https://github.com/intridea/grape-entity/pull/147): Remove catching of `NoMethodError` because it can occur deep inside in a method call so this exception does not mean that attribute not exist.
13+
* [#147](https://github.com/intridea/grape-entity/pull/147): `valid_exposures` is removed.
1014
* Your contribution here.
1115

1216
0.4.5 (2015-03-10)

lib/grape_entity.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
require 'active_support/core_ext'
33
require 'grape_entity/version'
44
require 'grape_entity/entity'
5+
require 'grape_entity/delegator'

lib/grape_entity/delegator.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
require 'grape_entity/delegator/base'
2+
require 'grape_entity/delegator/hash_object'
3+
require 'grape_entity/delegator/openstruct_object'
4+
require 'grape_entity/delegator/fetchable_object'
5+
require 'grape_entity/delegator/plain_object'
6+
7+
module Grape
8+
class Entity
9+
module Delegator
10+
def self.new(object)
11+
if object.is_a?(Hash)
12+
HashObject.new object
13+
elsif defined?(OpenStruct) && object.is_a?(OpenStruct)
14+
OpenStructObject.new object
15+
elsif object.respond_to? :fetch, true
16+
FetchableObject.new object
17+
else
18+
PlainObject.new object
19+
end
20+
end
21+
end
22+
end
23+
end

lib/grape_entity/delegator/base.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module Grape
2+
class Entity
3+
module Delegator
4+
class Base
5+
attr_reader :object
6+
7+
def initialize(object)
8+
@object = object
9+
end
10+
11+
def delegatable?(_attribute)
12+
true
13+
end
14+
end
15+
end
16+
end
17+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module Grape
2+
class Entity
3+
module Delegator
4+
class FetchableObject < Base
5+
def delegate(attribute)
6+
object.fetch attribute
7+
end
8+
end
9+
end
10+
end
11+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module Grape
2+
class Entity
3+
module Delegator
4+
class HashObject < Base
5+
def delegate(attribute)
6+
object[attribute]
7+
end
8+
end
9+
end
10+
end
11+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module Grape
2+
class Entity
3+
module Delegator
4+
class OpenStructObject < Base
5+
def delegate(attribute)
6+
object.send attribute
7+
end
8+
end
9+
end
10+
end
11+
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module Grape
2+
class Entity
3+
module Delegator
4+
class PlainObject < Base
5+
def delegate(attribute)
6+
object.send attribute
7+
end
8+
9+
def delegatable?(attribute)
10+
object.respond_to? attribute, true
11+
end
12+
end
13+
end
14+
end
15+
end

lib/grape_entity/entity.rb

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ module Grape
4242
# end
4343
# end
4444
class Entity
45-
attr_reader :object, :options
45+
attr_reader :object, :delegator, :options
4646

4747
# The Entity DSL allows you to mix entity functionality into
4848
# your existing classes.
@@ -104,6 +104,7 @@ class << self
104104
# containing object, the values are the options that were passed into expose.
105105
# @return [Hash] of exposures
106106
attr_accessor :exposures
107+
attr_accessor :root_exposures
107108
# Returns all formatters that are registered for this and it's ancestors
108109
# @return [Hash] of formatters
109110
attr_accessor :formatters
@@ -113,6 +114,7 @@ class << self
113114

114115
def self.inherited(subclass)
115116
subclass.exposures = exposures.try(:dup) || {}
117+
subclass.root_exposures = root_exposures.try(:dup) || {}
116118
subclass.nested_exposures = nested_exposures.try(:dup) || {}
117119
subclass.nested_attribute_names = nested_attribute_names.try(:dup) || {}
118120
subclass.formatters = formatters.try(:dup) || {}
@@ -159,7 +161,9 @@ def self.expose(*args, &block)
159161

160162
# rubocop:disable Style/Next
161163
args.each do |attribute|
162-
unless @nested_attributes.empty?
164+
if @nested_attributes.empty?
165+
root_exposures[attribute] = options
166+
else
163167
orig_attribute = attribute.to_sym
164168
attribute = "#{@nested_attributes.last}__#{attribute}".to_sym
165169
nested_attribute_names[attribute] = orig_attribute
@@ -391,17 +395,16 @@ def presented
391395

392396
def initialize(object, options = {})
393397
@object = object
398+
@delegator = Delegator.new object
394399
@options = options
395400
end
396401

397402
def exposures
398403
self.class.exposures
399404
end
400405

401-
def valid_exposures
402-
exposures.select do |attribute, exposure_options|
403-
!exposure_options[:nested] && valid_exposure?(attribute, exposure_options)
404-
end
406+
def root_exposures
407+
self.class.root_exposures
405408
end
406409

407410
def documentation
@@ -424,7 +427,7 @@ def serializable_hash(runtime_options = {})
424427

425428
opts = options.merge(runtime_options || {})
426429

427-
valid_exposures.each_with_object({}) do |(attribute, exposure_options), output|
430+
root_exposures.each_with_object({}) do |(attribute, exposure_options), output|
428431
next unless should_return_attribute?(attribute, opts) && conditions_met?(exposure_options, opts)
429432

430433
partial_output = value_for(attribute, opts)
@@ -536,6 +539,7 @@ def nested_value_for(attribute, options)
536539

537540
def value_for(attribute, options = {})
538541
exposure_options = exposures[attribute.to_sym]
542+
return unless valid_exposure?(attribute, exposure_options)
539543

540544
if exposure_options[:using]
541545
exposure_options[:using] = exposure_options[:using].constantize if exposure_options[:using].respond_to? :constantize
@@ -573,27 +577,24 @@ def delegate_attribute(attribute)
573577
name = self.class.name_for(attribute)
574578
if respond_to?(name, true)
575579
send(name)
576-
elsif object.is_a?(Hash)
577-
object[name]
578-
elsif object.respond_to?(name, true)
579-
object.send(name)
580-
elsif object.respond_to?(:fetch, true)
581-
object.fetch(name)
582580
else
583-
begin
584-
object.send(name)
585-
rescue NoMethodError
586-
raise NoMethodError, "#{self.class.name} missing attribute `#{name}' on #{object}"
587-
end
581+
delegator.delegate(name)
588582
end
589583
end
590584

591585
def valid_exposure?(attribute, exposure_options)
592-
(self.class.nested_exposures_for?(attribute) && self.class.nested_exposures[attribute].all? { |a, o| valid_exposure?(a, o) }) || \
593-
exposure_options.key?(:proc) || \
594-
!exposure_options[:safe] || \
595-
object.respond_to?(self.class.name_for(attribute)) || \
596-
object.is_a?(Hash) && object.key?(self.class.name_for(attribute))
586+
if self.class.nested_exposures_for?(attribute)
587+
self.class.nested_exposures[attribute].all? { |a, o| valid_exposure?(a, o) }
588+
elsif exposure_options.key?(:proc)
589+
true
590+
else
591+
name = self.class.name_for(attribute)
592+
if exposure_options[:safe]
593+
delegator.delegatable?(name)
594+
else
595+
delegator.delegatable?(name) || fail(NoMethodError, "#{self.class.name} missing attribute `#{name}' on #{object}")
596+
end
597+
end
597598
end
598599

599600
def conditions_met?(exposure_options, options)

spec/grape_entity/entity_spec.rb

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,14 @@ class Parent < Person
190190
subject.expose :nested
191191
end
192192
end
193-
194-
valid_keys = subject.represent({}).valid_exposures.keys
195-
196-
expect(valid_keys.include?(:awesome)).to be true
197-
expect(valid_keys.include?(:not_awesome)).to be false
193+
expect(subject.represent({}, serializable: true)).to eq(
194+
awesome: {
195+
nested: 'value'
196+
},
197+
not_awesome: {
198+
nested: nil
199+
}
200+
)
198201
end
199202
end
200203
end
@@ -848,12 +851,23 @@ class Parent < Person
848851
expect { fresh_class.new(model).serializable_hash }.not_to raise_error
849852
end
850853

851-
it "does not expose attributes that don't exist on the object" do
854+
it 'exposes values of private method calls' do
855+
some_class = Class.new do
856+
define_method :name do
857+
true
858+
end
859+
private :name
860+
end
861+
fresh_class.expose :name, safe: true
862+
expect(fresh_class.new(some_class.new).serializable_hash).to eq(name: true)
863+
end
864+
865+
it "does expose attributes that don't exist on the object as nil" do
852866
fresh_class.expose :email, :nonexistent_attribute, :name, safe: true
853867

854868
res = fresh_class.new(model).serializable_hash
855869
expect(res).to have_key :email
856-
expect(res).not_to have_key :nonexistent_attribute
870+
expect(res).to have_key :nonexistent_attribute
857871
expect(res).to have_key :name
858872
end
859873

@@ -864,15 +878,15 @@ class Parent < Person
864878
expect(res).to have_key :name
865879
end
866880

867-
it "does not expose attributes that don't exist on the object, even with criteria" do
881+
it "does expose attributes that don't exist on the object as nil if criteria is true" do
868882
fresh_class.expose :email
869-
fresh_class.expose :nonexistent_attribute, safe: true, if: -> { false }
870-
fresh_class.expose :nonexistent_attribute2, safe: true, if: -> { true }
883+
fresh_class.expose :nonexistent_attribute, safe: true, if: ->(_obj, _opts) { false }
884+
fresh_class.expose :nonexistent_attribute2, safe: true, if: ->(_obj, _opts) { true }
871885

872886
res = fresh_class.new(model).serializable_hash
873887
expect(res).to have_key :email
874888
expect(res).not_to have_key :nonexistent_attribute
875-
expect(res).not_to have_key :nonexistent_attribute2
889+
expect(res).to have_key :nonexistent_attribute2
876890
end
877891
end
878892

0 commit comments

Comments
 (0)