1 module ActiveRecord
#:nodoc:
3 # Builds an XML document to represent the model. Some configuration is
4 # available through +options+. However more complicated cases should
5 # override ActiveRecord::Base#to_xml.
7 # By default the generated XML document will include the processing
8 # instruction and all the object's attributes. For example:
10 # <?xml version="1.0" encoding="UTF-8"?>
12 # <title>The First Topic</title>
13 # <author-name>David</author-name>
14 # <id type="integer">1</id>
15 # <approved type="boolean">false</approved>
16 # <replies-count type="integer">0</replies-count>
17 # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
18 # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
19 # <content>Have a nice day</content>
20 # <author-email-address>david@loudthinking.com</author-email-address>
21 # <parent-id></parent-id>
22 # <last-read type="date">2004-04-15</last-read>
25 # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
26 # <tt>:skip_instruct</tt>, <tt>:skip_types</tt> and <tt>:dasherize</tt>.
27 # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
28 # +attributes+ method. The default is to dasherize all column names, but you
29 # can disable this setting <tt>:dasherize</tt> to +false+. To not have the
30 # column type included in the XML output set <tt>:skip_types</tt> to +true+.
34 # topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
37 # <title>The First Topic</title>
38 # <author-name>David</author-name>
39 # <approved type="boolean">false</approved>
40 # <content>Have a nice day</content>
41 # <author-email-address>david@loudthinking.com</author-email-address>
42 # <parent-id></parent-id>
43 # <last-read type="date">2004-04-15</last-read>
46 # To include first level associations use <tt>:include</tt>:
48 # firm.to_xml :include => [ :account, :clients ]
50 # <?xml version="1.0" encoding="UTF-8"?>
52 # <id type="integer">1</id>
53 # <rating type="integer">1</rating>
54 # <name>37signals</name>
55 # <clients type="array">
57 # <rating type="integer">1</rating>
61 # <rating type="integer">1</rating>
62 # <name>Microsoft</name>
66 # <id type="integer">1</id>
67 # <credit-limit type="integer">50</credit-limit>
71 # To include deeper levels of associations pass a hash like this:
73 # firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
74 # <?xml version="1.0" encoding="UTF-8"?>
76 # <id type="integer">1</id>
77 # <rating type="integer">1</rating>
78 # <name>37signals</name>
79 # <clients type="array">
81 # <rating type="integer">1</rating>
88 # <rating type="integer">1</rating>
89 # <name>Microsoft</name>
96 # <id type="integer">1</id>
97 # <credit-limit type="integer">50</credit-limit>
101 # To include any methods on the model being called use <tt>:methods</tt>:
103 # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
106 # # ... normal attributes as shown above ...
107 # <calculated-earnings>100000000000000000</calculated-earnings>
108 # <real-earnings>5</real-earnings>
111 # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a
112 # modified version of the options hash that was given to +to_xml+:
114 # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
115 # firm.to_xml :procs => [ proc ]
118 # # ... normal attributes as shown above ...
122 # Alternatively, you can yield the builder object as part of the +to_xml+ call:
124 # firm.to_xml do |xml|
126 # xml.first_name "David"
127 # xml.last_name "Heinemeier Hansson"
132 # # ... normal attributes as shown above ...
134 # <first_name>David</first_name>
135 # <last_name>Heinemeier Hansson</last_name>
139 # As noted above, you may override +to_xml+ in your ActiveRecord::Base
140 # subclasses to have complete control about what's generated. The general
141 # form of doing this is:
143 # class IHaveMyOwnXML < ActiveRecord::Base
144 # def to_xml(options = {})
145 # options[:indent] ||= 2
146 # xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
147 # xml.instruct! unless options[:skip_instruct]
149 # xml.tag!(:second_level, 'content')
153 def to_xml(options
= {}, &block
)
154 serializer
= XmlSerializer
.new(self, options
)
155 block_given
? ? serializer
.to_s(&block
) : serializer
.to_s
159 self.attributes
= Hash
.from_xml(xml
).values
.first
164 class XmlSerializer
< ActiveRecord
::Serialization::Serializer #:nodoc:
167 options
[:indent] ||= 2
168 builder
= options
[:builder] ||= Builder
::XmlMarkup.new(:indent => options
[:indent])
170 unless options
[:skip_instruct]
172 options
[:skip_instruct] = true
180 root
= (options
[:root] || @record.class.to_s
.underscore
).to_s
181 dasherize
? ? root
.dasherize
: root
185 !options
.has_key
?(:dasherize) || options
[:dasherize]
188 def serializable_attributes
189 serializable_attribute_names
.collect
{ |name
| Attribute
.new(name
, @record) }
192 def serializable_method_attributes
193 Array(options
[:methods]).inject([]) do |method_attributes
, name
|
194 method_attributes
<< MethodAttribute
.new(name
.to_s
, @record) if @record.respond_to
?(name
.to_s
)
200 (serializable_attributes
+ serializable_method_attributes
).each
do |attribute
|
206 if procs
= options
.delete(:procs)
207 [ *procs
].each
do |proc
|
213 def add_tag(attribute
)
215 dasherize
? ? attribute
.name
.dasherize
: attribute
.name
,
216 attribute
.value
.to_s
,
217 attribute
.decorations(!options
[:skip_types])
221 def add_associations(association
, records
, opts
)
222 if records
.is_a
?(Enumerable
)
223 tag
= association
.to_s
224 tag
= tag
.dasherize
if dasherize
?
226 builder
.tag
!(tag
, :type => :array)
228 builder
.tag
!(tag
, :type => :array) do
229 association_name
= association
.to_s
.singularize
230 records
.each
do |record
|
231 record
.to_xml opts
.merge(
232 :root => association_name
,
233 :type => (record
.class.to_s
.underscore
== association_name
? nil : record
.class.name
)
239 if record
= @record.send(association
)
240 record
.to_xml(opts
.merge(:root => association
))
247 if options
[:namespace]
248 args
<< {:xmlns=>options
[:namespace]}
252 args
<< {:type=>options
[:type]}
255 builder
.tag
!(*args
) do
257 procs
= options
.delete(:procs)
258 add_includes
{ |association
, records
, opts
| add_associations(association
, records
, opts
) }
259 options
[:procs] = procs
261 yield builder
if block_given
?
265 class Attribute
#:nodoc:
266 attr_reader
:name, :value, :type
268 def initialize(name
, record
)
269 @name, @record = name
, record
272 @value = compute_value
275 # There is a significant speed improvement if the value
276 # does not need to be escaped, as <tt>tag!</tt> escapes all values
277 # to ensure that valid XML is generated. For known binary
278 # values, it is at least an order of magnitude faster to
279 # Base64 encode binary values and directly put them in the
280 # output XML than to pass the original value or the Base64
281 # encoded value to the <tt>tag!</tt> method. It definitely makes
282 # no sense to Base64 encode the value and then give it to
283 # <tt>tag!</tt>, since that just adds additional overhead.
285 ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type
)
288 def decorations(include_types
= true)
292 decorations
[:encoding] = 'base64'
295 if include_types
&& type
!= :string
296 decorations
[:type] = type
300 decorations
[:nil] = true
308 type
= @record.class.serialized_attributes
.has_key
?(name
) ? :yaml : @record.class.columns_hash
[name
].type
321 value
= @record.send(name
)
323 if formatter
= Hash
::XML_FORMATTING[type
.to_s
]
324 value
? formatter
.call(value
) : nil
331 class MethodAttribute
< Attribute
#:nodoc:
334 Hash
::XML_TYPE_NAMES[@record.send(name
).class.name
] || :string