Skip to content

Respect hash_access setting when using expose_nil: false option #359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#### Fixes

* [#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-).
* Your contribution here.


Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ The field lookup takes several steps
* next try `object.fetch(exposure)`
* last raise an Exception

`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:

```ruby
class Status < GrapeEntity
self.hash_access = :to_s

expose :code
expose :message
end

Status.represent({ 'code' => 418, 'message' => "I'm a teapot" }).as_json
#=> { code: 418, message: "I'm a teapot" }
```

#### Exposing with a Presenter

Don't derive your model classes from `Grape::Entity`, expose them using a presenter.
Expand Down
12 changes: 8 additions & 4 deletions lib/grape_entity/delegator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ class Entity
module Delegator
def self.new(object)
delegator_klass =
if object.is_a?(Hash) then HashObject
elsif defined?(OpenStruct) && object.is_a?(OpenStruct) then OpenStructObject
elsif object.respond_to?(:fetch, true) then FetchableObject
else PlainObject
if object.is_a?(Hash)
HashObject
elsif defined?(OpenStruct) && object.is_a?(OpenStruct)
OpenStructObject
elsif object.respond_to?(:fetch, true)
FetchableObject
else
PlainObject
end

delegator_klass.new(object)
Expand Down
5 changes: 5 additions & 0 deletions lib/grape_entity/delegator/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ def initialize(object)
def delegatable?(_attribute)
true
end

def accepts_options?
# Why not `arity > 1`? It might be negative https://ruby-doc.org/core-2.6.6/Method.html#method-i-arity
method(:delegate).arity != 1
end
end
end
end
Expand Down
5 changes: 1 addition & 4 deletions lib/grape_entity/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -481,9 +481,6 @@ def initialize(object, options = {})
@object = object
@options = options.is_a?(Options) ? options : Options.new(options)
@delegator = Delegator.new(object)

# Why not `arity > 1`? It might be negative https://ruby-doc.org/core-2.6.6/Method.html#method-i-arity
@delegator_accepts_opts = @delegator.method(:delegate).arity != 1
end

def root_exposures
Expand Down Expand Up @@ -541,7 +538,7 @@ def value_for(key, options = Options.new)
def delegate_attribute(attribute)
if is_defined_in_entity?(attribute)
send(attribute)
elsif @delegator_accepts_opts
elsif delegator.accepts_options?
delegator.delegate(attribute, **self.class.delegation_opts)
else
delegator.delegate(attribute)
Expand Down
7 changes: 6 additions & 1 deletion lib/grape_entity/exposure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ def expose_nil_condition(attribute, options)
Condition.new_unless(
proc do |object, _options|
if options[:proc].nil?
Delegator.new(object).delegate(attribute).nil?
delegator = Delegator.new(object)
if is_a?(Grape::Entity) && delegator.accepts_options?
delegator.delegate(attribute, **self.class.delegation_opts).nil?
else
delegator.delegate(attribute).nil?
end
else
exec_with_object(options, &options[:proc]).nil?
end
Expand Down
16 changes: 15 additions & 1 deletion spec/grape_entity/hash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AddressWithString < Grape::Entity
expose :post, if: :full
expose :city
expose :street
expose :house
expose :house, expose_nil: false
end

class Company < Grape::Entity
Expand Down Expand Up @@ -62,9 +62,23 @@ class CompanyWithString < Grape::Entity
}
}

company_without_house_with_string = {
'full_name' => 'full_name',
'name' => 'name',
'address' => {
'post' => '123456',
'city' => 'city',
'street' => 'street',
'something_else' => 'something_else'
}
}

expect(EntitySpec::CompanyWithString.represent(company_with_string).serializable_hash).to eq \
company.slice(:name).merge(address: company[:address].slice(:city, :street, :house))

expect(EntitySpec::CompanyWithString.represent(company_without_house_with_string).serializable_hash).to eq \
company.slice(:name).merge(address: company[:address].slice(:city, :street))

expect(EntitySpec::CompanyWithString.represent(company_with_string, full: true).serializable_hash).to eq \
company.slice(:full_name, :name).merge(address: company[:address].slice(:city, :street, :house))

Expand Down