Skip to content

Commit d98042f

Browse files
committed
Add option :merge to expose. Allow to merge fields into nested hashes or into the root. This also closes #138
1 parent 30fd0bc commit d98042f

File tree

6 files changed

+126
-8
lines changed

6 files changed

+126
-8
lines changed

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ module API
2121
expose :text, documentation: { type: "String", desc: "Status update text." }
2222
expose :ip, if: { type: :full }
2323
expose :user_type, :user_id, if: lambda { |status, options| status.user.public? }
24+
expose :location, merge: true
2425
expose :contact_info do
2526
expose :phone
26-
expose :address, using: API::Entities::Address
27+
expose :address, merge: true, using: API::Entities::Address
2728
end
2829
expose :digest do |status, options|
2930
Digest::MD5.hexdigest status.txt
@@ -153,6 +154,34 @@ As example:
153154

154155
```
155156

157+
#### Merge Fields
158+
159+
Use `:merge` option to merge fields into the hash or into the root:
160+
161+
```ruby
162+
expose :contact_info do
163+
expose :phone
164+
expose :address, merge: true, using: API::Entities::Address
165+
end
166+
167+
expose :status, merge: true
168+
```
169+
170+
This will return something like:
171+
172+
```ruby
173+
{ contact_info: { phone: "88002000700", city: 'City 17', address_line: 'Block C' }, text: 'HL3', likes: 19 }
174+
```
175+
176+
It also works with collections:
177+
178+
```ruby
179+
expose :profiles do
180+
expose :users, merge: true, using: API::Entities::User
181+
expose :admins, merge: true, using: API::Entities::Admin
182+
end
183+
```
184+
156185
#### Runtime Exposure
157186

158187
Use a block or a `Proc` to evaluate exposure at runtime. The supplied block or

lib/grape_entity/entity.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def self.inherited(subclass)
146146
# block to the expose call to achieve the same effect.
147147
# @option options :documentation Define documenation for an exposed
148148
# field, typically the value is a hash with two fields, type and desc.
149+
# @option options :merge This option allows you to merge an exposed field to the root
149150
def self.expose(*args, &block)
150151
options = merge_options(args.last.is_a?(Hash) ? args.pop : {})
151152

@@ -498,7 +499,7 @@ def to_xml(options = {})
498499

499500
# All supported options.
500501
OPTIONS = [
501-
:rewrite, :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :attr_path, :if_extras, :unless_extras
502+
:rewrite, :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :attr_path, :if_extras, :unless_extras, :merge
502503
].to_set.freeze
503504

504505
# Merges the given options with current block options.

lib/grape_entity/exposure/base.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module Grape
22
class Entity
33
module Exposure
44
class Base
5-
attr_reader :attribute, :key, :is_safe, :documentation, :conditions
5+
attr_reader :attribute, :key, :is_safe, :documentation, :conditions, :for_merge
66

77
def self.new(attribute, options, conditions, *args, &block)
88
super(attribute, options, conditions).tap { |e| e.setup(*args, &block) }
@@ -13,6 +13,7 @@ def initialize(attribute, options, conditions)
1313
@options = options
1414
@key = (options[:as] || attribute).try(:to_sym)
1515
@is_safe = options[:safe]
16+
@for_merge = options[:merge]
1617
@attr_path_proc = options[:attr_path]
1718
@documentation = options[:documentation]
1819
@conditions = conditions

lib/grape_entity/exposure/nesting_exposure.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ def valid?(entity)
3030

3131
def value(entity, options)
3232
new_options = nesting_options_for(options)
33+
output = OutputBuilder.new
3334

34-
normalized_exposures(entity, new_options).each_with_object({}) do |exposure, output|
35+
normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
3536
exposure.with_attr_path(entity, new_options) do
3637
result = exposure.value(entity, new_options)
37-
output[exposure.key] = result
38+
out.add(exposure, result)
3839
end
3940
end
4041
end
@@ -53,11 +54,12 @@ def valid_value_for(key, entity, options)
5354

5455
def serializable_value(entity, options)
5556
new_options = nesting_options_for(options)
57+
output = OutputBuilder.new
5658

57-
normalized_exposures(entity, new_options).each_with_object({}) do |exposure, output|
59+
normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
5860
exposure.with_attr_path(entity, new_options) do
5961
result = exposure.serializable_value(entity, new_options)
60-
output[exposure.key] = result
62+
out.add(exposure, result)
6163
end
6264
end
6365
end
@@ -126,3 +128,4 @@ def normalized_exposures(entity, options)
126128
end
127129

128130
require 'grape_entity/exposure/nesting_exposure/nested_exposures'
131+
require 'grape_entity/exposure/nesting_exposure/output_builder'
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
module Grape
2+
class Entity
3+
module Exposure
4+
class NestingExposure
5+
class OutputBuilder < SimpleDelegator
6+
def initialize
7+
@output_hash = {}
8+
@output_collection = []
9+
end
10+
11+
def add(exposure, result)
12+
# Save a result array in collections' array if it should be merged
13+
if result.is_a?(Array) && exposure.for_merge
14+
@output_collection << result
15+
else
16+
17+
# If we have an array which should not be merged - save it with a key as a hash
18+
# If we have hash which should be merged - save it without a key (merge)
19+
if exposure.for_merge
20+
@output_hash.merge! result
21+
else
22+
@output_hash[exposure.key] = result
23+
end
24+
25+
end
26+
end
27+
28+
def __getobj__
29+
output
30+
end
31+
32+
private
33+
34+
# If output_collection contains at least one element we have to represent the output as a collection
35+
def output
36+
if @output_collection.empty?
37+
output = @output_hash
38+
else
39+
output = @output_collection
40+
output << @output_hash unless @output_hash.empty?
41+
output.flatten!
42+
end
43+
output
44+
end
45+
end
46+
end
47+
end
48+
end
49+
end

spec/grape_entity/entity_spec.rb

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@
3535
end
3636
end
3737

38+
context 'with a :merge option' do
39+
let(:nested_hash) do
40+
{ something: { like_nested_hash: true } }
41+
end
42+
43+
it 'merges an exposure to the root' do
44+
subject.expose(:something, merge: true)
45+
expect(subject.represent(nested_hash).serializable_hash).to eq(nested_hash[:something])
46+
end
47+
end
48+
3849
context 'with a block' do
3950
it 'errors out if called with multiple attributes' do
4051
expect { subject.expose(:name, :email) { true } }.to raise_error ArgumentError
@@ -243,6 +254,30 @@ class Parent < Person
243254
}
244255
)
245256
end
257+
it 'merges attriutes if :merge option is passed' do
258+
user_entity = Class.new(Grape::Entity)
259+
admin_entity = Class.new(Grape::Entity)
260+
user_entity.expose(:id, :name)
261+
admin_entity.expose(:id, :name)
262+
263+
subject.expose(:profiles) do
264+
subject.expose(:users, merge: true, using: user_entity)
265+
subject.expose(:admins, merge: true, using: admin_entity)
266+
end
267+
268+
subject.expose :awesome do
269+
subject.expose(:nested, merge: true) { |_| { just_a_key: 'value' } }
270+
subject.expose(:another_nested, merge: true) { |_| { just_another_key: 'value' } }
271+
end
272+
273+
additional_hash = { users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jay' }],
274+
admins: [{ id: 3, name: 'Jack' }, { id: 4, name: 'James' }]
275+
}
276+
expect(subject.represent(additional_hash).serializable_hash).to eq(
277+
profiles: additional_hash[:users] + additional_hash[:admins],
278+
awesome: { just_a_key: 'value', just_another_key: 'value' }
279+
)
280+
end
246281
end
247282
end
248283

@@ -863,7 +898,7 @@ class Parent < Person
863898
subject.expose :my_items
864899

865900
representation = subject.represent(4.times.map { Object.new }, serializable: true)
866-
expect(representation).to be_kind_of(Hash)
901+
expect(representation).to be_kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder)
867902
expect(representation).to have_key :my_items
868903
expect(representation[:my_items]).to be_kind_of Array
869904
expect(representation[:my_items].size).to be 4

0 commit comments

Comments
 (0)