diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..213fbaf1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +rvm: + - 1.8.7 + - 1.9.2 + - 1.9.3 + - ree + - jruby + - rbx \ No newline at end of file diff --git a/README.textile b/README.textile index 2b7b2acd..5750cae7 100644 --- a/README.textile +++ b/README.textile @@ -1,3 +1,6 @@ +"!https://secure.travis-ci.org/josevalim/active_model_serializers.png!":http://travis-ci.org/josevalim/active_model_serializers + + h2. Rails Serializers This guide describes how to use Active Model serializers to build non-trivial JSON services in Rails. By reading this guide, you will learn: @@ -193,10 +196,6 @@ JSON. In the above example, the +title+ and +body+ attributes were always includ class PostSerializer < ActiveModel::Serializer attributes :title, :body - def initialize(post, scope) - @post, @scope = post, scope - end - def serializable_hash hash = attributes hash.merge!(super_data) if super? @@ -355,7 +354,7 @@ h4. Modifying Associations You can also rename associations if required. Say for example you have an association that makes sense to be named one thing in your code, but another when data is serialized. -You can use the option to specify a different name for an association. +You can use the option to specify a different name for an association. Here is an exmaple:
diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb
index 97d1cd51..6f21512d 100644
--- a/lib/action_controller/serialization.rb
+++ b/lib/action_controller/serialization.rb
@@ -29,6 +29,7 @@ module ActionController
 
     included do
       class_attribute :_serialization_scope
+      self._serialization_scope = :current_user
     end
 
     def serialization_scope
@@ -37,7 +38,7 @@ module ActionController
 
     def _render_option_json(json, options)
       if json.respond_to?(:active_model_serializer) && (serializer = json.active_model_serializer)
-        json = serializer.new(json, serialization_scope)
+        json = serializer.new(json, serialization_scope, options)
       end
       super
     end
diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb
index f8e265e3..0a365820 100644
--- a/lib/active_model/serializer.rb
+++ b/lib/active_model/serializer.rb
@@ -11,14 +11,15 @@ module ActiveModel
   class ArraySerializer
     attr_reader :object, :scope
 
-    def initialize(object, scope)
-      @object, @scope = object, scope
+    def initialize(object, scope, options={})
+      @object, @scope, @options = object, scope, options
+      @hash = options[:hash]
     end
 
     def serializable_array
       @object.map do |item|
         if item.respond_to?(:active_model_serializer) && (serializer = item.active_model_serializer)
-          serializer.new(item, scope)
+          serializer.new(item, scope, :hash => @hash)
         else
           item
         end
@@ -26,7 +27,14 @@ module ActiveModel
     end
 
     def as_json(*args)
-      serializable_array.as_json(*args)
+      @hash = {}
+      array = serializable_array.as_json(*args)
+
+      if root = @options[:root]
+        @hash.merge!(root => array)
+      else
+        array
+      end
     end
   end
 
@@ -79,9 +87,9 @@ module ActiveModel
       end
 
       class HasMany < Config #:nodoc:
-        def serialize(collection, scope)
+        def serialize(collection, scope, options)
           collection.map do |item|
-            serializer.new(item, scope).serializable_hash
+            serializer.new(item, scope, options).serializable_hash
           end
         end
 
@@ -96,18 +104,18 @@ module ActiveModel
       end
 
       class HasOne < Config #:nodoc:
-        def serialize(object, scope)
+        def serialize(object, scope, options)
           return unless object
 
           if polymorphic?
             polymorphic_type = object.class.to_s.demodulize
             serializer_class = "#{object.class.to_s}Serializer".constantize
 
-            serializer_class.new(object, scope).serializable_hash.merge({
+            serializer_class.new(object, scope, options).serializable_hash.merge({
               "#{name}_type".to_sym => polymorphic_type
             })
           else
-            serializer.new(object, scope).serializable_hash
+            serializer.new(object, scope, options).serializable_hash
           end
         end
 
@@ -222,8 +230,8 @@ module ActiveModel
         columns = klass.columns_hash
 
         attrs = _attributes.inject({}) do |hash, (name,key)|
-          column = columns[name]
-          hash.merge key => column[:type]
+          column = columns[name.to_s]
+          hash.merge key => column.type
         end
 
         associations = _associations.inject({}) do |hash, association|
@@ -236,7 +244,7 @@ module ActiveModel
 
       # The model class associated with this serializer.
       def model_class
-        name.sub(/Serializer$/, '')
+        name.sub(/Serializer$/, '').constantize
       end
 
       # Define how associations should be embedded.
@@ -269,19 +277,20 @@ module ActiveModel
 
     attr_reader :object, :scope
 
-    def initialize(object, scope)
-      @object, @scope = object, scope
+    def initialize(object, scope, options={})
+      @object, @scope, @options = object, scope, options
+      @hash = options[:hash]
     end
 
     # Returns a json representation of the serializable
     # object including the root.
     def as_json(*)
-      if _root
-        hash = { _root => serializable_hash }
-        hash.merge!(associations) if _root_embed
+      if root = @options[:root] || _root
+        @hash = hash = {}
+        hash.merge!(root => serializable_hash)
         hash
       else
-        serializable_hash
+        @hash = serializable_hash
       end
     end
 
@@ -289,6 +298,7 @@ module ActiveModel
     # object without the root.
     def serializable_hash
       if _embed == :ids
+        merge_associations(@hash, associations) if _root_embed
         attributes.merge(association_ids)
       elsif _embed == :objects
         attributes.merge(associations)
@@ -297,6 +307,16 @@ module ActiveModel
       end
     end
 
+    def merge_associations(hash, associations)
+      associations.each do |key, value|
+        if hash[key]
+          hash[key] |= value
+        elsif value
+          hash[key] = value
+        end
+      end
+    end
+
     # Returns a hash representation of the serializable
     # object associations.
     def associations
@@ -304,10 +324,7 @@ module ActiveModel
 
       _associations.each do |association|
         associated_object = send(association.name)
-        hash[association.key] = association.serialize(associated_object, scope)
-
-        if association.polymorphic? && associated_object
-        end
+        hash[association.key] = association.serialize(associated_object, scope, :hash => @hash)
       end
 
       hash
diff --git a/test/serialization_test.rb b/test/serialization_test.rb
index 05abd862..b973f227 100644
--- a/test/serialization_test.rb
+++ b/test/serialization_test.rb
@@ -15,12 +15,14 @@ class RenderJsonTest < ActionController::TestCase
   end
 
   class JsonSerializer
-    def initialize(object, scope)
-      @object, @scope = object, scope
+    def initialize(object, scope, options={})
+      @object, @scope, @options = object, scope, options
     end
 
     def as_json(*)
-      { :object => @object.as_json, :scope => @scope.as_json }
+      hash = { :object => @object.as_json, :scope => @scope.as_json }
+      hash.merge!(:options => true) if @options[:options]
+      hash
     end
   end
 
@@ -89,6 +91,11 @@ class RenderJsonTest < ActionController::TestCase
       render :json => JsonSerializable.new
     end
 
+    def render_json_with_serializer_and_options
+      @current_user = Struct.new(:as_json).new(:current_user => true)
+      render :json => JsonSerializable.new, :options => true
+    end
+
     def render_json_with_serializer_api_but_without_serializer
       @current_user = Struct.new(:as_json).new(:current_user => true)
       render :json => JsonSerializable.new(true)
@@ -165,6 +172,13 @@ class RenderJsonTest < ActionController::TestCase
     assert_match '"object":{"serializable_object":true}', @response.body
   end
 
+  def test_render_json_with_serializer_and_options
+    get :render_json_with_serializer_and_options
+    assert_match '"scope":{"current_user":true}', @response.body
+    assert_match '"object":{"serializable_object":true}', @response.body
+    assert_match '"options":true', @response.body
+  end
+
   def test_render_json_with_serializer_api_but_without_serializer
     get :render_json_with_serializer_api_but_without_serializer
     assert_match '{"serializable_object":true}', @response.body
diff --git a/test/serializer_test.rb b/test/serializer_test.rb
index 99b030e2..1f5e6ef6 100644
--- a/test/serializer_test.rb
+++ b/test/serializer_test.rb
@@ -70,7 +70,7 @@ class SerializerTest < ActiveModel::TestCase
   end
 
   class CommentSerializer
-    def initialize(comment, scope)
+    def initialize(comment, scope, options={})
       @comment, @scope = comment, scope
     end
 
@@ -557,7 +557,7 @@ class SerializerTest < ActiveModel::TestCase
     Class.new do
       class << self
         def columns_hash
-          { :name => { :type => :string }, :age => { :type => :integer } }
+          { "name" => Struct.new(:type).new(:string), "age" => Struct.new(:type).new(:integer) }
         end
 
         def reflect_on_association(name)
@@ -734,4 +734,133 @@ class SerializerTest < ActiveModel::TestCase
       }
     }, serializer.as_json)
   end
+
+  def test_root_provided_in_options
+    author_serializer = Class.new(ActiveModel::Serializer) do
+      attributes :id, :name
+    end
+
+    serializer_class = Class.new(ActiveModel::Serializer) do
+      root :post
+
+      attributes :title, :body
+      has_one :author, :serializer => author_serializer
+    end
+
+    post_class = Class.new(Model) do
+      attr_accessor :author
+    end
+
+    author_class = Class.new(Model)
+
+    post = post_class.new(:title => "New Post", :body => "It's a new post!")
+    author = author_class.new(:id => 5, :name => "Tom Dale")
+    post.author = author
+
+    hash = serializer_class.new(post, nil, :root => :blog_post)
+
+    assert_equal({
+      :blog_post => {
+        :title => "New Post",
+        :body => "It's a new post!",
+        :author => { :id => 5, :name => "Tom Dale" }
+      }
+    }, hash.as_json)
+  end
+
+  def test_serializer_has_access_to_root_object
+    hash_object = nil
+
+    author_serializer = Class.new(ActiveModel::Serializer) do
+      attributes :id, :name
+
+      define_method :serializable_hash do
+        hash_object = @hash
+        super()
+      end
+    end
+
+    serializer_class = Class.new(ActiveModel::Serializer) do
+      root :post
+
+      attributes :title, :body
+      has_one :author, :serializer => author_serializer
+    end
+
+    post_class = Class.new(Model) do
+      attr_accessor :author
+    end
+
+    author_class = Class.new(Model)
+
+    post = post_class.new(:title => "New Post", :body => "It's a new post!")
+    author = author_class.new(:id => 5, :name => "Tom Dale")
+    post.author = author
+
+    expected = serializer_class.new(post, nil).as_json
+    assert_equal expected, hash_object
+  end
+
+  # the point of this test is to illustrate that deeply nested serializers
+  # still side-load at the root.
+  def test_embed_with_include_inserts_at_root
+    tag_serializer = Class.new(ActiveModel::Serializer) do
+      attributes :id, :name
+    end
+
+    comment_serializer = Class.new(ActiveModel::Serializer) do
+      embed :ids, :include => true
+      attributes :id, :body
+      has_many :tags, :serializer => tag_serializer
+    end
+
+    post_serializer = Class.new(ActiveModel::Serializer) do
+      embed :ids, :include => true
+      attributes :id, :title, :body
+      has_many :comments, :serializer => comment_serializer
+    end
+
+    post_class = Class.new(Model) do
+      attr_accessor :comments
+
+      define_method :active_model_serializer do
+        post_serializer
+      end
+    end
+
+    comment_class = Class.new(Model) do
+      attr_accessor :tags
+    end
+
+    tag_class = Class.new(Model)
+
+    post = post_class.new(:title => "New Post", :body => "NEW POST", :id => 1)
+    comment1 = comment_class.new(:body => "EWOT", :id => 1)
+    comment2 = comment_class.new(:body => "YARLY", :id => 2)
+    tag1 = tag_class.new(:name => "lolcat", :id => 1)
+    tag2 = tag_class.new(:name => "nyancat", :id => 2)
+    tag3 = tag_class.new(:name => "violetcat", :id => 3)
+
+    post.comments = [comment1, comment2]
+    comment1.tags = [tag1, tag3]
+    comment2.tags = [tag1, tag2]
+
+    actual = ActiveModel::ArraySerializer.new([post], nil, :root => :posts).as_json
+    assert_equal({
+      :posts => [
+        { :title => "New Post", :body => "NEW POST", :id => 1, :comments => [1,2] }
+      ],
+
+      :comments => [
+        { :body => "EWOT", :id => 1, :tags => [1,3] },
+        { :body => "YARLY", :id => 2, :tags => [1,2] }
+      ],
+
+      :tags => [
+        { :name => "lolcat", :id => 1 },
+        { :name => "violetcat", :id => 3 },
+        { :name => "nyancat", :id => 2 }
+      ]
+    }, actual)
+  end
 end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 2942aa58..97ad9f52 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,7 +1,5 @@
 require "rubygems"
-require "bundler"
-
-Bundler.setup
+require "bundler/setup"
 
 require "active_model_serializers"
 require "active_support/json"