Skip to content

Commit e4a5656

Browse files
EmpactNoah Silas
authored and
Noah Silas
committed
Add :except as an option to Adapter::Attributes
This supports passing `except:` as an option to `render` or as an option to `has_many`, etc. declrations. This is useful when avoiding circular includes. The `except` name was chosen to mirror behavior found in the 0.8x branch.
1 parent 532828b commit e4a5656

File tree

7 files changed

+92
-5
lines changed

7 files changed

+92
-5
lines changed

docs/general/adapters.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ It could be combined, like above, with other paths in any combination desired.
9797
render json: @posts, include: 'author.comments.**'
9898
```
9999

100+
#### Excluded
101+
102+
Sometimes you want to omit a specific field or association during serialization.
103+
You can use the `except` option for this:
104+
105+
```ruby
106+
render json: @posts, include: '*', except: :author
107+
```
108+
109+
This is particularly helpful if you are using the recursive include wildstar
110+
(`**`), as it can lead to infinite recursion when you have associations that
111+
can be traversed in a cycle.
112+
100113
##### Security Considerations
101114

102115
Since the included options may come from the query params (i.e. user-controller):

docs/general/serializers.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ def blog
7676
end
7777
```
7878

79+
#### Association Options
80+
- `key`: Sets the key name to assign in the serialized output for this
81+
association
82+
- `serializer`: Choose an explicit serializer to use for associated objects
83+
- `except`: Select attributes or associations belonging to the associated
84+
objects that should be omitted from serialization.
85+
7986
### Caching
8087

8188
#### ::cache

lib/active_model/serializer/adapter/attributes.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ def serializable_hash_for_single_resource(options)
6565

6666
def resource_relationships(options)
6767
relationships = {}
68+
excepts = Array(options[:except])
6869
serializer.associations(@include_tree).each do |association|
70+
next if excepts.include?(association.key)
6971
relationships[association.key] = relationship_value_for(association, options)
7072
end
7173

@@ -77,7 +79,8 @@ def relationship_value_for(association, options)
7779
return unless association.serializer && association.serializer.object
7880

7981
opts = instance_options.merge(include: @include_tree[association.key])
80-
Attributes.new(association.serializer, opts).serializable_hash(options)
82+
hash_opts = options.merge(except: association.options[:except])
83+
Attributes.new(association.serializer, opts).serializable_hash(hash_opts)
8184
end
8285

8386
# no-op: Attributes adapter does not include meta data, because it does not support root.
@@ -90,7 +93,9 @@ def resource_object_for(options)
9093

9194
cached_attributes(cached_serializer) do
9295
cached_serializer.cache_check(self) do
93-
serializer.attributes(options[:fields])
96+
serializer.attributes(
97+
only: options[:fields],
98+
except: options[:except])
9499
end
95100
end
96101
end

lib/active_model/serializer/adapter/json_api.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def process_relationship(serializer, include_tree)
130130
end
131131

132132
def attributes_for(serializer, fields)
133-
serializer.attributes(fields).except(:id)
133+
serializer.attributes(only: fields, except: :id)
134134
end
135135

136136
def resource_object_for(serializer)

lib/active_model/serializer/attributes.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ module Attributes
1414

1515
# Return the +attributes+ of +object+ as presented
1616
# by the serializer.
17-
def attributes(requested_attrs = nil, reload = false)
17+
def attributes(options = {}, reload = false)
18+
requested_attrs = options[:only]
19+
excepts = Array(options[:except])
1820
@attributes = nil if reload
1921
@attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash|
20-
next if attr.excluded?(self)
22+
next if attr.excluded?(self) || excepts.include?(key)
2123
next unless requested_attrs.nil? || requested_attrs.include?(key)
2224
hash[key] = attr.value(self)
2325
end

test/action_controller/serialization_test.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ def render_fragment_changed_object_with_relationship
141141

142142
render json: like
143143
end
144+
145+
def render_object_except
146+
blog = Blog.new(id: 1, name: 'Blogariffic')
147+
blog.articles = [
148+
Post.new(id: 1, title: 'Hello', body: 'world'),
149+
Post.new(id: 2, title: 'Moby Dick', body: 'Call me Ishmael.')
150+
]
151+
blog.writer = Author.new(id: 1, name: 'Joao Moura.')
152+
153+
render json: blog, except: [:articles, :name]
154+
end
144155
end
145156

146157
tests ImplicitSerializationTestController
@@ -463,6 +474,14 @@ def test_render_event_is_emmited
463474

464475
assert_equal 'render.active_model_serializers', @name
465476
end
477+
478+
def test_render_object_except
479+
get :render_object_except
480+
assert_equal(
481+
{ id: 1, writer: { id: 1, name: 'Joao Moura.' } }.to_json,
482+
@response.body
483+
)
484+
end
466485
end
467486
end
468487
end

test/serializers/associations_test.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,47 @@ def false
261261

262262
assert_equal(expected, hash)
263263
end
264+
265+
def test_association_except
266+
# `except` can take an array
267+
comment_serializer = Class.new(ActiveModel::Serializer) do
268+
attributes :id, :body
269+
belongs_to :author, except: [:posts, :roles, :bio]
270+
belongs_to :post
271+
end
272+
273+
# `except` can take a symbol
274+
post_serializer = Class.new(ActiveModel::Serializer) do
275+
attributes :id, :title, :body
276+
has_many :comments, except: :post, serializer: comment_serializer
277+
end
278+
279+
# Circular dependency created:
280+
# - post has_many comments
281+
# - comment belongs_to post
282+
# excluding the "post" association on comment resolves it when
283+
# we are including nested associations
284+
285+
author = Author.new(id: 1, name: 'Alice')
286+
post = Post.new(id: 7, title: 'Do work', body: 'work work work')
287+
post.comments = [
288+
Comment.new(post: post, author: author, id: 2, body: 'I agree'),
289+
Comment.new(post: post, author: author, id: 3, body: 'Right')
290+
]
291+
292+
hash = serializable(post, serializer: post_serializer, include: '**').serializable_hash
293+
294+
expected = {
295+
id: 7,
296+
title: 'Do work',
297+
body: 'work work work',
298+
comments: [
299+
{ id: 2, body: 'I agree', author: { id: 1, name: 'Alice' } },
300+
{ id: 3, body: 'Right', author: { id: 1, name: 'Alice' } }
301+
]
302+
}
303+
assert_equal(expected, hash)
304+
end
264305
end
265306
end
266307
end

0 commit comments

Comments
 (0)