Skip to content

Commit 18a4b01

Browse files
authored
Respect hash_access setting when using expose_nil: false option (#359)
* Fix Lint/ElseLayout Rubocop warning * Document Entity.hash_access= * Move Delegator option acceptance check to Delegator::Base * Pass hash_access to Delegator when it's both present and accepted * Update CHANGELOG
1 parent 061975f commit 18a4b01

File tree

7 files changed

+50
-10
lines changed

7 files changed

+50
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#### Fixes
88

9+
* [#359](https://github.com/ruby-grape/grape-entity/pull/359): Respect `hash_access` setting when using `expose_nil: false` option - [@magni-](https://github.com/magni-).
910
* Your contribution here.
1011

1112

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ The field lookup takes several steps
111111
* next try `object.fetch(exposure)`
112112
* last raise an Exception
113113

114+
`exposure` is a Symbol by default. If `object` is a Hash with stringified keys, you can set the hash accessor at the entity-class level to properly expose its members:
115+
116+
```ruby
117+
class Status < GrapeEntity
118+
self.hash_access = :to_s
119+
120+
expose :code
121+
expose :message
122+
end
123+
124+
Status.represent({ 'code' => 418, 'message' => "I'm a teapot" }).as_json
125+
#=> { code: 418, message: "I'm a teapot" }
126+
```
127+
114128
#### Exposing with a Presenter
115129

116130
Don't derive your model classes from `Grape::Entity`, expose them using a presenter.

lib/grape_entity/delegator.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ class Entity
1111
module Delegator
1212
def self.new(object)
1313
delegator_klass =
14-
if object.is_a?(Hash) then HashObject
15-
elsif defined?(OpenStruct) && object.is_a?(OpenStruct) then OpenStructObject
16-
elsif object.respond_to?(:fetch, true) then FetchableObject
17-
else PlainObject
14+
if object.is_a?(Hash)
15+
HashObject
16+
elsif defined?(OpenStruct) && object.is_a?(OpenStruct)
17+
OpenStructObject
18+
elsif object.respond_to?(:fetch, true)
19+
FetchableObject
20+
else
21+
PlainObject
1822
end
1923

2024
delegator_klass.new(object)

lib/grape_entity/delegator/base.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ def initialize(object)
1313
def delegatable?(_attribute)
1414
true
1515
end
16+
17+
def accepts_options?
18+
# Why not `arity > 1`? It might be negative https://ruby-doc.org/core-2.6.6/Method.html#method-i-arity
19+
method(:delegate).arity != 1
20+
end
1621
end
1722
end
1823
end

lib/grape_entity/entity.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -481,9 +481,6 @@ def initialize(object, options = {})
481481
@object = object
482482
@options = options.is_a?(Options) ? options : Options.new(options)
483483
@delegator = Delegator.new(object)
484-
485-
# Why not `arity > 1`? It might be negative https://ruby-doc.org/core-2.6.6/Method.html#method-i-arity
486-
@delegator_accepts_opts = @delegator.method(:delegate).arity != 1
487484
end
488485

489486
def root_exposures
@@ -541,7 +538,7 @@ def value_for(key, options = Options.new)
541538
def delegate_attribute(attribute)
542539
if is_defined_in_entity?(attribute)
543540
send(attribute)
544-
elsif @delegator_accepts_opts
541+
elsif delegator.accepts_options?
545542
delegator.delegate(attribute, **self.class.delegation_opts)
546543
else
547544
delegator.delegate(attribute)

lib/grape_entity/exposure.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ def expose_nil_condition(attribute, options)
5656
Condition.new_unless(
5757
proc do |object, _options|
5858
if options[:proc].nil?
59-
Delegator.new(object).delegate(attribute).nil?
59+
delegator = Delegator.new(object)
60+
if is_a?(Grape::Entity) && delegator.accepts_options?
61+
delegator.delegate(attribute, **self.class.delegation_opts).nil?
62+
else
63+
delegator.delegate(attribute).nil?
64+
end
6065
else
6166
exec_with_object(options, &options[:proc]).nil?
6267
end

spec/grape_entity/hash_spec.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class AddressWithString < Grape::Entity
1717
expose :post, if: :full
1818
expose :city
1919
expose :street
20-
expose :house
20+
expose :house, expose_nil: false
2121
end
2222

2323
class Company < Grape::Entity
@@ -62,9 +62,23 @@ class CompanyWithString < Grape::Entity
6262
}
6363
}
6464

65+
company_without_house_with_string = {
66+
'full_name' => 'full_name',
67+
'name' => 'name',
68+
'address' => {
69+
'post' => '123456',
70+
'city' => 'city',
71+
'street' => 'street',
72+
'something_else' => 'something_else'
73+
}
74+
}
75+
6576
expect(EntitySpec::CompanyWithString.represent(company_with_string).serializable_hash).to eq \
6677
company.slice(:name).merge(address: company[:address].slice(:city, :street, :house))
6778

79+
expect(EntitySpec::CompanyWithString.represent(company_without_house_with_string).serializable_hash).to eq \
80+
company.slice(:name).merge(address: company[:address].slice(:city, :street))
81+
6882
expect(EntitySpec::CompanyWithString.represent(company_with_string, full: true).serializable_hash).to eq \
6983
company.slice(:full_name, :name).merge(address: company[:address].slice(:city, :street, :house))
7084

0 commit comments

Comments
 (0)