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