From 4860143ee4ccafef474f14f40b8f70c2b6b54656 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Mon, 21 Feb 2011 22:55:49 -0800 Subject: ActiveModel support for the :include serialization option This commit moves support for the :include serialization option for serializing associated objects out of ActiveRecord in into ActiveModel. The following methods support the :include option: * serializable_hash * to_json * to_xml Instances must respond to methods named by the values of the :includes array (or keys of the :includes hash). If an association method returns an object that is_a?(Enumerable) (which AR has_many associations do), it is assumed to be a collection association, and its elements must respond to :serializable_hash. Otherwise it must respond to :serializable_hash itself. While here, fix #858, XmlSerializer should not singularize already singular association names. --- activemodel/lib/active_model/serialization.rb | 33 +++++++++- activemodel/lib/active_model/serializers/xml.rb | 40 ++++++++++++ activemodel/test/cases/serialization_test.rb | 73 ++++++++++++++++++++-- .../cases/serializers/xml_serialization_test.rb | 52 ++++++++++++++- 4 files changed, 189 insertions(+), 9 deletions(-) (limited to 'activemodel') diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 0b4067257e..9260c5082d 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -77,7 +77,38 @@ module ActiveModel end method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) } - Hash[(attribute_names + method_names).map { |n| [n, send(n)] }] + hash = Hash[(attribute_names + method_names).map { |n| [n, send(n)] }] + + serializable_add_includes(options) do |association, records, opts| + hash[association] = if records.is_a?(Enumerable) + records.map { |a| a.serializable_hash(opts) } + else + records.serializable_hash(opts) + end + end + + hash end + + private + # Add associations specified via the :include option. + # + # Expects a block that takes as arguments: + # +association+ - name of the association + # +records+ - the association record(s) to be serialized + # +opts+ - options for the association records + def serializable_add_includes(options = {}) + return unless include = options[:include] + + unless include.is_a?(Hash) + include = Hash[Array.wrap(include).map { |n| [n, {}] }] + end + + include.each do |association, opts| + if records = send(association) + yield association, records, opts + end + end + end end end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 9812af43d6..64dda3bcee 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -101,6 +101,7 @@ module ActiveModel @builder.tag!(*args) do add_attributes_and_methods + add_includes add_extra_behavior add_procs yield @builder if block_given? @@ -120,6 +121,45 @@ module ActiveModel end end + def add_includes + @serializable.send(:serializable_add_includes, options) do |association, records, opts| + add_associations(association, records, opts) + end + end + + # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. + def add_associations(association, records, opts) + merged_options = opts.merge(options.slice(:builder, :indent)) + merged_options[:skip_instruct] = true + + if records.is_a?(Enumerable) + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) + type = options[:skip_types] ? { } : {:type => "array"} + association_name = association.to_s.singularize + merged_options[:root] = association_name + + if records.empty? + @builder.tag!(tag, type) + else + @builder.tag!(tag, type) do + records.each do |record| + if options[:skip_types] + record_type = {} + else + record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name + record_type = {:type => record_class} + end + + record.to_xml merged_options.merge(record_type) + end + end + end + else + merged_options[:root] = association.to_s + records.to_xml(merged_options) + end + end + def add_procs if procs = options.delete(:procs) Array.wrap(procs).each do |proc| diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 10cb600e7b..5122f08eec 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -1,13 +1,19 @@ require "cases/helper" +require 'active_support/core_ext/object/instance_variables' class SerializationTest < ActiveModel::TestCase class User include ActiveModel::Serialization - attr_accessor :name, :email, :gender + attr_accessor :name, :email, :gender, :address, :friends + + def initialize(name, email, gender) + @name, @email, @gender = name, email, gender + @friends = [] + end def attributes - @attributes ||= {'name' => 'nil', 'email' => 'nil', 'gender' => 'nil'} + instance_values.except("address", "friends") end def foo @@ -15,11 +21,25 @@ class SerializationTest < ActiveModel::TestCase end end + class Address + include ActiveModel::Serialization + + attr_accessor :street, :city, :state, :zip + + def attributes + instance_values + end + end + setup do - @user = User.new - @user.name = 'David' - @user.email = 'david@example.com' - @user.gender = 'male' + @user = User.new('David', 'david@example.com', 'male') + @user.address = Address.new + @user.address.street = "123 Lane" + @user.address.city = "Springfield" + @user.address.state = "CA" + @user.address.zip = 11111 + @user.friends = [User.new('Joe', 'joe@example.com', 'male'), + User.new('Sue', 'sue@example.com', 'female')] end def test_method_serializable_hash_should_work @@ -57,4 +77,45 @@ class SerializationTest < ActiveModel::TestCase assert_equal expected , @user.serializable_hash(:methods => [:bar]) end + def test_include_option_with_singular_association + expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com", + :address=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}} + assert_equal expected , @user.serializable_hash(:include => :address) + end + + def test_include_option_with_plural_association + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} + assert_equal expected , @user.serializable_hash(:include => :friends) + end + + def test_include_option_with_empty_association + @user.friends = [] + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", :friends=>[]} + assert_equal expected , @user.serializable_hash(:include => :friends) + end + + def test_multiple_includes + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + :address=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}, + :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]} + assert_equal expected , @user.serializable_hash(:include => [:address, :friends]) + end + + def test_include_with_options + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + :address=>{"street"=>"123 Lane"}} + assert_equal expected , @user.serializable_hash(:include => {:address => {:only => "street"}}) + end + + def test_nested_include + @user.friends.first.friends = [@user] + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male', + :friends => ["email"=>"david@example.com", "gender"=>"male", "name"=>"David"]}, + {"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', :friends => []}]} + assert_equal expected , @user.serializable_hash(:include => {:friends => {:include => :friends}}) + end end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index f978191d22..a38ef8e223 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -7,9 +7,11 @@ class Contact extend ActiveModel::Naming include ActiveModel::Serializers::Xml + attr_accessor :address, :friends + def attributes - instance_values - end unless method_defined?(:attributes) + instance_values.except("address", "friends") + end end module Admin @@ -20,6 +22,17 @@ end class Customer < Struct.new(:name) end +class Address + extend ActiveModel::Naming + include ActiveModel::Serializers::Xml + + attr_accessor :street, :city, :state, :zip + + def attributes + instance_values + end +end + class XmlSerializationTest < ActiveModel::TestCase def setup @contact = Contact.new @@ -30,6 +43,12 @@ class XmlSerializationTest < ActiveModel::TestCase customer = Customer.new customer.name = "John" @contact.preferences = customer + @contact.address = Address.new + @contact.address.street = "123 Lane" + @contact.address.city = "Springfield" + @contact.address.state = "CA" + @contact.address.zip = 11111 + @contact.friends = [Contact.new, Contact.new] end test "should serialize default root" do @@ -138,4 +157,33 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{}, xml assert_match %r{aaron stack}, xml end + + test "include option with singular association" do + xml = @contact.to_xml :include => :address, :indent => 0 + assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true)) + end + + test "include option with plural association" do + xml = @contact.to_xml :include => :friends, :indent => 0 + assert_match %r{}, xml + assert_match %r{}, xml + end + + test "multiple includes" do + xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => [ :address, :friends ] + assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true)) + assert_match %r{}, xml + assert_match %r{}, xml + end + + test "include with options" do + xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => { :address => { :only => :city } } + assert xml.include?(%(>
Springfield
)) + end + + test "propagates skip_types option to included associations" do + xml = @contact.to_xml :include => :friends, :indent => 0, :skip_types => true + assert_match %r{}, xml + assert_match %r{}, xml + end end -- cgit v1.2.3