Skip to content

Commit 4284ee5

Browse files
wyattisimodblock
authored andcommitted
support nested expose declarations
1 parent cb08764 commit 4284ee5

File tree

4 files changed

+107
-31
lines changed

4 files changed

+107
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Next Release
1010
* [#28](https://github.com/intridea/grape-entity/pull/28): Look for method on entity before calling it on the object - [@MichaelXavier](https://github.com/MichaelXavier).
1111
* [#33](https://github.com/intridea/grape-entity/pull/33): Support proper merging of nested conditionals - [@wyattisimo](https://github.com/wyattisimo).
1212
* [#43](https://github.com/intridea/grape-entity/pull/43): Call procs in context of entity instance - [@joelvh](https://github.com/joelvh).
13+
* [#47](https://github.com/intridea/grape-entity/pull/47): Support nested exposures - [@wyattisimo](https://github.com/wyattisimo).
1314
* Your contribution here.
1415

1516
0.3.0 (2013-03-29)

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ module API
1818
expose :text, documentation: { type: "String", desc: "Status update text." }
1919
expose :ip, if: { type: :full }
2020
expose :user_type, :user_id, if: lambda { |status, options| status.user.public? }
21+
expose :contact_info do
22+
expose :phone
23+
expose :address, using: API::Address
24+
end
2125
expose :digest do |status, options|
2226
Digest::MD5.hexdigest status.txt
2327
end
@@ -89,9 +93,23 @@ Don't raise an exception and expose as nil, even if the :x cannot be evaluated.
8993
expose :ip, safe: true
9094
```
9195

96+
#### Nested Exposure
97+
98+
Supply a block to define a hash using nested exposures.
99+
100+
```ruby
101+
expose :contact_info do
102+
expose :phone
103+
expose :address, using: API::Address
104+
end
105+
```
106+
92107
#### Runtime Exposure
93108

94-
Use a block or a `Proc` to evaluate exposure at runtime.
109+
Use a block or a `Proc` to evaluate exposure at runtime. The supplied block or
110+
`Proc` will be called with two parameters: the represented object and runtime options.
111+
112+
**NOTE:** A block supplied with no parameters will be evaluated as a nested exposure (see above).
95113

96114
```ruby
97115
expose :digest do |status, options|

lib/grape_entity/entity.rb

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,22 @@ def self.expose(*args, &block)
132132

133133
raise ArgumentError, "You may not use block-setting when also using format_with" if block_given? && options[:format_with].respond_to?(:call)
134134

135-
options[:proc] = block if block_given?
135+
options[:proc] = block if block_given? && block.parameters.any?
136136

137+
@nested_attributes ||= []
137138
args.each do |attribute|
139+
unless @nested_attributes.empty?
140+
attribute = "#{@nested_attributes.last}__#{attribute}"
141+
end
142+
138143
exposures[attribute.to_sym] = options
144+
145+
# Nested exposures are given in a block with no parameters.
146+
if block_given? && block.parameters.empty?
147+
@nested_attributes << attribute
148+
block.call
149+
@nested_attributes.pop
150+
end
139151
end
140152
end
141153

@@ -368,27 +380,33 @@ def to_xml(options = {})
368380

369381
protected
370382

383+
def self.name_for(attribute)
384+
attribute.to_s.split('__').last.to_sym
385+
end
386+
371387
def self.key_for(attribute)
372-
exposures[attribute.to_sym][:as] || attribute.to_sym
388+
exposures[attribute.to_sym][:as] || name_for(attribute)
373389
end
374390

375391
def value_for(attribute, options = {})
376392
exposure_options = exposures[attribute.to_sym]
377393

378-
if exposure_options[:proc]
379-
if exposure_options[:using]
380-
using_options = options.dup
381-
using_options.delete(:collection)
382-
using_options[:root] = nil
383-
exposure_options[:using].represent(instance_exec(object, options, &exposure_options[:proc]), using_options)
384-
else
385-
instance_exec(object, options, &exposure_options[:proc])
386-
end
387-
elsif exposure_options[:using]
394+
nested_exposures = exposures.select { |a, _| a.to_s =~ /^#{attribute}__/ }
395+
396+
if exposure_options[:using]
388397
using_options = options.dup
389398
using_options.delete(:collection)
390399
using_options[:root] = nil
391-
exposure_options[:using].represent(delegate_attribute(attribute), using_options)
400+
401+
if exposure_options[:proc]
402+
exposure_options[:using].represent(instance_exec(object, options, &exposure_options[:proc]), using_options)
403+
else
404+
exposure_options[:using].represent(delegate_attribute(attribute), using_options)
405+
end
406+
407+
elsif exposure_options[:proc]
408+
instance_exec(object, options, &exposure_options[:proc])
409+
392410
elsif exposure_options[:format_with]
393411
format_with = exposure_options[:format_with]
394412

@@ -399,23 +417,30 @@ def value_for(attribute, options = {})
399417
elsif format_with.respond_to? :call
400418
instance_exec(delegate_attribute(attribute), &format_with)
401419
end
420+
421+
elsif nested_exposures.any?
422+
Hash[nested_exposures.map do |nested_attribute, _|
423+
[self.class.key_for(nested_attribute), value_for(nested_attribute, options)]
424+
end]
425+
402426
else
403427
delegate_attribute(attribute)
404428
end
405429
end
406430

407431
def delegate_attribute(attribute)
408-
if respond_to?(attribute, true)
409-
send(attribute)
432+
name = self.class.name_for(attribute)
433+
if respond_to?(name, true)
434+
send(name)
410435
else
411-
object.send(attribute)
436+
object.send(name)
412437
end
413438
end
414439

415440
def valid_exposure?(attribute, exposure_options)
416441
exposure_options.has_key?(:proc) || \
417442
!exposure_options[:safe] || \
418-
object.respond_to?(attribute)
443+
object.respond_to?(self.class.name_for(attribute))
419444
end
420445

421446
def conditions_met?(exposure_options, options)

spec/grape_entity/entity_spec.rb

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,6 @@
3636
expect { subject.expose(:name, :email) { true } }.to raise_error ArgumentError
3737
end
3838

39-
it 'sets the :proc option in the exposure options' do
40-
block = lambda { |_| true }
41-
subject.expose :name, using: 'Awesome', &block
42-
subject.exposures[:name].should == { proc: block, using: 'Awesome' }
43-
end
44-
45-
it 'references an instance of the entity without any options' do
46-
subject.expose(:size) { self }
47-
subject.represent(Hash.new).send(:value_for, :size).should be_an_instance_of fresh_class
48-
end
49-
5039
it 'references an instance of the entity with :using option' do
5140
module EntitySpec
5241
class SomeObject1
@@ -74,6 +63,49 @@ class BogusEntity < Grape::Entity
7463
prop1 = value.send(:value_for, :prop1)
7564
prop1.should == "MODIFIED 2"
7665
end
66+
67+
context 'with parameters passed to the block' do
68+
it 'sets the :proc option in the exposure options' do
69+
block = lambda { |_| true }
70+
subject.expose :name, using: 'Awesome', &block
71+
subject.exposures[:name].should == { proc: block, using: 'Awesome' }
72+
end
73+
74+
it 'references an instance of the entity without any options' do
75+
subject.expose(:size) { |_| self }
76+
subject.represent(Hash.new).send(:value_for, :size).should be_an_instance_of fresh_class
77+
end
78+
end
79+
80+
context 'with no parameters passed to the block' do
81+
it 'adds a nested exposure' do
82+
subject.expose :awesome do
83+
subject.expose :nested do
84+
subject.expose :moar_nested, as: 'weee'
85+
end
86+
subject.expose :another_nested, using: 'Awesome'
87+
end
88+
89+
subject.exposures.should == {
90+
awesome: {},
91+
awesome__nested: {},
92+
awesome__nested__moar_nested: { as: 'weee' },
93+
awesome__another_nested: { using: 'Awesome' }
94+
}
95+
end
96+
97+
it 'represents the exposure as a hash of its nested exposures' do
98+
subject.expose :awesome do
99+
subject.expose(:nested) { |_| "value" }
100+
subject.expose(:another_nested) { |_| "value" }
101+
end
102+
103+
subject.represent({}).send(:value_for, :awesome).should == {
104+
nested: "value",
105+
another_nested: "value"
106+
}
107+
end
108+
end
77109
end
78110

79111
context 'inherited exposures' do
@@ -293,13 +325,13 @@ class BogusEntity < Grape::Entity
293325
end
294326

295327
it 'returns a serialized hash of a single object if serializable: true' do
296-
subject.expose(:awesome) { true }
328+
subject.expose(:awesome) { |_| true }
297329
representation = subject.represent(Object.new, serializable: true)
298330
representation.should == { awesome: true }
299331
end
300332

301333
it 'returns a serialized array of hashes of multiple objects if serializable: true' do
302-
subject.expose(:awesome) { true }
334+
subject.expose(:awesome) { |_| true }
303335
representation = subject.represent(2.times.map { Object.new }, serializable: true)
304336
representation.should == [{ awesome: true }, { awesome: true }]
305337
end

0 commit comments

Comments
 (0)