Skip to content

Commit 6375f15

Browse files
estevaoamdblock
authored andcommitted
Added 'only' option that selects which attributes should be returned.
1 parent 81a69a8 commit 6375f15

File tree

5 files changed

+163
-55
lines changed

5 files changed

+163
-55
lines changed

.rubocop_todo.yml

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
# This configuration was generated by `rubocop --auto-gen-config`
2-
# on 2014-12-11 15:27:22 -0500 using RuboCop version 0.28.0.
2+
# on 2015-03-16 03:40:21 -0300 using RuboCop version 0.28.0.
33
# The point is for the user to remove these configuration records
44
# one by one as the offenses are removed from the code base.
55
# Note that changes in the inspected code, or installation of new
66
# versions of RuboCop, may require this file to be generated again.
77

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

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

17-
# Offense count: 4
17+
# Offense count: 5
1818
Metrics/CyclomaticComplexity:
1919
Max: 17
2020

21-
# Offense count: 175
21+
# Offense count: 187
2222
# Configuration parameters: AllowURI, URISchemes.
2323
Metrics/LineLength:
2424
Max: 147
2525

26-
# Offense count: 6
26+
# Offense count: 7
2727
# Configuration parameters: CountComments.
2828
Metrics/MethodLength:
29-
Max: 34
29+
Max: 32
3030

3131
# Offense count: 4
3232
Metrics/PerceivedComplexity:
@@ -37,11 +37,11 @@ Metrics/PerceivedComplexity:
3737
Style/Blocks:
3838
Enabled: false
3939

40-
# Offense count: 30
40+
# Offense count: 31
4141
Style/Documentation:
4242
Enabled: false
4343

44-
# Offense count: 2
44+
# Offense count: 3
4545
Style/EachWithObject:
4646
Enabled: false
4747

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
0.4.6 (Next)
22
============
33

4+
* [#114](https://github.com/intridea/grape-entity/pull/114): Added 'only' option that selects which attributes should be returned - [@estevaoam](https://github.com/estevaoam).
45
* Your contribution here.
56

67
0.4.5 (2015-03-10)

README.md

+34
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,40 @@ class MailingAddress < UserData
220220
end
221221
```
222222

223+
#### Returning only the fields you want
224+
225+
After exposing the desired attributes, you can choose which one you need when representing some object or collection, see the example:
226+
227+
```ruby
228+
class UserEntity
229+
expose :id
230+
expose :name
231+
expose :email
232+
end
233+
234+
class Entity
235+
expose :id
236+
expose :title
237+
expose :user, using: UserEntity
238+
end
239+
240+
data = Entity.represent(model, only: [:name, { user: [:name, :email] }])
241+
data.as_json
242+
```
243+
244+
This will return something like this:
245+
246+
```ruby
247+
{
248+
title: 'grape-entity is awesome!',
249+
user: {
250+
name: 'John Applet',
251+
email: 'john@example.com'
252+
}
253+
}
254+
```
255+
256+
Instead of returning all the exposed attributes.
223257

224258

225259

lib/grape_entity/entity.rb

+40-8
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ def self.present_collection(present_collection = false, collection_name = :items
404404
# even if one is defined for the entity.
405405
# @option options :serializable [true or false] when true a serializable Hash will be returned
406406
#
407+
# @option options :only [Array] all the fields that should be returned
407408
def self.represent(objects, options = {})
408409
if objects.respond_to?(:to_ary) && ! @present_collection
409410
root_element = @collection_root
@@ -458,27 +459,53 @@ def formatters
458459
# etc.
459460
def serializable_hash(runtime_options = {})
460461
return nil if object.nil?
462+
461463
opts = options.merge(runtime_options || {})
464+
462465
valid_exposures.inject({}) do |output, (attribute, exposure_options)|
463-
if conditions_met?(exposure_options, opts)
466+
if should_return_attribute?(attribute, opts) && conditions_met?(exposure_options, opts)
464467
partial_output = value_for(attribute, opts)
468+
465469
output[self.class.key_for(attribute)] =
466-
if partial_output.respond_to? :serializable_hash
470+
if partial_output.respond_to?(:serializable_hash)
467471
partial_output.serializable_hash(runtime_options)
468-
elsif partial_output.is_a?(Array) && !partial_output.map { |o| o.respond_to? :serializable_hash }.include?(false)
472+
elsif partial_output.is_a?(Array) && !partial_output.map { |o| o.respond_to?(:serializable_hash) }.include?(false)
469473
partial_output.map(&:serializable_hash)
470474
elsif partial_output.is_a?(Hash)
471475
partial_output.each do |key, value|
472-
partial_output[key] = value.serializable_hash if value.respond_to? :serializable_hash
476+
partial_output[key] = value.serializable_hash if value.respond_to?(:serializable_hash)
473477
end
474478
else
475479
partial_output
476480
end
477481
end
482+
478483
output
479484
end
480485
end
481486

487+
def should_return_attribute?(attribute, options)
488+
return true unless options[:only]
489+
only_fields(options).include?(self.class.key_for(attribute))
490+
end
491+
492+
def only_fields(options)
493+
return {} unless options[:only]
494+
495+
@only_fields ||= options[:only].inject({}) do |allowed_fields, attribute|
496+
if attribute.is_a?(Hash)
497+
attribute.each do |attr, nested_attrs|
498+
allowed_fields[attr] ||= []
499+
allowed_fields[attr] += nested_attrs
500+
end
501+
else
502+
allowed_fields[attribute] = true
503+
end
504+
505+
allowed_fields
506+
end
507+
end
508+
482509
alias_method :as_json, :serializable_hash
483510

484511
def to_json(options = {})
@@ -508,15 +535,12 @@ def self.nested_exposures_for(attribute)
508535

509536
def value_for(attribute, options = {})
510537
exposure_options = exposures[attribute.to_sym]
511-
512538
nested_exposures = self.class.nested_exposures_for(attribute)
513539

514540
if exposure_options[:using]
515541
exposure_options[:using] = exposure_options[:using].constantize if exposure_options[:using].respond_to? :constantize
516542

517-
using_options = options.dup
518-
using_options.delete(:collection)
519-
using_options[:root] = nil
543+
using_options = options_for_using(attribute, options)
520544

521545
if exposure_options[:proc]
522546
exposure_options[:using].represent(instance_exec(object, options, &exposure_options[:proc]), using_options)
@@ -612,6 +636,14 @@ def conditions_met?(exposure_options, options)
612636
true
613637
end
614638

639+
def options_for_using(attribute, options)
640+
using_options = options.dup
641+
using_options.delete(:collection)
642+
using_options[:root] = nil
643+
using_options[:only] = only_fields(using_options)[attribute]
644+
using_options
645+
end
646+
615647
# All supported options.
616648
OPTIONS = [
617649
:as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :if_extras, :unless_extras

spec/grape_entity/entity_spec.rb

+78-37
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class BogusEntity < Grape::Entity
7777

7878
it 'references an instance of the entity without any options' do
7979
subject.expose(:size) { |_| self }
80-
expect(subject.represent(Hash.new).send(:value_for, :size)).to be_an_instance_of fresh_class
80+
expect(subject.represent({}).send(:value_for, :size)).to be_an_instance_of fresh_class
8181
end
8282
end
8383

@@ -91,10 +91,10 @@ class BogusEntity < Grape::Entity
9191
end
9292

9393
expect(subject.exposures).to eq(
94-
awesome: {},
95-
awesome__nested: { nested: true },
96-
awesome__nested__moar_nested: { as: 'weee', nested: true },
97-
awesome__another_nested: { using: 'Awesome', nested: true }
94+
awesome: {},
95+
awesome__nested: { nested: true },
96+
awesome__nested__moar_nested: { as: 'weee', nested: true },
97+
awesome__another_nested: { using: 'Awesome', nested: true }
9898
)
9999
end
100100

@@ -105,8 +105,8 @@ class BogusEntity < Grape::Entity
105105
end
106106

107107
expect(subject.represent({}).send(:value_for, :awesome)).to eq(
108-
nested: 'value',
109-
another_nested: 'value'
108+
nested: 'value',
109+
another_nested: 'value'
110110
)
111111
end
112112

@@ -129,13 +129,13 @@ class BogusEntity < Grape::Entity
129129
end
130130

131131
expect(subject.represent({}).serializable_hash).to eq(
132-
awesome: {
133-
nested: 'value',
134-
another_nested: 'value',
135-
second_level_nested: {
136-
deeply_exposed_attr: 'value'
137-
}
138-
}
132+
awesome: {
133+
nested: 'value',
134+
another_nested: 'value',
135+
second_level_nested: {
136+
deeply_exposed_attr: 'value'
137+
}
138+
}
139139
)
140140
end
141141

@@ -162,22 +162,22 @@ class Parent < Person
162162
end
163163

164164
expect(ClassRoom.represent({}).serializable_hash).to eq(
165-
parents: [
166-
{
167-
user: { in_first: 'value' },
168-
children: [
169-
{ user: { in_first: 'value', user_id: 'value', display_id: 'value' } },
170-
{ user: { in_first: 'value', user_id: 'value', display_id: 'value' } }
171-
]
172-
},
173-
{
174-
user: { in_first: 'value' },
175-
children: [
176-
{ user: { in_first: 'value', user_id: 'value', display_id: 'value' } },
177-
{ user: { in_first: 'value', user_id: 'value', display_id: 'value' } }
178-
]
179-
}
180-
]
165+
parents: [
166+
{
167+
user: { in_first: 'value' },
168+
children: [
169+
{ user: { in_first: 'value', user_id: 'value', display_id: 'value' } },
170+
{ user: { in_first: 'value', user_id: 'value', display_id: 'value' } }
171+
]
172+
},
173+
{
174+
user: { in_first: 'value' },
175+
children: [
176+
{ user: { in_first: 'value', user_id: 'value', display_id: 'value' } },
177+
{ user: { in_first: 'value', user_id: 'value', display_id: 'value' } }
178+
]
179+
}
180+
]
181181
)
182182
end
183183

@@ -259,7 +259,7 @@ class Parent < Person
259259
end
260260

261261
it 'formats an exposure with a :format_with lambda that returns a value from the entity instance' do
262-
object = Hash.new
262+
object = {}
263263

264264
subject.expose(:size, format_with: lambda { |_value| self.object.class.to_s })
265265
expect(subject.represent(object).send(:value_for, :size)).to eq object.class.to_s
@@ -270,7 +270,7 @@ class Parent < Person
270270
self.object.class.to_s
271271
end
272272

273-
object = Hash.new
273+
object = {}
274274

275275
subject.expose(:size, format_with: :size_formatter)
276276
expect(subject.represent(object).send(:value_for, :size)).to eq object.class.to_s
@@ -383,8 +383,8 @@ class Parent < Person
383383
end
384384

385385
expect(subject.exposures[:awesome_thing]).to eq(
386-
if: { awesome: false, less_awesome: true },
387-
if_extras: [:awesome, match_proc]
386+
if: { awesome: false, less_awesome: true },
387+
if_extras: [:awesome, match_proc]
388388
)
389389
end
390390

@@ -408,8 +408,8 @@ class Parent < Person
408408
end
409409

410410
expect(subject.exposures[:awesome_thing]).to eq(
411-
unless: { awesome: false, less_awesome: true },
412-
unless_extras: [:awesome, match_proc]
411+
unless: { awesome: false, less_awesome: true },
412+
unless_extras: [:awesome, match_proc]
413413
)
414414
end
415415

@@ -461,7 +461,7 @@ class Parent < Person
461461
end
462462

463463
it 'returns a single entity if called with a hash' do
464-
expect(subject.represent(Hash.new)).to be_kind_of(subject)
464+
expect(subject.represent({})).to be_kind_of(subject)
465465
end
466466

467467
it 'returns multiple entities if called with a collection' do
@@ -506,6 +506,47 @@ class Parent < Person
506506
subject.represent(Object.new, serializable: true)
507507
end.to raise_error(NoMethodError, /missing attribute `awesome'/)
508508
end
509+
510+
context 'with specified fields' do
511+
it 'returns only specified fields with only option' do
512+
subject.expose(:id, :name, :phone)
513+
representation = subject.represent(OpenStruct.new, only: [:id, :name], serializable: true)
514+
expect(representation).to eq(id: nil, name: nil)
515+
end
516+
517+
it 'can specify children attributes' do
518+
user_entity = Class.new(Grape::Entity)
519+
user_entity.expose(:id, :name, :email)
520+
521+
subject.expose(:id, :name, :phone)
522+
subject.expose(:user, using: user_entity)
523+
524+
representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: [:name, :email] }], serializable: true)
525+
expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
526+
end
527+
528+
context 'specify attribute with exposure condition' do
529+
it 'returns only specified fields' do
530+
subject.expose(:id, :name)
531+
subject.with_options(if: { condition: true }) do
532+
subject.expose(:name)
533+
end
534+
535+
representation = subject.represent(OpenStruct.new, condition: true, only: [:id, :name], serializable: true)
536+
expect(representation).to eq(id: nil, name: nil)
537+
end
538+
end
539+
540+
context 'attribute with alias' do
541+
it 'returns only specified fields' do
542+
subject.expose(:id)
543+
subject.expose(:name, as: :title)
544+
545+
representation = subject.represent(OpenStruct.new, condition: true, only: [:id, :title], serializable: true)
546+
expect(representation).to eq(id: nil, title: nil)
547+
end
548+
end
549+
end
509550
end
510551

511552
describe '.present_collection' do

0 commit comments

Comments
 (0)