yob-roxml 3.1.6

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