Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

to_xml fixes, features, and speedup. Closes #4989.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4413 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit 48052d70ec065a3a8d9e6e121cab5ae857f8da1a 1 parent 49060cd
@jeremy jeremy authored
View
2  activerecord/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Allow models to override to_xml. #4989 [Blair Zajac <blair@orcaware.com>]
+
* PostgreSQL: don't ignore port when host is nil since it's often used to label the domain socket. #5247 [shimbo@is.naist.jp]
* Records and arrays of records are bound as quoted ids. [Jeremy Kemper]
View
210 activerecord/lib/active_record/base.rb
@@ -1,3 +1,4 @@
+require 'base64'
require 'yaml'
require 'set'
require 'active_record/deprecated_finders'
@@ -84,7 +85,7 @@ def initialize(errors)
# The array form is to be used when the condition input is tainted and requires sanitization. The string form can
# be used for statements that don't involve tainted data. Examples:
#
- # User < ActiveRecord::Base
+ # class User < ActiveRecord::Base
# def self.authenticate_unsafely(user_name, password)
# find(:first, :conditions => "user_name = '#{user_name}' AND password = '#{password}'")
# end
@@ -1024,7 +1025,7 @@ def merge_includes(first, second)
safe_to_array(first) + safe_to_array(second)
end
- # Object#to_a is deprecated, though it does have the desired behaviour
+ # Object#to_a is deprecated, though it does have the desired behavior
def safe_to_array(o)
case o
when NilClass
@@ -1635,7 +1636,7 @@ def readonly! #:nodoc:
# Builds an XML document to represent the model. Some configuration is
# availble through +options+, however more complicated cases should use
- # Builder.
+ # override ActiveRecord's to_xml.
#
# By default the generated XML document will include the processing
# instruction and all object's attributes. For example:
@@ -1655,8 +1656,14 @@ def readonly! #:nodoc:
# <last-read type="date">2004-04-15</last-read>
# </topic>
#
- # This behaviour can be controlled with :only, :except, and :skip_instruct
- # for instance:
+ # This behavior can be controlled with :only, :except,
+ # :skip_instruct, :skip_types and :dasherize. The :only and
+ # :except options are the same as for the #attributes method.
+ # The default is to dasherize all column names, to disable this,
+ # set :dasherize to false. To not have the column type included
+ # in the XML output, set :skip_types to false.
+ #
+ # For instance:
#
# topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
#
@@ -1697,49 +1704,198 @@ def readonly! #:nodoc:
#
# To include any methods on the object(s) being called use :methods
#
- # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
+ # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
#
# <firm>
# # ... normal attributes as shown above ...
# <calculated-earnings>100000000000000000</calculated-earnings>
# <real-earnings>5</real-earnings>
# </firm>
+ #
+ # To call any Proc's on the object(s) use :procs. The Proc's
+ # are passed a modified version of the options hash that was
+ # given to #to_xml.
+ #
+ # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
+ # firm.to_xml :procs => [ proc ]
+ #
+ # <firm>
+ # # ... normal attributes as shown above ...
+ # <abc>def</abc>
+ # </firm>
+ #
+ # You may override the to_xml method in your ActiveRecord::Base
+ # subclasses if you need to. The general form of doing this is
+ #
+ # class IHaveMyOwnXML < ActiveRecord::Base
+ # def to_xml(options = {})
+ # options[:indent] ||= 2
+ # xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+ # xml.instruct! unless options[:skip_instruct]
+ # xml.level_one do
+ # xml.tag!(:second_level, 'content')
+ # end
+ # end
+ # end
def to_xml(options = {})
- options[:root] ||= self.class.to_s.underscore
- options[:except] = Array(options[:except]) << self.class.inheritance_column unless options[:only] # skip type column
- root_only_or_except = { :only => options[:only], :except => options[:except] }
+ options[:indent] ||= 2
+ builder = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
- attributes_for_xml = attributes(root_only_or_except)
-
- if methods_to_call = options.delete(:methods)
- [*methods_to_call].each do |meth|
- attributes_for_xml[meth] = send(meth)
- end
+ unless options[:skip_instruct]
+ builder.instruct!
+ options[:skip_instruct] = true
end
-
- if include_associations = options.delete(:include)
- include_has_options = include_associations.is_a?(Hash)
-
- for association in include_has_options ? include_associations.keys : Array(include_associations)
- association_options = include_has_options ? include_associations[association] : root_only_or_except
- case self.class.reflect_on_association(association).macro
+ root = (options[:root] || self.class.to_s.underscore).to_s
+ if dasherize = !options.has_key?(:dasherize) || options[:dasherize]
+ root = root.dasherize
+ end
+
+ builder.tag!(root) do
+ # To replicate the behavior in ActiveRecord#attributes,
+ # :except takes precedence over :only. If :only is not set
+ # for a N level model but is set for the N+1 level models,
+ # then because :except is set to a default value, the second
+ # level model can have both :except and :only set. So if
+ # :only is set, always delete :except.
+
+ a_names = self.attribute_names
+ if options[:only]
+ options.delete(:except)
+ a_names = a_names & Array(options[:only]).collect { |n| n.to_s }
+ else
+ options[:except] = Array(options[:except]) | Array(self.class.inheritance_column)
+ a_names = a_names - options[:except].collect { |n| n.to_s }
+ end
+
+ columns_hash = self.class.columns_hash
+ a_names.each do |name|
+
+ # To be consistent with the ActiveRecord instance being
+ # converted into a Hash using #attributes, convert SQL
+ # type names. Map the different "string" types all to
+ # "string", as the client of the XML does not care if a
+ # TEXT or VARCHAR column stores the value.
+
+ type = columns_hash[name].type
+ case type
+ when :text
+ type = :string
+ when :time
+ type = :datetime
+ end
+
+ attributes = options[:skip_types] ? { } : { :type => type }
+
+ value = self.send(name)
+ if dasherize
+ name = name.dasherize
+ end
+
+ # There is a significant speed improvement if the value
+ # does not need to be escaped, as #tag! escapes all values
+ # to ensure that valid XML is generated. For known binary
+ # values, it is at least an order of magnitude faster to
+ # Base64 encode binary values and directly put them in the
+ # output XML than to pass the original value or the Base64
+ # encoded value to the #tag! method. It definitely makes
+ # no sense to Base64 encode the value and then give it to
+ # #tag!, since that just adds additional overhead.
+ if value.nil?
+ attributes[:nil] = 'true'
+ builder.tag!(name, attributes)
+ else
+ value_needs_no_encoding = false
+ case type
+ when :binary
+ value = Base64.encode64(value)
+ attributes[:encoding] = 'base64'
+ value_needs_no_encoding = true
+ when :date
+ value = value.to_s(:db)
+ value_needs_no_encoding = true
+ when :datetime
+ value = value.xmlschema
+ value_needs_no_encoding = true
+ when :boolean, :float, :integer
+ value = value.to_s
+ value_needs_no_encoding = true
+ end
+
+ if value_needs_no_encoding
+ builder.tag!(name, attributes) do
+ builder << value
+ end
+ else
+ builder.tag!(name, value, attributes)
+ end
+ end
+ end
+
+ if methods_to_call = options.delete(:methods)
+ [ *methods_to_call ].each do |meth|
+ value = self.send(meth)
+
+ tag = dasherize ? meth.to_s.dasherize : meth.to_s
+
+ type_name = ActiveSupport::CoreExtensions::Hash::Conversions::XML_TYPE_NAMES[value.class]
+
+ if formatter = ActiveSupport::CoreExtensions::Hash::Conversions::XML_FORMATTING[type_name]
+ value = formatter.call(value)
+ end
+
+ if value.nil?
+ attributes = { :nil => true }
+ else
+ if !options[:skip_types] && !type_name.nil?
+ attributes = { :type => type_name }
+ else
+ attributes = { }
+ end
+ end
+
+ builder.tag!(tag, value, attributes)
+ end
+ end
+
+ if include_associations = options.delete(:include)
+ root_only_or_except = { :except => options[:except],
+ :only => options[:only] }
+
+ include_has_options = include_associations.is_a?(Hash)
+
+ for association in include_has_options ? include_associations.keys : Array(include_associations)
+ association_options = include_has_options ? include_associations[association] : root_only_or_except
+
+ opts = options.merge(association_options)
+
+ case self.class.reflect_on_association(association).macro
when :has_many, :has_and_belongs_to_many
records = send(association).to_a
unless records.empty?
- attributes_for_xml[association] = records.collect do |record|
- record.attributes(association_options)
+ tag = records.first.class.to_s.underscore.pluralize
+ if dasherize
+ tag = tag.dasherize
+ end
+ builder.tag!(tag) do
+ records.each { |r| r.to_xml(opts) }
end
end
when :has_one, :belongs_to
if record = send(association)
- attributes_for_xml[association] = record.attributes(association_options)
+ record.to_xml(opts.merge(:root => association))
end
+ end
end
end
- end
- attributes_for_xml.to_xml(options)
+ if procs = options.delete(:procs)
+ [ *procs ].each do |proc|
+ proc.call(options)
+ end
+ end
+
+ end
end
private
View
46 activerecord/test/base_test.rb
@@ -439,18 +439,18 @@ def test_boolean_attributes
def test_increment_counter
Topic.increment_counter("replies_count", 1)
- assert_equal 1, Topic.find(1).replies_count
+ assert_equal 2, Topic.find(1).replies_count
Topic.increment_counter("replies_count", 1)
- assert_equal 2, Topic.find(1).replies_count
+ assert_equal 3, Topic.find(1).replies_count
end
def test_decrement_counter
Topic.decrement_counter("replies_count", 2)
- assert_equal 1, Topic.find(2).replies_count
+ assert_equal -1, Topic.find(2).replies_count
Topic.decrement_counter("replies_count", 2)
- assert_equal 0, Topic.find(1).replies_count
+ assert_equal -2, Topic.find(2).replies_count
end
def test_update_all
@@ -987,12 +987,12 @@ def test_class_level_delete
end
def test_increment_attribute
- assert_equal 0, topics(:first).replies_count
+ assert_equal 1, topics(:first).replies_count
topics(:first).increment! :replies_count
- assert_equal 1, topics(:first, :reload).replies_count
+ assert_equal 2, topics(:first, :reload).replies_count
topics(:first).increment(:replies_count).increment!(:replies_count)
- assert_equal 3, topics(:first, :reload).replies_count
+ assert_equal 4, topics(:first, :reload).replies_count
end
def test_increment_nil_attribute
@@ -1003,13 +1003,13 @@ def test_increment_nil_attribute
def test_decrement_attribute
topics(:first).increment(:replies_count).increment!(:replies_count)
- assert_equal 2, topics(:first).replies_count
+ assert_equal 3, topics(:first).replies_count
topics(:first).decrement!(:replies_count)
- assert_equal 1, topics(:first, :reload).replies_count
+ assert_equal 2, topics(:first, :reload).replies_count
topics(:first).decrement(:replies_count).decrement!(:replies_count)
- assert_equal -1, topics(:first, :reload).replies_count
+ assert_equal 0, topics(:first, :reload).replies_count
end
def test_toggle_attribute
@@ -1217,14 +1217,14 @@ def test_to_xml
written_on_in_current_timezone = topics(:first).written_on.xmlschema
last_read_in_current_timezone = topics(:first).last_read.xmlschema
assert_equal "<topic>", xml.first(7)
- assert xml.include?(%(<title>The First Topic</title>))
- assert xml.include?(%(<author-name>David</author-name>))
+ assert xml.include?(%(<title type="string">The First Topic</title>))
+ assert xml.include?(%(<author-name type="string">David</author-name>))
assert xml.include?(%(<id type="integer">1</id>))
- assert xml.include?(%(<replies-count type="integer">0</replies-count>))
+ assert xml.include?(%(<replies-count type="integer">1</replies-count>))
assert xml.include?(%(<written-on type="datetime">#{written_on_in_current_timezone}</written-on>))
- assert xml.include?(%(<content>Have a nice day</content>))
- assert xml.include?(%(<author-email-address>david@loudthinking.com</author-email-address>))
- assert xml.include?(%(<parent-id></parent-id>))
+ assert xml.include?(%(<content type="string">Have a nice day</content>))
+ assert xml.include?(%(<author-email-address type="string">david@loudthinking.com</author-email-address>))
+ assert xml.match(%r{<parent-id (type="integer"\s*|nil="true"\s*){2}/>})
if current_adapter?(:SybaseAdapter) or current_adapter?(:SQLServerAdapter)
assert xml.include?(%(<last-read type="datetime">#{last_read_in_current_timezone}</last-read>))
else
@@ -1236,23 +1236,23 @@ def test_to_xml
assert xml.include?(%(<bonus-time type="datetime">#{bonus_time_in_current_timezone}</bonus-time>))
end
end
-
+
def test_to_xml_skipping_attributes
xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => :title)
assert_equal "<topic>", xml.first(7)
- assert !xml.include?(%(<title>The First Topic</title>))
- assert xml.include?(%(<author-name>David</author-name>))
+ assert !xml.include?(%(<title type="string">The First Topic</title>))
+ assert xml.include?(%(<author-name type="string">David</author-name>))
xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [ :title, :author_name ])
- assert !xml.include?(%(<title>The First Topic</title>))
- assert !xml.include?(%(<author-name>David</author-name>))
+ assert !xml.include?(%(<title type="string">The First Topic</title>))
+ assert !xml.include?(%(<author-name type="string">David</author-name>))
end
def test_to_xml_including_has_many_association
xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
assert_equal "<topic>", xml.first(7)
assert xml.include?(%(<replies><reply>))
- assert xml.include?(%(<title>The Second Topic's of the day</title>))
+ assert xml.include?(%(<title type="string">The Second Topic's of the day</title>))
end
def test_to_xml_including_belongs_to_association
@@ -1277,7 +1277,7 @@ def test_to_xml_including_multiple_associations_with_options
)
assert_equal "<firm>", xml.first(6)
- assert xml.include?(%(<client><name>Summit</name></client>))
+ assert xml.include?(%(<client><name type="string">Summit</name></client>))
assert xml.include?(%(<clients><client>))
end
View
4 activerecord/test/fixtures/topics.yml
@@ -8,7 +8,7 @@ first:
bonus_time: 2005-01-30t15:28:00.00+01:00
content: Have a nice day
approved: false
- replies_count: 0
+ replies_count: 1
second:
id: 2
@@ -17,6 +17,6 @@ second:
written_on: 2003-07-15t15:28:00.00+01:00
content: Have a nice day
approved: true
- replies_count: 2
+ replies_count: 0
parent_id: 1
type: Reply
View
2  activesupport/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* to_xml fixes, features, and speedup: introduce :dasherize option that converts updated_at to updated-at if true (the existing default); binary columns get encoding="base64" attribute; nil values get nil="true" attribute to distinguish empty values; add type information for float columns; allow arbitrarily deep :include; include SQL type information as the type attribute. #4989 [Blair Zajac <blair@orcaware.com>]
+
* Add OrderedHash#values. [Sam Stephenson]
* Added Array#to_s(:db) that'll produce a comma-separated list of ids [DHH]. Example:
View
12 activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -52,12 +52,20 @@ def to_xml(options = {})
options[:indent] ||= 2
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
- root = options.delete(:root)
+ root = options.delete(:root).to_s
children = options.delete(:children)
+ if !options.has_key?(:dasherize) || options[:dasherize]
+ root = root.dasherize
+ end
+
options[:builder].instruct! unless options.delete(:skip_instruct)
- options[:builder].tag!(root.to_s.dasherize) { each { |e| e.to_xml(options.merge({ :skip_instruct => true, :root => children })) } }
+
+ opts = options.merge({ :skip_instruct => true, :root => children })
+
+ options[:builder].tag!(root) { each { |e| e.to_xml(opts) } }
end
+
end
end
end
View
57 activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -3,40 +3,69 @@ module CoreExtensions #:nodoc:
module Hash #:nodoc:
module Conversions
XML_TYPE_NAMES = {
- "Fixnum" => "integer",
- "Date" => "date",
- "Time" => "datetime",
- "TrueClass" => "boolean",
- "FalseClass" => "boolean"
+ ::Fixnum => "integer",
+ ::Float => "float",
+ ::Date => "date",
+ ::DateTime => "datetime",
+ ::Time => "datetime",
+ ::TrueClass => "boolean",
+ ::FalseClass => "boolean"
}
-
+
XML_FORMATTING = {
"date" => Proc.new { |date| date.to_s(:db) },
"datetime" => Proc.new { |time| time.xmlschema }
}
-
+
def to_xml(options = {})
options[:indent] ||= 2
- options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]), :root => "hash" })
+ options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]),
+ :root => "hash" })
options[:builder].instruct! unless options.delete(:skip_instruct)
+ dasherize = !options.has_key?(:dasherize) || options[:dasherize]
+ root = dasherize ? options[:root].to_s.dasherize : options[:root].to_s
- options[:builder].__send__(options[:root].to_s.dasherize) do
+ options[:builder].__send__(root) do
each do |key, value|
case value
when ::Hash
value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
when ::Array
value.to_xml(options.merge({ :root => key, :children => key.to_s.singularize, :skip_instruct => true}))
+ when ::Method, ::Proc
+ # If the Method or Proc takes two arguments, then
+ # pass the suggested child element name. This is
+ # used if the Method or Proc will be operating over
+ # multiple records and needs to create an containing
+ # element that will contain the objects being
+ # serialized.
+ if 1 == value.arity
+ value.call(options.merge({ :root => key, :skip_instruct => true }))
+ else
+ value.call(options.merge({ :root => key, :skip_instruct => true }), key.to_s.singularize)
+ end
else
- type_name = XML_TYPE_NAMES[value.class.to_s]
+ if value.respond_to?(:to_xml)
+ value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
+ else
+ type_name = XML_TYPE_NAMES[value.class]
+
+ key = dasherize ? key.to_s.dasherize : key.to_s
- options[:builder].tag!(key.to_s.dasherize,
- XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
- options[:skip_types] || value.nil? || type_name.nil? ? { } : { :type => type_name }
- )
+ attributes = options[:skip_types] || value.nil? || type_name.nil? ? { } : { :type => type_name }
+ if value.nil?
+ attributes[:nil] = true
+ end
+
+ options[:builder].tag!(key,
+ XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
+ attributes
+ )
+ end
end
end
end
+
end
end
end
View
20 activesupport/test/core_ext/array_ext_test.rb
@@ -136,4 +136,24 @@ def test_to_xml_with_options
assert xml.include?(%(<street-address>Evergreen</street-address>))
assert xml.include?(%(<name>Jason</name>))
end
+
+ def test_to_xml_with_dasherize_false
+ xml = [
+ { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
+ ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => false)
+
+ assert_equal "<records><record>", xml.first(17)
+ assert xml.include?(%(<street_address>Paulina</street_address>))
+ assert xml.include?(%(<street_address>Evergreen</street_address>))
+ end
+
+ def test_to_xml_with_dasherize_true
+ xml = [
+ { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
+ ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => true)
+
+ assert_equal "<records><record>", xml.first(17)
+ assert xml.include?(%(<street-address>Paulina</street-address>))
+ assert xml.include?(%(<street-address>Evergreen</street-address>))
+ end
end
View
39 activesupport/test/core_ext/hash_ext_test.rb
@@ -3,7 +3,6 @@
class HashExtTest < Test::Unit::TestCase
def setup
-
@strings = { 'a' => 1, 'b' => 2 }
@symbols = { :a => 1, :b => 2 }
@mixed = { :a => 1, 'b' => 2 }
@@ -191,6 +190,17 @@ def test_diff
end
end
+class IWriteMyOwnXML
+ def to_xml(options = {})
+ options[:indent] ||= 2
+ xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+ xml.instruct! unless options[:skip_instruct]
+ xml.level_one do
+ xml.tag!(:second_level, 'content')
+ end
+ end
+end
+
class HashToXmlTest < Test::Unit::TestCase
def setup
@xml_options = { :root => :person, :skip_instruct => true, :indent => 0 }
@@ -203,6 +213,20 @@ def test_one_level
assert xml.include?(%(<name>David</name>))
end
+ def test_one_level_dasherize_false
+ xml = { :name => "David", :street_name => "Paulina" }.to_xml(@xml_options.merge(:dasherize => false))
+ assert_equal "<person>", xml.first(8)
+ assert xml.include?(%(<street_name>Paulina</street_name>))
+ assert xml.include?(%(<name>David</name>))
+ end
+
+ def test_one_level_dasherize_true
+ xml = { :name => "David", :street_name => "Paulina" }.to_xml(@xml_options.merge(:dasherize => true))
+ assert_equal "<person>", xml.first(8)
+ assert xml.include?(%(<street-name>Paulina</street-name>))
+ assert xml.include?(%(<name>David</name>))
+ end
+
def test_one_level_with_types
xml = { :name => "David", :street => "Paulina", :age => 26, :moved_on => Date.new(2005, 11, 15) }.to_xml(@xml_options)
assert_equal "<person>", xml.first(8)
@@ -217,7 +241,7 @@ def test_one_level_with_nils
assert_equal "<person>", xml.first(8)
assert xml.include?(%(<street>Paulina</street>))
assert xml.include?(%(<name>David</name>))
- assert xml.include?(%(<age></age>))
+ assert xml.include?(%(<age nil="true"></age>))
end
def test_one_level_with_skipping_types
@@ -225,7 +249,7 @@ def test_one_level_with_skipping_types
assert_equal "<person>", xml.first(8)
assert xml.include?(%(<street>Paulina</street>))
assert xml.include?(%(<name>David</name>))
- assert xml.include?(%(<age></age>))
+ assert xml.include?(%(<age nil="true"></age>))
end
def test_two_levels
@@ -234,6 +258,13 @@ def test_two_levels
assert xml.include?(%(<address><street>Paulina</street></address>))
assert xml.include?(%(<name>David</name>))
end
+
+ def test_two_levels_with_second_level_overriding_to_xml
+ xml = { :name => "David", :address => { :street => "Paulina" }, :child => IWriteMyOwnXML.new }.to_xml(@xml_options)
+ assert_equal "<person>", xml.first(8)
+ assert xml.include?(%(<address><street>Paulina</street></address>))
+ assert xml.include?(%(<level_one><second_level>content</second_level></level_one>))
+ end
def test_two_levels_with_array
xml = { :name => "David", :addresses => [{ :street => "Paulina" }, { :street => "Evergreen" }] }.to_xml(@xml_options)
@@ -243,10 +274,10 @@ def test_two_levels_with_array
assert xml.include?(%(<address><street>Evergreen</street></address>))
assert xml.include?(%(<name>David</name>))
end
-
def test_three_levels_with_array
xml = { :name => "David", :addresses => [{ :streets => [ { :name => "Paulina" }, { :name => "Paulina" } ] } ] }.to_xml(@xml_options)
assert xml.include?(%(<addresses><address><streets><street><name>))
end
+
end
Please sign in to comment.
Something went wrong with that request. Please try again.