Skip to content

Add option :merge to expose. Allow to merge fields into nested hashes and into the root #204

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 1 commit into from
Jan 29, 2016
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

* Your contribution here.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this back please.


* [#204](https://github.com/ruby-grape/grape-entity/pull/204): Added ability to merge fields into hashes/root (`:merge` option for `.expose`): [#138](https://github.com/ruby-grape/grape-entity/issues/138) - [@avyy](https://github.com/avyy).

0.5.0 (2015-12-07)
==================

Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ module API
expose :text, documentation: { type: "String", desc: "Status update text." }
expose :ip, if: { type: :full }
expose :user_type, :user_id, if: lambda { |status, options| status.user.public? }
expose :location, merge: true
expose :contact_info do
expose :phone
expose :address, using: API::Entities::Address
expose :address, merge: true, using: API::Entities::Address
end
expose :digest do |status, options|
Digest::MD5.hexdigest status.txt
Expand Down Expand Up @@ -153,6 +154,40 @@ As example:

```

#### Merge Fields

Use `:merge` option to merge fields into the hash or into the root:

```ruby
expose :contact_info do
expose :phone
expose :address, merge: true, using: API::Entities::Address
end

expose :status, merge: true
```

This will return something like:

```ruby
{ contact_info: { phone: "88002000700", city: 'City 17', address_line: 'Block C' }, text: 'HL3', likes: 19 }
```

It also works with collections:

```ruby
expose :profiles do
expose :users, merge: true, using: API::Entities::User
expose :admins, merge: true, using: API::Entities::Admin
end
```

Provide lambda to solve collisions:

```ruby
expose :status, merge: ->(key, old_val, new_val) { old_val + new_val if old_val && new_val }
```

#### Runtime Exposure

Use a block or a `Proc` to evaluate exposure at runtime. The supplied block or
Expand Down
3 changes: 2 additions & 1 deletion lib/grape_entity/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def self.inherited(subclass)
# block to the expose call to achieve the same effect.
# @option options :documentation Define documenation for an exposed
# field, typically the value is a hash with two fields, type and desc.
# @option options :merge This option allows you to merge an exposed field to the root
def self.expose(*args, &block)
options = merge_options(args.last.is_a?(Hash) ? args.pop : {})

Expand Down Expand Up @@ -498,7 +499,7 @@ def to_xml(options = {})

# All supported options.
OPTIONS = [
:rewrite, :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :attr_path, :if_extras, :unless_extras
:rewrite, :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :attr_path, :if_extras, :unless_extras, :merge
].to_set.freeze

# Merges the given options with current block options.
Expand Down
3 changes: 2 additions & 1 deletion lib/grape_entity/exposure/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Grape
class Entity
module Exposure
class Base
attr_reader :attribute, :key, :is_safe, :documentation, :conditions
attr_reader :attribute, :key, :is_safe, :documentation, :conditions, :for_merge

def self.new(attribute, options, conditions, *args, &block)
super(attribute, options, conditions).tap { |e| e.setup(*args, &block) }
Expand All @@ -13,6 +13,7 @@ def initialize(attribute, options, conditions)
@options = options
@key = (options[:as] || attribute).try(:to_sym)
@is_safe = options[:safe]
@for_merge = options[:merge]
@attr_path_proc = options[:attr_path]
@documentation = options[:documentation]
@conditions = conditions
Expand Down
11 changes: 7 additions & 4 deletions lib/grape_entity/exposure/nesting_exposure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ def valid?(entity)

def value(entity, options)
new_options = nesting_options_for(options)
output = OutputBuilder.new

normalized_exposures(entity, new_options).each_with_object({}) do |exposure, output|
normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
exposure.with_attr_path(entity, new_options) do
result = exposure.value(entity, new_options)
output[exposure.key] = result
out.add(exposure, result)
end
end
end
Expand All @@ -53,11 +54,12 @@ def valid_value_for(key, entity, options)

def serializable_value(entity, options)
new_options = nesting_options_for(options)
output = OutputBuilder.new

normalized_exposures(entity, new_options).each_with_object({}) do |exposure, output|
normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
exposure.with_attr_path(entity, new_options) do
result = exposure.serializable_value(entity, new_options)
output[exposure.key] = result
out.add(exposure, result)
end
end
end
Expand Down Expand Up @@ -126,3 +128,4 @@ def normalized_exposures(entity, options)
end

require 'grape_entity/exposure/nesting_exposure/nested_exposures'
require 'grape_entity/exposure/nesting_exposure/output_builder'
58 changes: 58 additions & 0 deletions lib/grape_entity/exposure/nesting_exposure/output_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module Grape
class Entity
module Exposure
class NestingExposure
class OutputBuilder < SimpleDelegator
def initialize
@output_hash = {}
@output_collection = []
end

def add(exposure, result)
# Save a result array in collections' array if it should be merged
if result.is_a?(Array) && exposure.for_merge
@output_collection << result
else

# If we have an array which should not be merged - save it with a key as a hash
# If we have hash which should be merged - save it without a key (merge)
if exposure.for_merge
@output_hash.merge! result, &merge_strategy(exposure.for_merge)
else
@output_hash[exposure.key] = result
end

end
end

def __getobj__
output
end

private

# If output_collection contains at least one element we have to represent the output as a collection
def output
if @output_collection.empty?
output = @output_hash
else
output = @output_collection
output << @output_hash unless @output_hash.empty?
output.flatten!
end
output
end

# In case if we want to solve collisions providing lambda to :merge option
def merge_strategy(for_merge)
if for_merge.respond_to? :call
for_merge
else
-> {}
end
end
end
end
end
end
end
43 changes: 42 additions & 1 deletion spec/grape_entity/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@
end
end

context 'with a :merge option' do
let(:nested_hash) do
{ something: { like_nested_hash: true }, special: { like_nested_hash: '12' } }
end

it 'merges an exposure to the root' do
subject.expose(:something, merge: true)
expect(subject.represent(nested_hash).serializable_hash).to eq(nested_hash[:something])
end

it 'allows to solve collisions providing a lambda to a :merge option' do
subject.expose(:something, merge: true)
subject.expose(:special, merge: ->(_, v1, v2) { v1 && v2 ? 'brand new val' : v2 })
expect(subject.represent(nested_hash).serializable_hash).to eq(like_nested_hash: 'brand new val')
end
end

context 'with a block' do
it 'errors out if called with multiple attributes' do
expect { subject.expose(:name, :email) { true } }.to raise_error ArgumentError
Expand Down Expand Up @@ -243,6 +260,30 @@ class Parent < Person
}
)
end
it 'merges attriutes if :merge option is passed' do
user_entity = Class.new(Grape::Entity)
admin_entity = Class.new(Grape::Entity)
user_entity.expose(:id, :name)
admin_entity.expose(:id, :name)

subject.expose(:profiles) do
subject.expose(:users, merge: true, using: user_entity)
subject.expose(:admins, merge: true, using: admin_entity)
end

subject.expose :awesome do
subject.expose(:nested, merge: true) { |_| { just_a_key: 'value' } }
subject.expose(:another_nested, merge: true) { |_| { just_another_key: 'value' } }
end

additional_hash = { users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jay' }],
admins: [{ id: 3, name: 'Jack' }, { id: 4, name: 'James' }]
}
expect(subject.represent(additional_hash).serializable_hash).to eq(
profiles: additional_hash[:users] + additional_hash[:admins],
awesome: { just_a_key: 'value', just_another_key: 'value' }
)
end
end
end

Expand Down Expand Up @@ -863,7 +904,7 @@ class Parent < Person
subject.expose :my_items

representation = subject.represent(4.times.map { Object.new }, serializable: true)
expect(representation).to be_kind_of(Hash)
expect(representation).to be_kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder)
expect(representation).to have_key :my_items
expect(representation[:my_items]).to be_kind_of Array
expect(representation[:my_items].size).to be 4
Expand Down