Skip to content

Commit fa05156

Browse files
committed
tracking attribute path into options[:attr_path]
some refines; new test cases; change log updated
1 parent 36d7d75 commit fa05156

File tree

5 files changed

+181
-22
lines changed

5 files changed

+181
-22
lines changed

.rubocop_todo.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77

88
# Offense count: 8
99
Metrics/AbcSize:
10-
Max: 51
10+
Max: 53
1111

1212
# Offense count: 1
1313
# Configuration parameters: CountComments.
1414
Metrics/ClassLength:
15-
Max: 328
15+
Max: 346
1616

1717
# Offense count: 5
1818
Metrics/CyclomaticComplexity:
@@ -26,7 +26,7 @@ Metrics/LineLength:
2626
# Offense count: 7
2727
# Configuration parameters: CountComments.
2828
Metrics/MethodLength:
29-
Max: 32
29+
Max: 37
3030

3131
# Offense count: 5
3232
Metrics/PerceivedComplexity:

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
0.5.0 (Next)
22
============
3+
* [#139](https://github.com/intridea/grape-entity/pull/139): Keep a track of attribute nesting path during condition check or runtime exposure - [@calfzhou](https://github.com/calfzhou).
34
* Your contribution here.
45

56
0.4.6 (2015-07-27)
@@ -83,4 +84,3 @@
8384
==================
8485

8586
* Initial public release - [@agileanimal](https://github.com/agileanimal).
86-

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,32 @@ end
334334
```
335335
**Notice**: In the above code, you should pay attention to [**Safe Exposure**](#safe-exposure) yourself, for example, `instance.address` might be `nil`, in this situation, it is better to expose it as nil directly.
336336

337+
#### Attribute Path Tracking
338+
339+
Sometimes, especially when there are nested attributes, you might want to know which attribute it
340+
is being exposed. For example, some APIs allow user provide a parameter to control which fields
341+
will be included in (or excluded from) the response.
342+
343+
Grape entity can track the path of each attribute, then you could access it during condition check
344+
or runtime exposure, via `options[:attr_path]`.
345+
346+
Attribute path is an array. The last item of this array is the name (alias) of current attribute.
347+
And if the attribute is nested, the former items are names (aliases) of its ancestor attributes.
348+
349+
Here is an example shows what the attribute path will be.
350+
351+
```ruby
352+
class Status < Grape::Entity
353+
expose :user # path is [:user]
354+
expose :foo, as: :bar # path is [:bar]
355+
expose :a do
356+
expose :b, as: :xx do
357+
expose :c # path is [:a, :xx, :c]
358+
end
359+
end
360+
end
361+
```
362+
337363
### Using the Exposure DSL
338364

339365
Grape ships with a DSL to easily define entities within the context of an existing class:

lib/grape_entity/entity.rb

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -428,22 +428,11 @@ def serializable_hash(runtime_options = {})
428428
opts = options.merge(runtime_options || {})
429429

430430
root_exposures.each_with_object({}) do |(attribute, exposure_options), output|
431-
next unless should_return_attribute?(attribute, opts) && conditions_met?(exposure_options, opts)
432-
433-
partial_output = value_for(attribute, opts)
434-
435-
output[self.class.key_for(attribute)] =
436-
if partial_output.respond_to?(:serializable_hash)
437-
partial_output.serializable_hash(runtime_options)
438-
elsif partial_output.is_a?(Array) && partial_output.all? { |o| o.respond_to?(:serializable_hash) }
439-
partial_output.map(&:serializable_hash)
440-
elsif partial_output.is_a?(Hash)
441-
partial_output.each do |key, value|
442-
partial_output[key] = value.serializable_hash if value.respond_to?(:serializable_hash)
443-
end
444-
else
445-
partial_output
446-
end
431+
parent_path = track_attr_path(attribute, opts)
432+
if should_return_attribute?(attribute, opts) && conditions_met?(exposure_options, opts)
433+
output[self.class.key_for(attribute)] = partial_hash(attribute, opts)
434+
end
435+
backtrack_attr_path(parent_path, opts)
447436
end
448437
end
449438

@@ -529,14 +518,34 @@ def nested_value_for(attribute, options)
529518
nested_exposures = self.class.nested_exposures[attribute]
530519
nested_attributes =
531520
nested_exposures.map do |nested_attribute, nested_exposure_options|
532-
if conditions_met?(nested_exposure_options, options)
533-
[self.class.key_for(nested_attribute), value_for(nested_attribute, options)]
521+
parent_path = track_attr_path(nested_attribute, options)
522+
begin
523+
if conditions_met?(nested_exposure_options, options)
524+
[self.class.key_for(nested_attribute), value_for(nested_attribute, options)]
525+
end
526+
ensure
527+
backtrack_attr_path(parent_path, options)
534528
end
535529
end
536530

537531
Hash[nested_attributes.compact]
538532
end
539533

534+
def self.path_for(attribute)
535+
key_for(attribute)
536+
end
537+
538+
def track_attr_path(attribute, options)
539+
parent_path = options[:attr_path]
540+
current_path = self.class.path_for(attribute)
541+
options[:attr_path] = (parent_path || []).dup << current_path unless current_path.nil?
542+
parent_path
543+
end
544+
545+
def backtrack_attr_path(parent_path, options)
546+
options[:attr_path] = parent_path
547+
end
548+
540549
def value_for(attribute, options = {})
541550
exposure_options = exposures[attribute.to_sym]
542551
return unless valid_exposure?(attribute, exposure_options)
@@ -573,6 +582,21 @@ def value_for(attribute, options = {})
573582
end
574583
end
575584

585+
def partial_hash(attribute, options = {})
586+
partial_output = value_for(attribute, options)
587+
if partial_output.respond_to?(:serializable_hash)
588+
partial_output.serializable_hash(options)
589+
elsif partial_output.is_a?(Array) && !partial_output.map { |o| o.respond_to?(:serializable_hash) }.include?(false)
590+
partial_output.map(&:serializable_hash)
591+
elsif partial_output.is_a?(Hash)
592+
partial_output.each do |key, value|
593+
partial_output[key] = value.serializable_hash if value.respond_to?(:serializable_hash)
594+
end
595+
else
596+
partial_output
597+
end
598+
end
599+
576600
def delegate_attribute(attribute)
577601
name = self.class.name_for(attribute)
578602
if respond_to?(name, true)

spec/grape_entity/entity_spec.rb

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,11 @@ class Parent < Person
829829
friends: [
830830
double(name: 'Friend 1', email: 'friend1@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: []),
831831
double(name: 'Friend 2', email: 'friend2@example.com', characteristics: [], fantasies: [], birthday: Time.gm(2012, 2, 27), friends: [])
832+
],
833+
extra: { key: 'foo', value: 'bar' },
834+
nested: [
835+
{ name: 'n1', data: { key: 'ex1', value: 'v1' } },
836+
{ name: 'n2', data: { key: 'ex2', value: 'v2' } }
832837
]
833838
}
834839
end
@@ -991,6 +996,110 @@ def embedded
991996
expect(presenter.serializable_hash).to eq(name: 'abc', embedded: { a: nil, b: { abc: 'def' } })
992997
end
993998
end
999+
1000+
context '#attr_path' do
1001+
it 'for all kinds of attributes' do
1002+
module EntitySpec
1003+
class EmailEntity < Grape::Entity
1004+
expose(:email, as: :addr) { |_, o| o[:attr_path].join('/') }
1005+
end
1006+
1007+
class UserEntity < Grape::Entity
1008+
expose(:name, as: :full_name) { |_, o| o[:attr_path].join('/') }
1009+
expose :email, using: 'EntitySpec::EmailEntity'
1010+
end
1011+
1012+
class ExtraEntity < Grape::Entity
1013+
expose(:key) { |_, o| o[:attr_path].join('/') }
1014+
expose(:value) { |_, o| o[:attr_path].join('/') }
1015+
end
1016+
1017+
class NestedEntity < Grape::Entity
1018+
expose(:name) { |_, o| o[:attr_path].join('/') }
1019+
expose :data, using: 'EntitySpec::ExtraEntity'
1020+
end
1021+
end
1022+
1023+
fresh_class.class_eval do
1024+
expose(:id) { |_, o| o[:attr_path].join('/') }
1025+
expose(:foo, as: :bar) { |_, o| o[:attr_path].join('/') }
1026+
expose :title do
1027+
expose :full do
1028+
expose(:prefix, as: :pref) { |_, o| o[:attr_path].join('/') }
1029+
expose(:main) { |_, o| o[:attr_path].join('/') }
1030+
end
1031+
end
1032+
expose :friends, as: :social, using: 'EntitySpec::UserEntity'
1033+
expose :extra, using: 'EntitySpec::ExtraEntity'
1034+
expose :nested, using: 'EntitySpec::NestedEntity'
1035+
end
1036+
1037+
expect(subject.serializable_hash).to eq(
1038+
id: 'id',
1039+
bar: 'bar',
1040+
title: { full: { pref: 'title/full/pref', main: 'title/full/main' } },
1041+
social: [
1042+
{ full_name: 'social/full_name', email: { addr: 'social/email/addr' } },
1043+
{ full_name: 'social/full_name', email: { addr: 'social/email/addr' } }
1044+
],
1045+
extra: { key: 'extra/key', value: 'extra/value' },
1046+
nested: [
1047+
{ name: 'nested/name', data: { key: 'nested/data/key', value: 'nested/data/value' } },
1048+
{ name: 'nested/name', data: { key: 'nested/data/key', value: 'nested/data/value' } }
1049+
]
1050+
)
1051+
end
1052+
1053+
it 'allows customize path of an attribute' do
1054+
module EntitySpec
1055+
class CharacterEntity < Grape::Entity
1056+
expose(:key) { |_, o| o[:attr_path].join('/') }
1057+
expose(:value) { |_, o| o[:attr_path].join('/') }
1058+
end
1059+
end
1060+
1061+
fresh_class.class_eval do
1062+
expose :characteristics, using: EntitySpec::CharacterEntity
1063+
1064+
protected
1065+
1066+
def self.path_for(attribute)
1067+
attribute == :characteristics ? :character : super
1068+
end
1069+
end
1070+
1071+
expect(subject.serializable_hash).to eq(
1072+
characteristics: [
1073+
{ key: 'character/key', value: 'character/value' }
1074+
]
1075+
)
1076+
end
1077+
1078+
it 'can drop one nest level by set path_for to nil' do
1079+
module EntitySpec
1080+
class NoPathCharacterEntity < Grape::Entity
1081+
expose(:key) { |_, o| o[:attr_path].join('/') }
1082+
expose(:value) { |_, o| o[:attr_path].join('/') }
1083+
end
1084+
end
1085+
1086+
fresh_class.class_eval do
1087+
expose :characteristics, using: EntitySpec::NoPathCharacterEntity
1088+
1089+
protected
1090+
1091+
def self.path_for(_attribute)
1092+
nil
1093+
end
1094+
end
1095+
1096+
expect(subject.serializable_hash).to eq(
1097+
characteristics: [
1098+
{ key: 'key', value: 'value' }
1099+
]
1100+
)
1101+
end
1102+
end
9941103
end
9951104

9961105
describe '#value_for' do

0 commit comments

Comments
 (0)