Updated README.rdoc again
[feedcatcher.git] / vendor / rails / activerecord / lib / active_record / serializers / xml_serializer.rb
1 module ActiveRecord #:nodoc:
2 module Serialization
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.
6 #
7 # By default the generated XML document will include the processing
8 # instruction and all the object's attributes. For example:
9 #
10 # <?xml version="1.0" encoding="UTF-8"?>
11 # <topic>
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>
23 # </topic>
24 #
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+.
32 #
33 # For instance:
34 #
35 # topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
36 #
37 # <topic>
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>
45 # </topic>
46 #
47 # To include first level associations use <tt>:include</tt>:
48 #
49 # firm.to_xml :include => [ :account, :clients ]
50 #
51 # <?xml version="1.0" encoding="UTF-8"?>
52 # <firm>
53 # <id type="integer">1</id>
54 # <rating type="integer">1</rating>
55 # <name>37signals</name>
56 # <clients type="array">
57 # <client>
58 # <rating type="integer">1</rating>
59 # <name>Summit</name>
60 # </client>
61 # <client>
62 # <rating type="integer">1</rating>
63 # <name>Microsoft</name>
64 # </client>
65 # </clients>
66 # <account>
67 # <id type="integer">1</id>
68 # <credit-limit type="integer">50</credit-limit>
69 # </account>
70 # </firm>
71 #
72 # To include deeper levels of associations pass a hash like this:
73 #
74 # firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
75 # <?xml version="1.0" encoding="UTF-8"?>
76 # <firm>
77 # <id type="integer">1</id>
78 # <rating type="integer">1</rating>
79 # <name>37signals</name>
80 # <clients type="array">
81 # <client>
82 # <rating type="integer">1</rating>
83 # <name>Summit</name>
84 # <address>
85 # ...
86 # </address>
87 # </client>
88 # <client>
89 # <rating type="integer">1</rating>
90 # <name>Microsoft</name>
91 # <address>
92 # ...
93 # </address>
94 # </client>
95 # </clients>
96 # <account>
97 # <id type="integer">1</id>
98 # <credit-limit type="integer">50</credit-limit>
99 # </account>
100 # </firm>
101 #
102 # To include any methods on the model being called use <tt>:methods</tt>:
103 #
104 # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
105 #
106 # <firm>
107 # # ... normal attributes as shown above ...
108 # <calculated-earnings>100000000000000000</calculated-earnings>
109 # <real-earnings>5</real-earnings>
110 # </firm>
111 #
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+:
114 #
115 # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
116 # firm.to_xml :procs => [ proc ]
117 #
118 # <firm>
119 # # ... normal attributes as shown above ...
120 # <abc>def</abc>
121 # </firm>
122 #
123 # Alternatively, you can yield the builder object as part of the +to_xml+ call:
124 #
125 # firm.to_xml do |xml|
126 # xml.creator do
127 # xml.first_name "David"
128 # xml.last_name "Heinemeier Hansson"
129 # end
130 # end
131 #
132 # <firm>
133 # # ... normal attributes as shown above ...
134 # <creator>
135 # <first_name>David</first_name>
136 # <last_name>Heinemeier Hansson</last_name>
137 # </creator>
138 # </firm>
139 #
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:
143 #
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]
149 # xml.level_one do
150 # xml.tag!(:second_level, 'content')
151 # end
152 # end
153 # end
154 def to_xml(options = {}, &block)
155 serializer = XmlSerializer.new(self, options)
156 block_given? ? serializer.to_s(&block) : serializer.to_s
157 end
158
159 def from_xml(xml)
160 self.attributes = Hash.from_xml(xml).values.first
161 self
162 end
163 end
164
165 class XmlSerializer < ActiveRecord::Serialization::Serializer #:nodoc:
166 def builder
167 @builder ||= begin
168 options[:indent] ||= 2
169 builder = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
170
171 unless options[:skip_instruct]
172 builder.instruct!
173 options[:skip_instruct] = true
174 end
175
176 builder
177 end
178 end
179
180 def root
181 root = (options[:root] || @record.class.to_s.underscore).to_s
182 reformat_name(root)
183 end
184
185 def dasherize?
186 !options.has_key?(:dasherize) || options[:dasherize]
187 end
188
189 def camelize?
190 options.has_key?(:camelize) && options[:camelize]
191 end
192
193 def reformat_name(name)
194 name = name.camelize if camelize?
195 dasherize? ? name.dasherize : name
196 end
197
198 def serializable_attributes
199 serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
200 end
201
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)
205 method_attributes
206 end
207 end
208
209 def add_attributes
210 (serializable_attributes + serializable_method_attributes).each do |attribute|
211 add_tag(attribute)
212 end
213 end
214
215 def add_procs
216 if procs = options.delete(:procs)
217 [ *procs ].each do |proc|
218 proc.call(options)
219 end
220 end
221 end
222
223 def add_tag(attribute)
224 builder.tag!(
225 reformat_name(attribute.name),
226 attribute.value.to_s,
227 attribute.decorations(!options[:skip_types])
228 )
229 end
230
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"}
235
236 if records.empty?
237 builder.tag!(tag, type)
238 else
239 builder.tag!(tag, type) do
240 association_name = association.to_s.singularize
241 records.each do |record|
242 if options[:skip_types]
243 record_type = {}
244 else
245 record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
246 record_type = {:type => record_class}
247 end
248
249 record.to_xml opts.merge(:root => association_name).merge(record_type)
250 end
251 end
252 end
253 else
254 if record = @record.send(association)
255 record.to_xml(opts.merge(:root => association))
256 end
257 end
258 end
259
260 def serialize
261 args = [root]
262 if options[:namespace]
263 args << {:xmlns=>options[:namespace]}
264 end
265
266 if options[:type]
267 args << {:type=>options[:type]}
268 end
269
270 builder.tag!(*args) do
271 add_attributes
272 procs = options.delete(:procs)
273 add_includes { |association, records, opts| add_associations(association, records, opts) }
274 options[:procs] = procs
275 add_procs
276 yield builder if block_given?
277 end
278 end
279
280 class Attribute #:nodoc:
281 attr_reader :name, :value, :type
282
283 def initialize(name, record)
284 @name, @record = name, record
285
286 @type = compute_type
287 @value = compute_value
288 end
289
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.
299 def needs_encoding?
300 ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
301 end
302
303 def decorations(include_types = true)
304 decorations = {}
305
306 if type == :binary
307 decorations[:encoding] = 'base64'
308 end
309
310 if include_types && type != :string
311 decorations[:type] = type
312 end
313
314 if value.nil?
315 decorations[:nil] = true
316 end
317
318 decorations
319 end
320
321 protected
322 def compute_type
323 type = @record.class.serialized_attributes.has_key?(name) ? :yaml : @record.class.columns_hash[name].type
324
325 case type
326 when :text
327 :string
328 when :time
329 :datetime
330 else
331 type
332 end
333 end
334
335 def compute_value
336 value = @record.send(name)
337
338 if formatter = Hash::XML_FORMATTING[type.to_s]
339 value ? formatter.call(value) : nil
340 else
341 value
342 end
343 end
344 end
345
346 class MethodAttribute < Attribute #:nodoc:
347 protected
348 def compute_type
349 Hash::XML_TYPE_NAMES[@record.send(name).class.name] || :string
350 end
351 end
352 end
353 end