From 68dc57eb734bce1bf03ef6bff11322c249ef2738 Mon Sep 17 00:00:00 2001 From: Dan Gebhardt Date: Wed, 29 Aug 2012 07:42:23 -0400 Subject: [PATCH 1/3] simplified the API for `include_associations!()` to make conditional includes cleaner --- lib/active_model/serializer.rb | 41 ++++++++++--------------- test/serializer_test.rb | 55 ++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index cd743e4c..e1cb7c6a 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -460,8 +460,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,33 +476,17 @@ 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 - end - end - - def included_association?(name) - if options.key?(:include) - options[:include].include?(name) - elsif options.key?(:exclude) - !options[:exclude].include?(name) - else - true + def include_associations! + _associations.each_key do |name| + include! name end end @@ -526,9 +509,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 diff --git a/test/serializer_test.rb b/test/serializer_test.rb index 8358178d..fc4ec7dc 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -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,48 @@ 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 Blog < Model attr_accessor :author end From 42221a6140ae3d7d731914a2fba6008a181179cf Mon Sep 17 00:00:00 2001 From: Dan Gebhardt Date: Wed, 29 Aug 2012 09:26:41 -0400 Subject: [PATCH 2/3] define `include_XXX?` methods, which can be overridden to conditionally include attributes and associations --- lib/active_model/serializer.rb | 21 +++++++++++-- test/serializer_test.rb | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index e1cb7c6a..ab60349b 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -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 @@ -486,10 +499,14 @@ module ActiveModel def include_associations! _associations.each_key do |name| - include! name + include!(name) if include?(name) end end + def include?(name) + send "include_#{name}?".to_sym + end + def include!(name, options={}) # Make sure that if a special options[:hash] was passed in, we generate # a new unique values hash and don't clobber the original. If the hash @@ -569,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 diff --git a/test/serializer_test.rb b/test/serializer_test.rb index fc4ec7dc..941d371c 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -227,6 +227,62 @@ class SerializerTest < ActiveModel::TestCase }, 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 From 7fc8606101ad6a5a76e4937bbae7429e86b0e6c0 Mon Sep 17 00:00:00 2001 From: Dan Gebhardt Date: Wed, 29 Aug 2012 10:15:51 -0400 Subject: [PATCH 3/3] documentation of conditional include options for serializers --- README.md | 123 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 90 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index aeb19a39..5bf77e3a 100644 --- a/README.md +++ b/README.md @@ -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 +```