ROXML 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. data/.gitignore +6 -0
  2. data/.gitmodules +3 -0
  3. data/History.txt +299 -0
  4. data/MIT-LICENSE +18 -0
  5. data/README.rdoc +161 -0
  6. data/Rakefile +95 -0
  7. data/TODO +39 -0
  8. data/VERSION +1 -0
  9. data/config/website.yml +2 -0
  10. data/examples/amazon.rb +35 -0
  11. data/examples/current_weather.rb +27 -0
  12. data/examples/dashed_elements.rb +20 -0
  13. data/examples/library.rb +40 -0
  14. data/examples/posts.rb +27 -0
  15. data/examples/rails.rb +70 -0
  16. data/examples/twitter.rb +37 -0
  17. data/examples/xml/active_record.xml +70 -0
  18. data/examples/xml/amazon.xml +133 -0
  19. data/examples/xml/current_weather.xml +89 -0
  20. data/examples/xml/dashed_elements.xml +52 -0
  21. data/examples/xml/posts.xml +23 -0
  22. data/examples/xml/twitter.xml +422 -0
  23. data/lib/roxml.rb +547 -0
  24. data/lib/roxml/definition.rb +236 -0
  25. data/lib/roxml/hash_definition.rb +25 -0
  26. data/lib/roxml/xml.rb +43 -0
  27. data/lib/roxml/xml/parsers/libxml.rb +91 -0
  28. data/lib/roxml/xml/parsers/nokogiri.rb +77 -0
  29. data/lib/roxml/xml/references.rb +297 -0
  30. data/roxml.gemspec +201 -0
  31. data/spec/definition_spec.rb +486 -0
  32. data/spec/examples/active_record_spec.rb +40 -0
  33. data/spec/examples/amazon_spec.rb +54 -0
  34. data/spec/examples/current_weather_spec.rb +37 -0
  35. data/spec/examples/dashed_elements_spec.rb +20 -0
  36. data/spec/examples/library_spec.rb +46 -0
  37. data/spec/examples/post_spec.rb +24 -0
  38. data/spec/examples/twitter_spec.rb +32 -0
  39. data/spec/roxml_spec.rb +372 -0
  40. data/spec/shared_specs.rb +15 -0
  41. data/spec/spec.opts +1 -0
  42. data/spec/spec_helper.rb +14 -0
  43. data/spec/support/libxml.rb +3 -0
  44. data/spec/support/nokogiri.rb +3 -0
  45. data/spec/xml/attributes_spec.rb +36 -0
  46. data/spec/xml/namespace_spec.rb +240 -0
  47. data/spec/xml/namespaces_spec.rb +32 -0
  48. data/spec/xml/parser_spec.rb +26 -0
  49. data/tasks/rdoc.rake +13 -0
  50. data/tasks/rspec.rake +25 -0
  51. data/tasks/test.rake +35 -0
  52. data/test/fixtures/book_malformed.xml +5 -0
  53. data/test/fixtures/book_pair.xml +8 -0
  54. data/test/fixtures/book_text_with_attribute.xml +5 -0
  55. data/test/fixtures/book_valid.xml +5 -0
  56. data/test/fixtures/book_with_authors.xml +7 -0
  57. data/test/fixtures/book_with_contributions.xml +9 -0
  58. data/test/fixtures/book_with_contributors.xml +7 -0
  59. data/test/fixtures/book_with_contributors_attrs.xml +7 -0
  60. data/test/fixtures/book_with_default_namespace.xml +9 -0
  61. data/test/fixtures/book_with_depth.xml +6 -0
  62. data/test/fixtures/book_with_octal_pages.xml +4 -0
  63. data/test/fixtures/book_with_publisher.xml +7 -0
  64. data/test/fixtures/book_with_wrapped_attr.xml +3 -0
  65. data/test/fixtures/dictionary_of_attr_name_clashes.xml +8 -0
  66. data/test/fixtures/dictionary_of_attrs.xml +6 -0
  67. data/test/fixtures/dictionary_of_guarded_names.xml +6 -0
  68. data/test/fixtures/dictionary_of_mixeds.xml +4 -0
  69. data/test/fixtures/dictionary_of_name_clashes.xml +10 -0
  70. data/test/fixtures/dictionary_of_names.xml +4 -0
  71. data/test/fixtures/dictionary_of_texts.xml +10 -0
  72. data/test/fixtures/library.xml +30 -0
  73. data/test/fixtures/library_uppercase.xml +30 -0
  74. data/test/fixtures/muffins.xml +3 -0
  75. data/test/fixtures/nameless_ageless_youth.xml +2 -0
  76. data/test/fixtures/node_with_attr_name_conflicts.xml +1 -0
  77. data/test/fixtures/node_with_name_conflicts.xml +4 -0
  78. data/test/fixtures/numerology.xml +4 -0
  79. data/test/fixtures/person.xml +1 -0
  80. data/test/fixtures/person_with_guarded_mothers.xml +13 -0
  81. data/test/fixtures/person_with_mothers.xml +10 -0
  82. data/test/mocks/dictionaries.rb +57 -0
  83. data/test/mocks/mocks.rb +279 -0
  84. data/test/support/fixtures.rb +11 -0
  85. data/test/test_helper.rb +34 -0
  86. data/test/unit/definition_test.rb +235 -0
  87. data/test/unit/deprecations_test.rb +24 -0
  88. data/test/unit/to_xml_test.rb +81 -0
  89. data/test/unit/xml_attribute_test.rb +39 -0
  90. data/test/unit/xml_block_test.rb +81 -0
  91. data/test/unit/xml_bool_test.rb +122 -0
  92. data/test/unit/xml_convention_test.rb +150 -0
  93. data/test/unit/xml_hash_test.rb +115 -0
  94. data/test/unit/xml_initialize_test.rb +49 -0
  95. data/test/unit/xml_name_test.rb +141 -0
  96. data/test/unit/xml_namespace_test.rb +31 -0
  97. data/test/unit/xml_object_test.rb +207 -0
  98. data/test/unit/xml_required_test.rb +94 -0
  99. data/test/unit/xml_text_test.rb +71 -0
  100. data/website/index.html +98 -0
  101. metadata +254 -0
@@ -0,0 +1,547 @@
1
+ require 'uri'
2
+ require 'active_support'
3
+
4
+ require 'lib/roxml/definition'
5
+ require 'lib/roxml/xml'
6
+
7
+ module ROXML # :nodoc:
8
+ VERSION = '3.0.0'
9
+
10
+ def self.included(base) # :nodoc:
11
+ base.class_eval do
12
+ extend ClassMethods::Accessors,
13
+ ClassMethods::Declarations,
14
+ ClassMethods::Operations
15
+ include InstanceMethods
16
+ end
17
+ end
18
+
19
+ module InstanceMethods # :nodoc:
20
+ # Returns an XML object representing this object
21
+ def to_xml(name = self.class.tag_name)
22
+ XML::Node.create(name.to_s).tap do |root|
23
+ self.class.roxml_attrs.each do |attr|
24
+ ref = attr.to_ref(self)
25
+ value = ref.to_xml(self)
26
+ unless value.nil?
27
+ ref.update_xml(root, value)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ # This class defines the annotation methods that are mixed into your
35
+ # Ruby classes for XML mapping information and behavior.
36
+ #
37
+ # See xml_name, xml_initialize, xml, xml_reader and xml_accessor for
38
+ # available annotations.
39
+ #
40
+ module ClassMethods # :nodoc:
41
+ module Declarations
42
+ # Sets the name of the XML element that represents this class. Use this
43
+ # to override the default lowercase class name.
44
+ #
45
+ # Example:
46
+ # class BookWithPublisher
47
+ # xml_name :book
48
+ # end
49
+ #
50
+ # Without the xml_name annotation, the XML mapped tag would have been "bookwithpublisher".
51
+ #
52
+ def xml_name(name)
53
+ @roxml_tag_name = name
54
+ end
55
+
56
+ # Sets the namemespace for attributes and elements of this class. You can override
57
+ # this value on individual elements via the :from option
58
+ #
59
+ # Example:
60
+ # class Book
61
+ # xml_namespace :aws
62
+ #
63
+ # xml_reader :default_namespace
64
+ # xml_reader :different_namespace, :from => 'different:namespace'
65
+ # xml_reader :no_namespace, :from => 'no_namespace', :namespace => false
66
+ # end
67
+ #
68
+ # <aws:book xmlns:aws="http://www.aws.com/aws" xmlns:different="http://www.aws.com/different">
69
+ # <aws:default_namespace>value</aws:default_namespace>
70
+ # <different:namespace>value</different:namespace>
71
+ # <no_namespace>value</no_namespace>
72
+ # </aws:book>
73
+ #
74
+ def xml_namespace(namespace)
75
+ @roxml_namespace = namespace.to_s
76
+ end
77
+
78
+ # Sets up a mapping of namespace prefixes to hrefs, to be used by this class.
79
+ # These namespace prefixes are independent of what appears in the xml, only
80
+ # the namespace hrefs themselves need to match
81
+ #
82
+ # Example:
83
+ # class Tires
84
+ # include ROXML
85
+ #
86
+ # xml_namespaces \
87
+ # :bobsbike => 'http://bobsbikes.example.com',
88
+ # :alicesauto => 'http://alicesautosupply.example.com/'
89
+ #
90
+ # xml_reader :bike_tires, :as => [], :from => '@name', :in => 'bobsbike:tire'
91
+ # xml_reader :car_tires, :as => [], :from => '@name', :in => 'alicesauto:tire'
92
+ # end
93
+ #
94
+ # >> xml = %{
95
+ # <?xml version="1.0"?>
96
+ # <inventory xmlns="http://alicesautosupply.example.com/" xmlns:bike="http://bobsbikes.example.com">
97
+ # <tire name="super slick racing tire" />
98
+ # <tire name="all weather tire" />
99
+ # <bike:tire name="skinny street" />
100
+ # </inventory>
101
+ # }
102
+ # >> Tires.from_xml(xml).bike_tires
103
+ # => ['skinny street']
104
+ #
105
+ def xml_namespaces(namespaces)
106
+ @roxml_namespaces = namespaces.inject({}) do |all, (prefix, href)|
107
+ all[prefix.to_s] = href.to_s
108
+ all
109
+ end
110
+ end
111
+
112
+ def roxml_namespaces # :nodoc:
113
+ @roxml_namespaces || {}
114
+ end
115
+
116
+ # Most xml documents have a consistent naming convention, for example, the node and
117
+ # and attribute names might appear in CamelCase. xml_convention enables you to adapt
118
+ # the roxml default names for this object to suit this convention. For example,
119
+ # if I had a document like so:
120
+ #
121
+ # <XmlDoc>
122
+ # <MyPreciousData />
123
+ # <MoreToSee InAttrs="" />
124
+ # </XmlDoc>
125
+ #
126
+ # Then I could access it's contents by defining the following class:
127
+ #
128
+ # class XmlDoc
129
+ # include ROXML
130
+ # xml_convention :camelcase
131
+ # xml_reader :my_precious_data
132
+ # xml_reader :in_attrs, :in => 'MoreToSee'
133
+ # end
134
+ #
135
+ # You may supply a block or any #to_proc-able object as the argument,
136
+ # and it will be called against the default node and attribute names before searching
137
+ # the document. Here are some example declaration:
138
+ #
139
+ # xml_convention :upcase
140
+ # xml_convention &:camelcase
141
+ # xml_convention {|val| val.gsub('_', '').downcase }
142
+ #
143
+ # See ActiveSupport::CoreExtensions::String::Inflections for more prepackaged formats
144
+ #
145
+ # Note that the xml_convention is also applied to the default root-level tag_name,
146
+ # but in this case an underscored version of the name is applied, for convenience.
147
+ def xml_convention(to_proc_able = nil, &block)
148
+ raise ArgumentError, "conventions are already set" if @roxml_naming_convention
149
+ @roxml_naming_convention =
150
+ if to_proc_able
151
+ raise ArgumentError, "only one conventions can be set" if block_given?
152
+ to_proc_able.to_proc
153
+ elsif block_given?
154
+ block
155
+ end
156
+ end
157
+
158
+ def roxml_naming_convention # :nodoc:
159
+ (@roxml_naming_convention || begin
160
+ superclass.roxml_naming_convention if superclass.respond_to?(:roxml_naming_convention)
161
+ end).freeze
162
+ end
163
+
164
+ # Declares a reference to a certain xml element, whether an attribute, a node,
165
+ # or a typed collection of nodes. This method does not add a corresponding accessor
166
+ # to the object. For that behavior see the similar methods: .xml_reader and .xml_accessor.
167
+ #
168
+ # == Sym Option
169
+ # [sym] Symbol representing the name of the accessor.
170
+ #
171
+ # === Default naming
172
+ # This name will be the default node or attribute name searched for,
173
+ # if no other is declared. For example,
174
+ #
175
+ # xml_reader :bob
176
+ # xml_accessor :pony, :from => :attr
177
+ #
178
+ # are equivalent to:
179
+ #
180
+ # xml_reader :bob, :from => 'bob'
181
+ # xml_accessor :pony, :from => '@pony'
182
+ #
183
+ # === Boolean attributes
184
+ # If the name ends in a ?, ROXML will attempt to coerce the value to true or false,
185
+ # with True, TRUE, true and 1 mapping to true and False, FALSE, false and 0 mapping
186
+ # to false, as shown below:
187
+ #
188
+ # xml_reader :desirable?
189
+ # xml_reader :bizzare?, :from => '@BIZZARE'
190
+ #
191
+ # x = #from_xml(%{
192
+ # <object BIZZARE="1">
193
+ # <desirable>False</desirable>
194
+ # </object>
195
+ # })
196
+ # x.desirable?
197
+ # => false
198
+ # x.bizzare?
199
+ # => true
200
+ #
201
+ # If an unexpected value is encountered, the attribute will be set to nil,
202
+ # unless you provide a block, in which case the block will recived
203
+ # the actual unexpected value.
204
+ #
205
+ # #from_xml(%{
206
+ # <object>
207
+ # <desirable>Dunno</desirable>
208
+ # </object>
209
+ # }).desirable?
210
+ # => nil
211
+ #
212
+ # xml_reader :strange? do |val|
213
+ # val.upcase
214
+ # end
215
+ #
216
+ # #from_xml(%{
217
+ # <object>
218
+ # <strange>Dunno</strange>
219
+ # </object>
220
+ # }).strange?
221
+ # => DUNNO
222
+ #
223
+ # == Blocks
224
+ # You may also pass a block which manipulates the associated parsed value.
225
+ #
226
+ # class Muffins
227
+ # include ROXML
228
+ #
229
+ # xml_reader(:count, :from => 'bakers_dozens') {|val| val.to_i * 13 }
230
+ # end
231
+ #
232
+ # For hash types, the block recieves the key and value as arguments, and they should
233
+ # be returned as an array of [key, value]
234
+ #
235
+ # For array types, the entire array is passed in, and must be returned in the same fashion.
236
+ #
237
+ # == Options
238
+ # === :as
239
+ # ==== Basic Types
240
+ # Allows you to specify one of several basic types to return the value as. For example
241
+ #
242
+ # xml_reader :count, :as => Integer
243
+ #
244
+ # is equivalent to:
245
+ #
246
+ # xml_reader(:count) {|val| Integer(val) unless val.empty? }
247
+ #
248
+ # Such block shorthands for Integer, Float, Fixnum, BigDecimal, Date, Time, and DateTime
249
+ # are currently available, but only for non-Hash declarations.
250
+ #
251
+ # To reference many elements, put the desired type in a literal array. e.g.:
252
+ #
253
+ # xml_reader :counts, :as => [Integer]
254
+ #
255
+ # Even an array of text nodes can be specified with :as => []
256
+ #
257
+ # xml_reader :quotes, :as => []
258
+ #
259
+ # === Other ROXML Class
260
+ # Declares an accessor that represents another ROXML class as child XML element
261
+ # (one-to-one or composition) or array of child elements (one-to-many or
262
+ # aggregation) of this type. Default is one-to-one. For one-to-many, simply pass the class
263
+ # as the only element in an array.
264
+ #
265
+ # Composition example:
266
+ # <book>
267
+ # <publisher>
268
+ # <name>Pragmatic Bookshelf</name>
269
+ # </publisher>
270
+ # </book>
271
+ #
272
+ # Can be mapped using the following code:
273
+ # class Book
274
+ # xml_reader :publisher, :as => Publisher
275
+ # end
276
+ #
277
+ # Aggregation example:
278
+ # <library>
279
+ # <books>
280
+ # <book/>
281
+ # <book/>
282
+ # </books>
283
+ # </library>
284
+ #
285
+ # Can be mapped using the following code:
286
+ # class Library
287
+ # xml_reader :books, :as => [Book], :in => "books"
288
+ # end
289
+ #
290
+ # If you don't have the <books> tag to wrap around the list of <book> tags:
291
+ # <library>
292
+ # <name>Ruby books</name>
293
+ # <book/>
294
+ # <book/>
295
+ # </library>
296
+ #
297
+ # You can skip the wrapper argument:
298
+ # xml_reader :books, :as => [Book]
299
+ #
300
+ # ==== Hash
301
+ # Somewhere between the simplicity of a :text/:attr mapping, and the complexity of
302
+ # a full Object/Type mapping, lies the Hash mapping. It serves in the case where you have
303
+ # a collection of key-value pairs represented in your xml. You create a hash declaration by
304
+ # passing a hash mapping as the type argument. A few examples:
305
+ #
306
+ # ===== Hash of element contents
307
+ # For xml such as this:
308
+ #
309
+ # <dictionary>
310
+ # <definition>
311
+ # <word/>
312
+ # <meaning/>
313
+ # </definition>
314
+ # <definition>
315
+ # <word/>
316
+ # <meaning/>
317
+ # </definition>
318
+ # </dictionary>
319
+ #
320
+ # You can individually declare your key and value names:
321
+ # xml_reader :definitions, :as => {:key => 'word',
322
+ # :value => 'meaning'}
323
+ #
324
+ # ===== Hash of :content &c.
325
+ # For xml such as this:
326
+ #
327
+ # <dictionary>
328
+ # <definition word="quaquaversally">adjective: (of a geological formation) sloping downward from the center in all directions.</definition>
329
+ # <definition word="tergiversate">To use evasions or ambiguities; equivocate.</definition>
330
+ # </dictionary>
331
+ #
332
+ # You can individually declare the key and value, but with the attr, you need to provide both the type
333
+ # and name of that type (i.e. {:attr => :word}), because omitting the type will result in ROXML
334
+ # defaulting to :text
335
+ # xml_reader :definitions, :as => {:key => {:attr => 'word'},
336
+ # :value => :content}
337
+ #
338
+ # ===== Hash of :name &c.
339
+ # For xml such as this:
340
+ #
341
+ # <dictionary>
342
+ # <quaquaversally>adjective: (of a geological formation) sloping downward from the center in all directions.</quaquaversally>
343
+ # <tergiversate>To use evasions or ambiguities; equivocate.</tergiversate>
344
+ # </dictionary>
345
+ #
346
+ # You can pick up the node names (e.g. quaquaversally) using the :name keyword:
347
+ # xml_reader :definitions, :as => {:key => :name,
348
+ # :value => :content}
349
+ #
350
+ # === :from
351
+ # The name by which the xml value will be found, either an attribute or tag name in XML.
352
+ # Default is sym, or the singular form of sym, in the case of arrays and hashes.
353
+ #
354
+ # This value may also include XPath notation.
355
+ #
356
+ # ==== :from => :content
357
+ # When :from is set to :content, this refers to the content of the current node,
358
+ # rather than a sub-node. It is equivalent to :from => '.'
359
+ #
360
+ # Example:
361
+ # class Contributor
362
+ # xml_reader :name, :from => :content
363
+ # xml_reader :role, :from => :attr
364
+ # end
365
+ #
366
+ # To map:
367
+ # <contributor role="editor">James Wick</contributor>
368
+ #
369
+ # ==== :from => :attr
370
+ # When :from is set to :attr, this refers to the content of an attribute,
371
+ # rather than a sub-node. It is equivalent to :from => '@attribute_name'
372
+ #
373
+ # Example:
374
+ # class Book
375
+ # xml_reader :isbn, :from => "@ISBN"
376
+ # xml_accessor :title, :from => :attr # :from defaults to '@title'
377
+ # end
378
+ #
379
+ # To map:
380
+ # <book ISBN="0974514055" title="Programming Ruby: the pragmatic programmers' guide" />
381
+ #
382
+ # ==== :from => :text
383
+ # The default source, if none is specified, this means the accessor
384
+ # represents a text node from XML. This is documented for completeness
385
+ # only. You should just leave this option off when you want the default behavior,
386
+ # as in the examples below.
387
+ #
388
+ # :text is equivalent to :from => accessor_name, and you should specify the
389
+ # actual node name (and, optionally, a namespace) if it differs, as in the case of :author below.
390
+ #
391
+ # Example:
392
+ # class Book
393
+ # xml_reader :author, :from => 'Author'
394
+ # xml_accessor :description, :cdata => true
395
+ # xml_reader :title
396
+ # end
397
+ #
398
+ # To map:
399
+ # <book>
400
+ # <title>Programming Ruby: the pragmatic programmers' guide</title>
401
+ # <description><![CDATA[Probably the best Ruby book out there]]></description>
402
+ # <Author>David Thomas</Author>
403
+ # </book>
404
+ #
405
+ # Likewise, a number of :text node values can be collected in an array like so:
406
+ #
407
+ # Example:
408
+ # class Library
409
+ # xml_reader :books, :as => []
410
+ # end
411
+ #
412
+ # To map:
413
+ # <library>
414
+ # <book>To kill a mockingbird</book>
415
+ # <book>House of Leaves</book>
416
+ # <book>Gödel, Escher, Bach</book>
417
+ # </library>
418
+ #
419
+ # === Other Options
420
+ # [:in] An optional name of a wrapping tag for this XML accessor.
421
+ # This can include other xpath values, which will be joined with :from with a '/'
422
+ # [:else] Default value for attribute, if missing from the xml on .from_xml
423
+ # [:required] If true, throws RequiredElementMissing when the element isn't present
424
+ # [:frozen] If true, all results are frozen (using #freeze) at parse-time.
425
+ # [:cdata] true for values which should be input from or output as cdata elements
426
+ # [:to_xml] this proc is applied to the attributes value outputting the instance via #to_xml
427
+ # [:namespace] (false) disables or (string) overrides the default namespace declared with xml_namespace
428
+ #
429
+ def xml_attr(*syms, &block)
430
+ opts = syms.extract_options!
431
+ syms.map do |sym|
432
+ Definition.new(sym, opts, &block).tap do |attr|
433
+ if roxml_attrs.map(&:accessor).include? attr.accessor
434
+ raise "Accessor #{attr.accessor} is already defined as XML accessor in class #{self.name}"
435
+ end
436
+ @roxml_attrs << attr
437
+ end
438
+ end
439
+ end
440
+
441
+ # Declares a read-only xml reference. See xml_attr for details.
442
+ #
443
+ # Note that while xml_reader does not create a setter for this attribute,
444
+ # its value can be modified indirectly via methods. For more complete
445
+ # protection, consider the :frozen option.
446
+ def xml_reader(*syms, &block)
447
+ xml_attr(*syms, &block).each do |attr|
448
+ add_reader(attr)
449
+ end
450
+ end
451
+
452
+ # Declares a writable xml reference. See xml_attr for details.
453
+ #
454
+ # Note that while xml_accessor does create a setter for this attribute,
455
+ # you can use the :frozen option to prevent its value from being
456
+ # modified indirectly via methods.
457
+ def xml_accessor(*syms, &block)
458
+ xml_attr(*syms, &block).each do |attr|
459
+ add_reader(attr)
460
+ attr_writer(attr.attr_name)
461
+ end
462
+ end
463
+
464
+ private
465
+ def add_reader(attr)
466
+ define_method(attr.accessor) do
467
+ instance_variable_get(attr.instance_variable_name)
468
+ end
469
+ end
470
+ end
471
+
472
+ module Accessors
473
+ # Returns the tag name (also known as xml_name) of the class.
474
+ # If no tag name is set with xml_name method, returns default class name
475
+ # in lowercase.
476
+ #
477
+ # If xml_convention is set, it is called with an *underscored* version of
478
+ # the class name. This is because active support's inflector generally expects
479
+ # an underscored version, and several operations (e.g. camelcase(:lower), dasherize)
480
+ # do not work without one.
481
+ def tag_name
482
+ return roxml_tag_name if roxml_tag_name
483
+
484
+ if tag_name = name.split('::').last
485
+ roxml_naming_convention ? roxml_naming_convention.call(tag_name.underscore) : tag_name.downcase
486
+ end
487
+ end
488
+
489
+ def roxml_tag_name # :nodoc:
490
+ @roxml_tag_name || begin
491
+ superclass.roxml_tag_name if superclass.respond_to?(:roxml_tag_name)
492
+ end
493
+ end
494
+
495
+ def roxml_namespace # :nodoc:
496
+ @roxml_namespace || begin
497
+ superclass.roxml_namespace if superclass.respond_to?(:roxml_namespace)
498
+ end
499
+ end
500
+
501
+ # Returns array of internal reference objects, such as attributes
502
+ # and composed XML objects
503
+ def roxml_attrs
504
+ @roxml_attrs ||= []
505
+ (@roxml_attrs + (superclass.respond_to?(:roxml_attrs) ? superclass.roxml_attrs : [])).freeze
506
+ end
507
+ end
508
+
509
+ module Operations
510
+ #
511
+ # Creates a new Ruby object from XML using mapping information
512
+ # annotated in the class.
513
+ #
514
+ # The input data is either an XML::Node, String, Pathname, or File representing
515
+ # the XML document.
516
+ #
517
+ # Example
518
+ # book = Book.from_xml(File.read("book.xml"))
519
+ # or
520
+ # book = Book.from_xml("<book><name>Beyond Java</name></book>")
521
+ #
522
+ # _initialization_args_ passed into from_xml will be passed into
523
+ # the object's .new, prior to populating the xml_attrs.
524
+ #
525
+ # After the instatiation and xml population
526
+ #
527
+ # See also: xml_initialize
528
+ #
529
+ def from_xml(data, *initialization_args)
530
+ xml = XML::Node.from(data)
531
+
532
+ new(*initialization_args).tap do |inst|
533
+ roxml_attrs.each do |attr|
534
+ value = attr.to_ref(inst).value_in(xml)
535
+ inst.respond_to?(attr.setter) \
536
+ ? inst.send(attr.setter, value) \
537
+ : inst.instance_variable_set(attr.instance_variable_name, value)
538
+ end
539
+ inst.send(:after_parse) if inst.respond_to?(:after_parse, true)
540
+ end
541
+ rescue ArgumentError => e
542
+ raise e, e.message + " for class #{self}"
543
+ end
544
+ end
545
+ end
546
+ end
547
+