Skip to content

Commit

Permalink
Add option :merge to expose. Allow to merge fields into nested hashes…
Browse files Browse the repository at this point in the history
… or into the root. This also closes ruby-grape#138
  • Loading branch information
sagebomb committed Jan 28, 2016
1 parent 30fd0bc commit d0a1b72
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 8 deletions.
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.

* [#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
31 changes: 30 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,34 @@ 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
```

#### 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'
49 changes: 49 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,49 @@
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
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
end
end
end
end
end
37 changes: 36 additions & 1 deletion spec/grape_entity/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@
end
end

context 'with a :merge option' do
let(:nested_hash) do
{ something: { like_nested_hash: true } }
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
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 +254,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 +898,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

0 comments on commit d0a1b72

Please sign in to comment.