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>, <tt>:dasherize</tt> and <tt>:camelize</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+. Setting <tt>:camelize</tt>
30 # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
31 # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
35 # topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
38 # <title>The First Topic</title>
39 # <author-name>David</author-name>
40 # <approved type="boolean">false</approved>
41 # <content>Have a nice day</content>
42 # <author-email-address>david@loudthinking.com</author-email-address>
43 # <parent-id></parent-id>
44 # <last-read type="date">2004-04-15</last-read>
47 # To include first level associations use <tt>:include</tt>:
49 # firm.to_xml :include => [ :account, :clients ]
51 # <?xml version="1.0" encoding="UTF-8"?>
53 # <id type="integer">1</id>
54 # <rating type="integer">1</rating>
55 # <name>37signals</name>
56 # <clients type="array">
58 # <rating type="integer">1</rating>
62 # <rating type="integer">1</rating>
63 # <name>Microsoft</name>
67 # <id type="integer">1</id>
68 # <credit-limit type="integer">50</credit-limit>
72 # To include deeper levels of associations pass a hash like this:
74 # firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
75 # <?xml version="1.0" encoding="UTF-8"?>
77 # <id type="integer">1</id>
78 # <rating type="integer">1</rating>
79 # <name>37signals</name>
80 # <clients type="array">
82 # <rating type="integer">1</rating>
89 # <rating type="integer">1</rating>
90 # <name>Microsoft</name>
97 # <id type="integer">1</id>
98 # <credit-limit type="integer">50</credit-limit>
102 # To include any methods on the model being called use <tt>:methods</tt>:
104 # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
107 # # ... normal attributes as shown above ...
108 # <calculated-earnings>100000000000000000</calculated-earnings>
109 # <real-earnings>5</real-earnings>
112 # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a
113 # modified version of the options hash that was given to +to_xml+:
115 # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
116 # firm.to_xml :procs => [ proc ]
119 # # ... normal attributes as shown above ...
123 # Alternatively, you can yield the builder object as part of the +to_xml+ call:
125 # firm.to_xml do |xml|
127 # xml.first_name "David"
128 # xml.last_name "Heinemeier Hansson"
133 # # ... normal attributes as shown above ...
135 # <first_name>David</first_name>
136 # <last_name>Heinemeier Hansson</last_name>
140 # As noted above, you may override +to_xml+ in your ActiveRecord::Base
141 # subclasses to have complete control about what's generated. The general
142 # form of doing this is:
144 # class IHaveMyOwnXML < ActiveRecord::Base
145 # def to_xml(options = {})
146 # options[:indent] ||= 2
147 # xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
148 # xml.instruct! unless options[:skip_instruct]
150 # xml.tag!(:second_level, 'content')
154 def to_xml(options
= {}, &block
)
155 serializer
= XmlSerializer
.new(self, options
)
156 block_given
? ? serializer
.to_s(&block
) : serializer
.to_s
160 self.attributes
= Hash
.from_xml(xml
).values
.first
165 class XmlSerializer
< ActiveRecord
::Serialization::Serializer #:nodoc:
168 options
[:indent] ||= 2
169 builder
= options
[:builder] ||= Builder
::XmlMarkup.new(:indent => options
[:indent])
171 unless options
[:skip_instruct]
173 options
[:skip_instruct] = true
181 root
= (options
[:root] || @record.class.to_s
.underscore
).to_s
186 !options
.has_key
?(:dasherize) || options
[:dasherize]
190 options
.has_key
?(:camelize) && options
[:camelize]
193 def reformat_name(name
)
194 name
= name
.camelize
if camelize
?
195 dasherize
? ? name
.dasherize
: name
198 def serializable_attributes
199 serializable_attribute_names
.collect
{ |name
| Attribute
.new(name
, @record) }
202 def serializable_method_attributes
203 Array(options
[:methods]).inject([]) do |method_attributes
, name
|
204 method_attributes
<< MethodAttribute
.new(name
.to_s
, @record) if @record.respond_to
?(name
.to_s
)
210 (serializable_attributes
+ serializable_method_attributes
).each
do |attribute
|
216 if procs
= options
.delete(:procs)
217 [ *procs
].each
do |proc
|
223 def add_tag(attribute
)
225 reformat_name(attribute
.name
),
226 attribute
.value
.to_s
,
227 attribute
.decorations(!options
[:skip_types])
231 def add_associations(association
, records
, opts
)
232 if records
.is_a
?(Enumerable
)
233 tag
= reformat_name(association
.to_s
)
234 type
= options
[:skip_types] ? {} : {:type => "array"}
237 builder
.tag
!(tag
, type
)
239 builder
.tag
!(tag
, type
) do
240 association_name
= association
.to_s
.singularize
241 records
.each
do |record
|
242 if options
[:skip_types]
245 record_class
= (record
.class.to_s
.underscore
== association_name
) ? nil : record
.class.name
246 record_type
= {:type => record_class
}
249 record
.to_xml opts
.merge(:root => association_name
).merge(record_type
)
254 if record
= @record.send(association
)
255 record
.to_xml(opts
.merge(:root => association
))
262 if options
[:namespace]
263 args
<< {:xmlns=>options
[:namespace]}
267 args
<< {:type=>options
[:type]}
270 builder
.tag
!(*args
) do
272 procs
= options
.delete(:procs)
273 add_includes
{ |association
, records
, opts
| add_associations(association
, records
, opts
) }
274 options
[:procs] = procs
276 yield builder
if block_given
?
280 class Attribute
#:nodoc:
281 attr_reader
:name, :value, :type
283 def initialize(name
, record
)
284 @name, @record = name
, record
287 @value = compute_value
290 # There is a significant speed improvement if the value
291 # does not need to be escaped, as <tt>tag!</tt> escapes all values
292 # to ensure that valid XML is generated. For known binary
293 # values, it is at least an order of magnitude faster to
294 # Base64 encode binary values and directly put them in the
295 # output XML than to pass the original value or the Base64
296 # encoded value to the <tt>tag!</tt> method. It definitely makes
297 # no sense to Base64 encode the value and then give it to
298 # <tt>tag!</tt>, since that just adds additional overhead.
300 ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type
)
303 def decorations(include_types
= true)
307 decorations
[:encoding] = 'base64'
310 if include_types
&& type
!= :string
311 decorations
[:type] = type
315 decorations
[:nil] = true
323 type
= @record.class.serialized_attributes
.has_key
?(name
) ? :yaml : @record.class.columns_hash
[name
].type
336 value
= @record.send(name
)
338 if formatter
= Hash
::XML_FORMATTING[type
.to_s
]
339 value
? formatter
.call(value
) : nil
346 class MethodAttribute
< Attribute
#:nodoc:
349 Hash
::XML_TYPE_NAMES[@record.send(name
).class.name
] || :string