ROXML 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.gitmodules +3 -0
- data/History.txt +299 -0
- data/MIT-LICENSE +18 -0
- data/README.rdoc +161 -0
- data/Rakefile +95 -0
- data/TODO +39 -0
- data/VERSION +1 -0
- data/config/website.yml +2 -0
- data/examples/amazon.rb +35 -0
- data/examples/current_weather.rb +27 -0
- data/examples/dashed_elements.rb +20 -0
- data/examples/library.rb +40 -0
- data/examples/posts.rb +27 -0
- data/examples/rails.rb +70 -0
- data/examples/twitter.rb +37 -0
- data/examples/xml/active_record.xml +70 -0
- data/examples/xml/amazon.xml +133 -0
- data/examples/xml/current_weather.xml +89 -0
- data/examples/xml/dashed_elements.xml +52 -0
- data/examples/xml/posts.xml +23 -0
- data/examples/xml/twitter.xml +422 -0
- data/lib/roxml.rb +547 -0
- data/lib/roxml/definition.rb +236 -0
- data/lib/roxml/hash_definition.rb +25 -0
- data/lib/roxml/xml.rb +43 -0
- data/lib/roxml/xml/parsers/libxml.rb +91 -0
- data/lib/roxml/xml/parsers/nokogiri.rb +77 -0
- data/lib/roxml/xml/references.rb +297 -0
- data/roxml.gemspec +201 -0
- data/spec/definition_spec.rb +486 -0
- data/spec/examples/active_record_spec.rb +40 -0
- data/spec/examples/amazon_spec.rb +54 -0
- data/spec/examples/current_weather_spec.rb +37 -0
- data/spec/examples/dashed_elements_spec.rb +20 -0
- data/spec/examples/library_spec.rb +46 -0
- data/spec/examples/post_spec.rb +24 -0
- data/spec/examples/twitter_spec.rb +32 -0
- data/spec/roxml_spec.rb +372 -0
- data/spec/shared_specs.rb +15 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/libxml.rb +3 -0
- data/spec/support/nokogiri.rb +3 -0
- data/spec/xml/attributes_spec.rb +36 -0
- data/spec/xml/namespace_spec.rb +240 -0
- data/spec/xml/namespaces_spec.rb +32 -0
- data/spec/xml/parser_spec.rb +26 -0
- data/tasks/rdoc.rake +13 -0
- data/tasks/rspec.rake +25 -0
- data/tasks/test.rake +35 -0
- data/test/fixtures/book_malformed.xml +5 -0
- data/test/fixtures/book_pair.xml +8 -0
- data/test/fixtures/book_text_with_attribute.xml +5 -0
- data/test/fixtures/book_valid.xml +5 -0
- data/test/fixtures/book_with_authors.xml +7 -0
- data/test/fixtures/book_with_contributions.xml +9 -0
- data/test/fixtures/book_with_contributors.xml +7 -0
- data/test/fixtures/book_with_contributors_attrs.xml +7 -0
- data/test/fixtures/book_with_default_namespace.xml +9 -0
- data/test/fixtures/book_with_depth.xml +6 -0
- data/test/fixtures/book_with_octal_pages.xml +4 -0
- data/test/fixtures/book_with_publisher.xml +7 -0
- data/test/fixtures/book_with_wrapped_attr.xml +3 -0
- data/test/fixtures/dictionary_of_attr_name_clashes.xml +8 -0
- data/test/fixtures/dictionary_of_attrs.xml +6 -0
- data/test/fixtures/dictionary_of_guarded_names.xml +6 -0
- data/test/fixtures/dictionary_of_mixeds.xml +4 -0
- data/test/fixtures/dictionary_of_name_clashes.xml +10 -0
- data/test/fixtures/dictionary_of_names.xml +4 -0
- data/test/fixtures/dictionary_of_texts.xml +10 -0
- data/test/fixtures/library.xml +30 -0
- data/test/fixtures/library_uppercase.xml +30 -0
- data/test/fixtures/muffins.xml +3 -0
- data/test/fixtures/nameless_ageless_youth.xml +2 -0
- data/test/fixtures/node_with_attr_name_conflicts.xml +1 -0
- data/test/fixtures/node_with_name_conflicts.xml +4 -0
- data/test/fixtures/numerology.xml +4 -0
- data/test/fixtures/person.xml +1 -0
- data/test/fixtures/person_with_guarded_mothers.xml +13 -0
- data/test/fixtures/person_with_mothers.xml +10 -0
- data/test/mocks/dictionaries.rb +57 -0
- data/test/mocks/mocks.rb +279 -0
- data/test/support/fixtures.rb +11 -0
- data/test/test_helper.rb +34 -0
- data/test/unit/definition_test.rb +235 -0
- data/test/unit/deprecations_test.rb +24 -0
- data/test/unit/to_xml_test.rb +81 -0
- data/test/unit/xml_attribute_test.rb +39 -0
- data/test/unit/xml_block_test.rb +81 -0
- data/test/unit/xml_bool_test.rb +122 -0
- data/test/unit/xml_convention_test.rb +150 -0
- data/test/unit/xml_hash_test.rb +115 -0
- data/test/unit/xml_initialize_test.rb +49 -0
- data/test/unit/xml_name_test.rb +141 -0
- data/test/unit/xml_namespace_test.rb +31 -0
- data/test/unit/xml_object_test.rb +207 -0
- data/test/unit/xml_required_test.rb +94 -0
- data/test/unit/xml_text_test.rb +71 -0
- data/website/index.html +98 -0
- metadata +254 -0
data/lib/roxml.rb
ADDED
@@ -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
|
+
|