Skip to content

Commit

Permalink
Add :except as an option to Adapter::Attributes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Empact authored and Noah Silas committed Feb 24, 2016
1 parent 532828b commit e4a5656
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 5 deletions.
13 changes: 13 additions & 0 deletions docs/general/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@ It could be combined, like above, with other paths in any combination desired.
render json: @posts, include: 'author.comments.**'
```

#### Excluded

Sometimes you want to omit a specific field or association during serialization.
You can use the `except` option for this:

```ruby
render json: @posts, include: '*', except: :author
```

This is particularly helpful if you are using the recursive include wildstar
(`**`), as it can lead to infinite recursion when you have associations that
can be traversed in a cycle.

##### Security Considerations

Since the included options may come from the query params (i.e. user-controller):
Expand Down
7 changes: 7 additions & 0 deletions docs/general/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ def blog
end
```

#### Association Options
- `key`: Sets the key name to assign in the serialized output for this
association
- `serializer`: Choose an explicit serializer to use for associated objects
- `except`: Select attributes or associations belonging to the associated
objects that should be omitted from serialization.

### Caching

#### ::cache
Expand Down
9 changes: 7 additions & 2 deletions lib/active_model/serializer/adapter/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ def serializable_hash_for_single_resource(options)

def resource_relationships(options)
relationships = {}
excepts = Array(options[:except])
serializer.associations(@include_tree).each do |association|
next if excepts.include?(association.key)
relationships[association.key] = relationship_value_for(association, options)
end

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

opts = instance_options.merge(include: @include_tree[association.key])
Attributes.new(association.serializer, opts).serializable_hash(options)
hash_opts = options.merge(except: association.options[:except])
Attributes.new(association.serializer, opts).serializable_hash(hash_opts)
end

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

cached_attributes(cached_serializer) do
cached_serializer.cache_check(self) do
serializer.attributes(options[:fields])
serializer.attributes(
only: options[:fields],
except: options[:except])
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/active_model/serializer/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def process_relationship(serializer, include_tree)
end

def attributes_for(serializer, fields)
serializer.attributes(fields).except(:id)
serializer.attributes(only: fields, except: :id)
end

def resource_object_for(serializer)
Expand Down
6 changes: 4 additions & 2 deletions lib/active_model/serializer/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ module Attributes

# Return the +attributes+ of +object+ as presented
# by the serializer.
def attributes(requested_attrs = nil, reload = false)
def attributes(options = {}, reload = false)
requested_attrs = options[:only]
excepts = Array(options[:except])
@attributes = nil if reload
@attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash|
next if attr.excluded?(self)
next if attr.excluded?(self) || excepts.include?(key)
next unless requested_attrs.nil? || requested_attrs.include?(key)
hash[key] = attr.value(self)
end
Expand Down
19 changes: 19 additions & 0 deletions test/action_controller/serialization_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ def render_fragment_changed_object_with_relationship

render json: like
end

def render_object_except
blog = Blog.new(id: 1, name: 'Blogariffic')
blog.articles = [
Post.new(id: 1, title: 'Hello', body: 'world'),
Post.new(id: 2, title: 'Moby Dick', body: 'Call me Ishmael.')
]
blog.writer = Author.new(id: 1, name: 'Joao Moura.')

render json: blog, except: [:articles, :name]
end
end

tests ImplicitSerializationTestController
Expand Down Expand Up @@ -463,6 +474,14 @@ def test_render_event_is_emmited

assert_equal 'render.active_model_serializers', @name
end

def test_render_object_except
get :render_object_except
assert_equal(
{ id: 1, writer: { id: 1, name: 'Joao Moura.' } }.to_json,
@response.body
)
end
end
end
end
41 changes: 41 additions & 0 deletions test/serializers/associations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,47 @@ def false

assert_equal(expected, hash)
end

def test_association_except
# `except` can take an array
comment_serializer = Class.new(ActiveModel::Serializer) do
attributes :id, :body
belongs_to :author, except: [:posts, :roles, :bio]
belongs_to :post
end

# `except` can take a symbol
post_serializer = Class.new(ActiveModel::Serializer) do
attributes :id, :title, :body
has_many :comments, except: :post, serializer: comment_serializer
end

# Circular dependency created:
# - post has_many comments
# - comment belongs_to post
# excluding the "post" association on comment resolves it when
# we are including nested associations

author = Author.new(id: 1, name: 'Alice')
post = Post.new(id: 7, title: 'Do work', body: 'work work work')
post.comments = [
Comment.new(post: post, author: author, id: 2, body: 'I agree'),
Comment.new(post: post, author: author, id: 3, body: 'Right')
]

hash = serializable(post, serializer: post_serializer, include: '**').serializable_hash

expected = {
id: 7,
title: 'Do work',
body: 'work work work',
comments: [
{ id: 2, body: 'I agree', author: { id: 1, name: 'Alice' } },
{ id: 3, body: 'Right', author: { id: 1, name: 'Alice' } }
]
}
assert_equal(expected, hash)
end
end
end
end
Expand Down

0 comments on commit e4a5656

Please sign in to comment.