Merge pull request #123 from dgeb/conditional-includes

Conditional includes (closes #111)
This commit is contained in:
Yehuda Katz 2012-09-02 21:56:38 -07:00
commit a21529370c
3 changed files with 227 additions and 63 deletions

123
README.md
View File

@ -187,11 +187,49 @@ end
## Attributes
For specified attributes, the serializer will look up the attribute on the
For specified attributes, a serializer will look up the attribute on the
object you passed to `render :json`. It uses
`read_attribute_for_serialization`, which `ActiveRecord` objects implement as a
regular attribute lookup.
Before looking up the attribute on the object, a serializer will check for the
presence of a method with the name of the attribute. This allows serializers to
include properties beyond the simple attributes of the model. For example:
```ruby
class PersonSerializer < ActiveModel::Serializer
attributes :first_name, :last_name, :full_name
def full_name
"#{object.first_name} #{object.last_name}"
end
end
```
Within a serializer's methods, you can access the object being
serialized as either `object` or the name of the serialized object
(e.g. `admin_comment` for the `AdminCommentSerializer`).
You can also access the `scope` method, which provides an
authorization context to your serializer. By default, scope
is the current user of your application, but this
[can be customized](#customizing-scope).
Serializers will check for the presence of a method named
`include_[ATTRIBUTE]?` to determine whether a particular attribute should be
included in the output. This is typically used to customize output
based on `scope`. For example:
```ruby
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :author
def include_author?
scope.admin?
end
end
```
If you would like the key in the outputted JSON to be different from its name
in ActiveRecord, you can use the `:key` option to customize it:
@ -205,45 +243,24 @@ class PostSerializer < ActiveModel::Serializer
end
```
## Custom Attributes
If you would like customize your JSON to include things beyond the simple
attributes of the model, you can override its `attributes` method
to return anything you need.
The most common scenario to use this feature is when an attribute
depends on a serialization scope. By default, the current user of your
application will be available in your serializer under the method
`scope`. This allows you to check for permissions before adding
an attribute. For example:
If you would like direct, low-level control of attribute serialization, you can
completely override the `attributes` method to return the hash you need:
```ruby
class Person < ActiveRecord::Base
def full_name
"#{first_name} #{last_name}"
end
end
class PersonSerializer < ActiveModel::Serializer
attributes :first_name, :last_name
def attributes
hash = super
hash["full_name"] = object.full_name if scope.admin?
if scope.admin?
hash["ssn"] = object.ssn
hash["secret"] = object.mothers_maiden_name
end
hash
end
end
```
The serialization scope can be customized in your controller by
calling `serialization_scope`:
```ruby
class ApplicationController < ActionController::Base
serialization_scope :current_admin
end
```
## Associations
For specified associations, the serializer will look up the association and
@ -268,11 +285,7 @@ class PostSerializer < ActiveModel::Serializer
end
```
In a serializer, `scope` is the current authorization scope (usually
`current_user`), which the controller gives to the serializer when you call
`render :json`
As with attributes, you can also change the JSON key that the serializer should
As with attributes, you can change the JSON key that the serializer should
use for a particular association.
```ruby
@ -284,6 +297,37 @@ class PostSerializer < ActiveModel::Serializer
end
```
Also, as with attributes, serializers will check for the presence
of a method named `include_[ASSOCIATION]?` to determine whether a particular association
should be included in the output. For example:
```ruby
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_many :comments
def include_comments?
!post.comments_disabled?
end
end
```
If you would like lower-level control of association serialization, you can
override `include_associations!` to specify which associations should be included:
```ruby
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
has_one :author
has_many :comments
def include_associations!
include! :author if scope.admin?
include! :comments unless object.comments_disabled?
end
end
```
## Embedding Associations
By default, associations will be embedded inside the serialized object. So if
@ -405,3 +449,16 @@ data looking for information, is extremely useful.
If you are mostly working with the data in simple scenarios and manually making
Ajax requests, you probably just want to use the default embedded behavior.
## Customizing Scope
In a serializer, `scope` is the current authorization scope which the controller
provides to the serializer when you call `render :json`. By default, this is
`current_user`, but can be customized in your controller by calling
`serialization_scope`:
```ruby
class ApplicationController < ActionController::Base
serialization_scope :current_admin
end
```

View File

@ -328,6 +328,8 @@ module ActiveModel
object.read_attribute_for_serialization(attr.to_sym)
end
end
define_include_method attr
end
def associate(klass, attrs) #:nodoc:
@ -341,10 +343,21 @@ module ActiveModel
end
end
define_include_method attr
self._associations[attr] = klass.refine(attr, options)
end
end
def define_include_method(name)
method = "include_#{name}?".to_sym
unless method_defined?(method)
define_method method do
true
end
end
end
# Defines an association in the object should be rendered.
#
# The serializer object should implement the association name
@ -460,8 +473,7 @@ module ActiveModel
# Returns a json representation of the serializable
# object including the root.
def as_json(options=nil)
options ||= {}
def as_json(options={})
if root = options.fetch(:root, @options.fetch(:root, _root))
@options[:hash] = hash = {}
@options[:unique_values] = {}
@ -477,34 +489,22 @@ module ActiveModel
# object without the root.
def serializable_hash
instrument(:serialize, :serializer => self.class.name) do
node = attributes
@node = attributes
instrument :associations do
include_associations!(node) if _embed
include_associations! if _embed
end
node
@node
end
end
def include_associations!(node)
_associations.each do |attr, klass|
opts = { :node => node }
if options.include?(:include) || options.include?(:exclude)
opts[:include] = included_association?(attr)
end
include! attr, opts
def include_associations!
_associations.each_key do |name|
include!(name) if include?(name)
end
end
def included_association?(name)
if options.key?(:include)
options[:include].include?(name)
elsif options.key?(:exclude)
!options[:exclude].include?(name)
else
true
end
def include?(name)
send "include_#{name}?".to_sym
end
def include!(name, options={})
@ -526,9 +526,17 @@ module ActiveModel
@options[:unique_values] ||= {}
end
node = options[:node]
node = options[:node] ||= @node
value = options[:value]
if options[:include] == nil
if @options.key?(:include)
options[:include] = @options[:include].include?(name)
elsif @options.include?(:exclude)
options[:include] = !@options[:exclude].include?(name)
end
end
association_class =
if klass = _associations[name]
klass
@ -578,7 +586,7 @@ module ActiveModel
hash = {}
_attributes.each do |name,key|
hash[key] = read_attribute_for_serialization(name)
hash[key] = read_attribute_for_serialization(name) if include?(name)
end
hash

View File

@ -37,10 +37,11 @@ class SerializerTest < ActiveModel::TestCase
def initialize(attributes)
super(attributes)
self.comments ||= []
self.comments_disabled = false
self.author = nil
end
attr_accessor :comments, :author
attr_accessor :comments, :comments_disabled, :author
def active_model_serializer; PostSerializer; end
end
@ -89,11 +90,6 @@ class SerializerTest < ActiveModel::TestCase
end
end
class PostSerializer < ActiveModel::Serializer
attributes :title, :body
has_many :comments, :serializer => CommentSerializer
end
def test_scope_works_correct
serializer = ActiveModel::Serializer.new :foo, :scope => :bar
assert_equal serializer.scope, :bar
@ -163,6 +159,11 @@ class SerializerTest < ActiveModel::TestCase
}, hash)
end
class PostSerializer < ActiveModel::Serializer
attributes :title, :body
has_many :comments, :serializer => CommentSerializer
end
def test_has_many
user = User.new
@ -184,6 +185,104 @@ class SerializerTest < ActiveModel::TestCase
}, post_serializer.as_json)
end
class PostWithConditionalCommentsSerializer < ActiveModel::Serializer
root :post
attributes :title, :body
has_many :comments, :serializer => CommentSerializer
def include_associations!
include! :comments unless object.comments_disabled
end
end
def test_conditionally_included_associations
user = User.new
post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
comments = [Comment.new(:title => "Comment1"), Comment.new(:title => "Comment2")]
post.comments = comments
post_serializer = PostWithConditionalCommentsSerializer.new(post, :scope => user)
# comments enabled
post.comments_disabled = false
assert_equal({
:post => {
:title => "New Post",
:body => "Body of new post",
:comments => [
{ :title => "Comment1" },
{ :title => "Comment2" }
]
}
}, post_serializer.as_json)
# comments disabled
post.comments_disabled = true
assert_equal({
:post => {
:title => "New Post",
:body => "Body of new post"
}
}, post_serializer.as_json)
end
class PostWithMultipleConditionalsSerializer < ActiveModel::Serializer
root :post
attributes :title, :body, :author
has_many :comments, :serializer => CommentSerializer
def include_comments?
!object.comments_disabled
end
def include_author?
scope.super_user?
end
end
def test_conditionally_included_associations_and_attributes
user = User.new
post = Post.new(:title => "New Post", :body => "Body of new post", :author => 'Sausage King', :email => "tenderlove@tenderlove.com")
comments = [Comment.new(:title => "Comment1"), Comment.new(:title => "Comment2")]
post.comments = comments
post_serializer = PostWithMultipleConditionalsSerializer.new(post, :scope => user)
# comments enabled
post.comments_disabled = false
assert_equal({
:post => {
:title => "New Post",
:body => "Body of new post",
:comments => [
{ :title => "Comment1" },
{ :title => "Comment2" }
]
}
}, post_serializer.as_json)
# comments disabled
post.comments_disabled = true
assert_equal({
:post => {
:title => "New Post",
:body => "Body of new post"
}
}, post_serializer.as_json)
# superuser - should see author
user.superuser = true
assert_equal({
:post => {
:title => "New Post",
:body => "Body of new post",
:author => "Sausage King"
}
}, post_serializer.as_json)
end
class Blog < Model
attr_accessor :author
end