Empact-roxml 2.1 → 2.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -3,7 +3,7 @@ visit http://roxml.rubyforge.org
3
3
 
4
4
  =Quick Start Guide
5
5
 
6
- This is a short usage example. See ROXML::ROXML_Class and packaged test cases for more information.
6
+ This is a short usage example. See ROXML::ClassMethods::Declarations and packaged test cases for more information.
7
7
 
8
8
  ==Basic Mapping
9
9
 
@@ -47,7 +47,7 @@ To save this information to an XML file:
47
47
 
48
48
  To later populate the library object from the XML file:
49
49
 
50
- lib = Library.parse(File.read("library.xml"))
50
+ lib = Library.from_xml(File.read("library.xml"))
51
51
 
52
52
  Similarly, to do a one-to-one mapping between XML objects, such as book and publisher,
53
53
  you would add a reference to another ROXML class. For example:
@@ -119,4 +119,4 @@ explicitly require one or the other, you may do the following:
119
119
  end
120
120
  require 'roxml'
121
121
 
122
- For more information on available annotations, see ROXML::ROXML_Class
122
+ For more information on available annotations, see ROXML::ClassMethods::Declarations
data/Rakefile CHANGED
@@ -38,8 +38,8 @@ task :rails_plugin=>:clobber
38
38
  desc "Publish Ruby on Rails plug-in on RubyForge"
39
39
  task :release_plugin=>:rails_plugin do |task|
40
40
  pub = Rake::SshDirPublisher.new("#{RubyForgeConfig[:user_name]}@rubyforge.org",
41
- "/var/www/gforge-projects/#{RubyForgeConfig[:unix_name]}",
42
- "pkg/rails_plugin")
41
+ "/var/www/gforge-projects/#{RubyForgeConfig[:unix_name]}",
42
+ "pkg/rails_plugin")
43
43
  pub.upload()
44
44
  end
45
45
 
@@ -60,7 +60,13 @@ Rake::TestTask.new(:bugs) do |t|
60
60
  t.verbose = true
61
61
  end
62
62
 
63
- task :test => :'test:rexml'
63
+ @test_files = 'test/unit/*_test.rb'
64
+ desc "Test ROXML using the default parser selection behavior"
65
+ task :test do
66
+ require 'lib/roxml'
67
+ require 'rake/runtest'
68
+ Rake.run_tests @test_files
69
+ end
64
70
 
65
71
  namespace :test do
66
72
  desc "Test ROXML under the LibXML parser"
@@ -68,9 +74,7 @@ namespace :test do
68
74
  module ROXML
69
75
  XML_PARSER = 'libxml'
70
76
  end
71
- require 'lib/roxml'
72
- require 'rake/runtest'
73
- Rake.run_tests 'test/unit/*_test.rb'
77
+ Rake::Task["test"].invoke
74
78
  end
75
79
 
76
80
  desc "Test ROXML under the REXML parser"
@@ -78,9 +82,12 @@ namespace :test do
78
82
  module ROXML
79
83
  XML_PARSER = 'rexml'
80
84
  end
81
- require 'lib/roxml'
82
- require 'rake/runtest'
83
- Rake.run_tests 'test/unit/*_test.rb'
85
+ Rake::Task["test"].invoke
86
+ end
87
+
88
+ desc "Runs tests under RCOV"
89
+ task :rcov do
90
+ system "rcov -T --no-html -x '^/' #{FileList[@test_files]}"
84
91
  end
85
92
  end
86
93
 
data/lib/roxml.rb CHANGED
@@ -1,361 +1,404 @@
1
1
  require 'rubygems'
2
2
  require 'extensions/enumerable'
3
3
  require 'extensions/array'
4
+ require 'extensions/object'
4
5
  require 'activesupport'
5
6
 
6
- %w(array string options xml).each do |file|
7
+ %w(extensions/array extensions/string options xml).each do |file|
7
8
  require File.join(File.dirname(__FILE__), 'roxml', file)
8
9
  end
9
10
 
10
- module ROXML
11
+ module ROXML # :nodoc:
12
+ def self.included(base) # :nodoc:
13
+ base.extend ClassMethods::Declarations
14
+ base.extend ClassMethods::Accessors
15
+ base.extend ClassMethods::Operations
16
+ base.class_eval do
17
+ include InstanceMethods::Accessors
18
+ include InstanceMethods::Conversions
19
+ end
20
+ end
21
+
22
+ module InstanceMethods # :nodoc:
23
+ # Instance method equivalents of the Class method accessors
24
+ module Accessors
25
+ # Provides access to ROXML::ClassMethods::Accessors::tag_name directly from an instance of a ROXML class
26
+ def tag_name
27
+ self.class.tag_name
28
+ end
29
+
30
+ # Provides access to ROXML::ClassMethods::Accessors::tag_refs directly from an instance of a ROXML class
31
+ def tag_refs
32
+ self.class.tag_refs
33
+ end
34
+ end
35
+
36
+ module Conversions
37
+ # Returns a LibXML::XML::Node or a REXML::Element representing this object
38
+ def to_xml(name = nil)
39
+ returning XML::Node.new_element(name || tag_name) do |root|
40
+ tag_refs.each do |ref|
41
+ if v = __send__(ref.accessor)
42
+ ref.update_xml(root, v)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
11
50
  # This class defines the annotation methods that are mixed into your
12
51
  # Ruby classes for XML mapping information and behavior.
13
52
  #
14
53
  # See xml_name, xml_construct, xml, xml_reader and xml_accessor for
15
54
  # available annotations.
16
55
  #
17
- module ROXML_Class
18
- #
19
- # Creates a new Ruby object from XML using mapping information
20
- # annotated in the class.
21
- #
22
- # The input data is either an XML::Node or a String representing
23
- # the XML document.
24
- #
25
- # Example
26
- # book = Book.parse(File.read("book.xml"))
27
- # or
28
- # book = Book.parse("<book><name>Beyond Java</name></book>")
29
- #
30
- # See also: xml_construct
31
- #
32
- def parse(data)
33
- xml = (data.kind_of?(XML::Node) ? data : XML::Parser.parse(data).root)
34
-
35
- unless xml_construction_args.empty?
36
- args = xml_construction_args.map do |arg|
37
- tag_refs.find {|ref| ref.name == arg.to_s }
38
- end.map {|ref| ref.value(xml) }
39
- new(*args)
40
- else
41
- returning allocate do |inst|
42
- tag_refs.each do |ref|
43
- ref.populate(xml, inst)
44
- end
45
- end
56
+ module ClassMethods # :nodoc:
57
+ module Declarations
58
+ # Sets the name of the XML element that represents this class. Use this
59
+ # to override the default lowercase class name.
60
+ #
61
+ # Example:
62
+ # class BookWithPublisher
63
+ # xml_name :book
64
+ # end
65
+ #
66
+ # Without the xml_name annotation, the XML mapped tag would have been "bookwithpublisher".
67
+ #
68
+ def xml_name(name)
69
+ @tag_name = name
46
70
  end
47
- end
48
71
 
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
- @tag_name = name
61
- end
72
+ # Declares an accesser to a certain xml element, whether an attribute, a node,
73
+ # or a typed collection of nodes
74
+ #
75
+ # [sym] Symbol representing the name of the accessor
76
+ #
77
+ # == Type options
78
+ # All type arguments may be used as the type argument to indicate just type,
79
+ # or used as :from, pointing to a xml name to indicate both type and attribute name.
80
+ # Also, any type may be passed via an array to indicate that multiple instances
81
+ # of the object should be returned as an array.
82
+ #
83
+ # === :attr
84
+ # Declare an accessor that represents an XML attribute.
85
+ #
86
+ # Example:
87
+ # class Book
88
+ # xml_reader :isbn, :attr => "ISBN" # 'ISBN' is used to specify :from
89
+ # xml_accessor :title, :attr # :from defaults to :title
90
+ # end
91
+ #
92
+ # To map:
93
+ # <book ISBN="0974514055" title="Programming Ruby: the pragmatic programmers' guide" />
94
+ #
95
+ # === :text
96
+ # The default type, if none is specified. Declares an accessor that
97
+ # represents a text node from XML.
98
+ #
99
+ # Example:
100
+ # class Book
101
+ # xml :author, false, :text => 'Author'
102
+ # xml_accessor :description, :text, :as => :cdata
103
+ # xml_reader :title
104
+ # end
105
+ #
106
+ # To map:
107
+ # <book>
108
+ # <title>Programming Ruby: the pragmatic programmers' guide</title>
109
+ # <description><![CDATA[Probably the best Ruby book out there]]></description>
110
+ # <Author>David Thomas</author>
111
+ # </book>
112
+ #
113
+ # Likewise, a number of :text node values can be collected in an array like so:
114
+ #
115
+ # Example:
116
+ # class Library
117
+ # xml_reader :books, [:text], :in => 'books'
118
+ # end
119
+ #
120
+ # To map:
121
+ # <library>
122
+ # <books>
123
+ # <book>To kill a mockingbird</book>
124
+ # <book>House of Leaves</book>
125
+ # <book>Gödel, Escher, Bach</book>
126
+ # </books>
127
+ # </library>
128
+ #
129
+ # === :content
130
+ # A special case of :text, this refers to the content of the current node,
131
+ # rather than a sub-node
132
+ #
133
+ # Example:
134
+ # class Contributor
135
+ # xml_reader :name, :content
136
+ # xml_reader :role, :attr
137
+ # end
138
+ #
139
+ # To map:
140
+ # <contributor role="editor">James Wick</contributor>
141
+ #
142
+ # === Hash
143
+ # Somewhere between the simplicity of a :text/:attr mapping, and the complexity of
144
+ # a full Object/Type mapping, lies the Hash mapping. It serves in the case where you have
145
+ # a collection of key-value pairs represented in your xml. You create a hash declaration by
146
+ # passing a hash mapping as the type argument. A few examples:
147
+ #
148
+ # ==== Hash of :attrs
149
+ # For xml such as this:
150
+ #
151
+ # <dictionary>
152
+ # <definitions>
153
+ # <definition dt="quaquaversally"
154
+ # dd="adjective: (of a geological formation) sloping downward from the center in all directions." />
155
+ # <definition dt="tergiversate"
156
+ # dd="To use evasions or ambiguities; equivocate." />
157
+ # </definitions>
158
+ # </dictionary>
159
+ #
160
+ # You can use the :attrs key in you has with a [:key, :value] name array:
161
+ #
162
+ # xml_reader :definitions, {:attrs => ['dt', 'dd']}, :in => :definitions
163
+ #
164
+ # ==== Hash of :texts
165
+ # For xml such as this:
166
+ #
167
+ # <dictionary>
168
+ # <definition>
169
+ # <word/>
170
+ # <meaning/>
171
+ # </definition>
172
+ # <definition>
173
+ # <word/>
174
+ # <meaning/>
175
+ # </definition>
176
+ # </dictionary>
177
+ #
178
+ # You can individually declare your key and value names:
179
+ # xml_reader :definitions, {:key => 'word',
180
+ # :value => 'meaning'}
181
+ #
182
+ # ==== Hash of :content &c.
183
+ # For xml such as this:
184
+ #
185
+ # <dictionary>
186
+ # <definition word="quaquaversally">adjective: (of a geological formation) sloping downward from the center in all directions.</definition>
187
+ # <definition word="tergiversate">To use evasions or ambiguities; equivocate.</definition>
188
+ # </dictionary>
189
+ #
190
+ # You can individually declare the key and value, but with the attr, you need to provide both the type
191
+ # and name of that type (i.e. {:attr => :word}), because omitting the type will result in ROXML
192
+ # defaulting to :text
193
+ # xml_reader :definitions, {:key => {:attr => 'word'},
194
+ # :value => :content}
195
+ #
196
+ # ==== Hash of :name &c.
197
+ # For xml such as this:
198
+ #
199
+ # <dictionary>
200
+ # <quaquaversally>adjective: (of a geological formation) sloping downward from the center in all directions.</quaquaversally>
201
+ # <tergiversate>To use evasions or ambiguities; equivocate.</tergiversate>
202
+ # </dictionary>
203
+ #
204
+ # You can pick up the node names (e.g. quaquaversally) using the :name keyword:
205
+ # xml_reader :definitions, {:key => :name,
206
+ # :value => :content}
207
+ #
208
+ # === Other ROXML Class
209
+ # Declares an accessor that represents another ROXML class as child XML element
210
+ # (one-to-one or composition) or array of child elements (one-to-many or
211
+ # aggregation) of this type. Default is one-to-one. Use :array option for one-to-many, or
212
+ # simply pass the class in an array.
213
+ #
214
+ # Composition example:
215
+ # <book>
216
+ # <publisher>
217
+ # <name>Pragmatic Bookshelf</name>
218
+ # </publisher>
219
+ # </book>
220
+ #
221
+ # Can be mapped using the following code:
222
+ # class Book
223
+ # xml_reader :publisher, Publisher
224
+ # end
225
+ #
226
+ # Aggregation example:
227
+ # <library>
228
+ # <books>
229
+ # <book/>
230
+ # <book/>
231
+ # </books>
232
+ # </library>
233
+ #
234
+ # Can be mapped using the following code:
235
+ # class Library
236
+ # xml_reader :books, [Book], :in => "books"
237
+ # end
238
+ #
239
+ # If you don't have the <books> tag to wrap around the list of <book> tags:
240
+ # <library>
241
+ # <name>Ruby books</name>
242
+ # <book/>
243
+ # <book/>
244
+ # </library>
245
+ #
246
+ # You can skip the wrapper argument:
247
+ # xml_reader :books, [Book]
248
+ #
249
+ # == Blocks
250
+ # You may also pass a block which manipulates the associated parsed value.
251
+ #
252
+ # class Muffins
253
+ # include ROXML
254
+ #
255
+ # xml_reader(:count, :from => 'bakers_dozens') {|val| val.to_i * 13 }
256
+ # end
257
+ #
258
+ # For hash types, the block recieves the key and value as arguments, and they should
259
+ # be returned as an array of [key, value]
260
+ #
261
+ # === Block Shorthands
262
+ #
263
+ # Alternatively, you may use block shorthands to specify common coercions, such that:
264
+ #
265
+ # xml_reader :count, :as => Integer
266
+ #
267
+ # is equivalent to:
268
+ #
269
+ # xml_reader(:count) {|val| Integer(val) }
270
+ #
271
+ # Block shorthands :float, Float, :integer and Integer are currently available,
272
+ # but only for non-Hash declarations.
273
+ #
274
+ # == Other options
275
+ # [:from] The name by which the xml value will be found, either an attribute or tag name in XML. Default is sym, or the singular form of sym, in the case of arrays and hashes.
276
+ # [:as] :cdata for character data; :integer, Integer, :float, Float to coerce to Integer or Float respectively
277
+ # [:in] An optional name of a wrapping tag for this XML accessor
278
+ # [:else] Default value for attribute, if missing
279
+ # [:required] If true, throws RequiredElementMissing when the element isn't present
280
+ #
281
+ def xml(sym, writable = false, type_and_or_opts = :text, opts = nil, &block)
282
+ opts = Opts.new(sym, *[type_and_or_opts, opts].compact, &block)
62
283
 
63
- # Declares an accesser to a certain xml element, whether an attribute, a node,
64
- # or a typed collection of nodes
65
- #
66
- # [sym] Symbol representing the name of the accessor
67
- #
68
- # == Type options
69
- # All type arguments may be used as the type argument to indicate just type,
70
- # or used as :from, pointing to a xml name to indicate both type and attribute name.
71
- # Also, any type may be passed via an array to indicate that multiple instances
72
- # of the object should be returned as an array.
73
- #
74
- # === :attr
75
- # Declare an accessor that represents an XML attribute.
76
- #
77
- # Example:
78
- # class Book
79
- # xml_reader :isbn, :attr => "ISBN" # 'ISBN' is used to specify :from
80
- # xml_accessor :title, :attr # :from defaults to :title
81
- # end
82
- #
83
- # To map:
84
- # <book ISBN="0974514055" title="Programming Ruby: the pragmatic programmers' guide" />
85
- #
86
- # === :text
87
- # The default type, if none is specified. Declares an accessor that
88
- # represents a text node from XML.
89
- #
90
- # Example:
91
- # class Book
92
- # xml :author, false, :text => 'Author'
93
- # xml_accessor :description, :text, :as => :cdata
94
- # xml_reader :title
95
- # end
96
- #
97
- # To map:
98
- # <book>
99
- # <title>Programming Ruby: the pragmatic programmers' guide</title>
100
- # <description><![CDATA[Probably the best Ruby book out there]]></description>
101
- # <Author>David Thomas</author>
102
- # </book>
103
- #
104
- # Likewise, a number of :text node values can be collected in an array like so:
105
- #
106
- # Example:
107
- # class Library
108
- # xml_reader :books, [:text], :in => 'books'
109
- # end
110
- #
111
- # To map:
112
- # <library>
113
- # <books>
114
- # <book>To kill a mockingbird</book>
115
- # <book>House of Leaves</book>
116
- # <book>Gödel, Escher, Bach</book>
117
- # </books>
118
- # </library>
119
- #
120
- # === :content
121
- # A special case of :text, this refers to the content of the current node,
122
- # rather than a sub-node
123
- #
124
- # Example:
125
- # class Contributor
126
- # xml_reader :name, :content
127
- # xml_reader :role, :attr
128
- # end
129
- #
130
- # To map:
131
- # <contributor role="editor">James Wick</contributor>
132
- #
133
- # === Hash
134
- # Somewhere between the simplicity of a :text/:attr mapping, and the complexity of
135
- # a full Object/Type mapping, lies the Hash mapping. It serves in the case where you have
136
- # a collection of key-value pairs represented in your xml. You create a hash declaration by
137
- # passing a hash mapping as the type argument. A few examples:
138
- #
139
- # ==== Hash of :attrs
140
- # For xml such as this:
141
- #
142
- # <dictionary>
143
- # <definitions>
144
- # <definition dt="quaquaversally"
145
- # dd="adjective: (of a geological formation) sloping downward from the center in all directions." />
146
- # <definition dt="tergiversate"
147
- # dd="To use evasions or ambiguities; equivocate." />
148
- # </definitions>
149
- # </dictionary>
150
- #
151
- # You can use the :attrs key in you has with a [:key, :value] name array:
152
- #
153
- # xml_reader :definitions, {:attrs => ['dt', 'dd']}, :in => :definitions
154
- #
155
- # ==== Hash of :texts
156
- # For xml such as this:
157
- #
158
- # <dictionary>
159
- # <definition>
160
- # <word/>
161
- # <meaning/>
162
- # </definition>
163
- # <definition>
164
- # <word/>
165
- # <meaning/>
166
- # </definition>
167
- # </dictionary>
168
- #
169
- # You can individually declare your key and value names:
170
- # xml_reader :definitions, {:key => 'word',
171
- # :value => 'meaning'}
172
- #
173
- # ==== Hash of :content &c.
174
- # For xml such as this:
175
- #
176
- # <dictionary>
177
- # <definition word="quaquaversally">adjective: (of a geological formation) sloping downward from the center in all directions.</definition>
178
- # <definition word="tergiversate">To use evasions or ambiguities; equivocate.</definition>
179
- # </dictionary>
180
- #
181
- # You can individually declare the key and value, but with the attr, you need to provide both the type
182
- # and name of that type (i.e. {:attr => :word}), because omitting the type will result in ROXML
183
- # defaulting to :text
184
- # xml_reader :definitions, {:key => {:attr => 'word'},
185
- # :value => :content}
186
- #
187
- # ==== Hash of :name &c.
188
- # For xml such as this:
189
- #
190
- # <dictionary>
191
- # <quaquaversally>adjective: (of a geological formation) sloping downward from the center in all directions.</quaquaversally>
192
- # <tergiversate>To use evasions or ambiguities; equivocate.</tergiversate>
193
- # </dictionary>
194
- #
195
- # You can pick up the node names (e.g. quaquaversally) using the :name keyword:
196
- # xml_reader :definitions, {:key => :name,
197
- # :value => :content}
198
- #
199
- # === Other ROXML Class
200
- # Declares an accessor that represents another ROXML class as child XML element
201
- # (one-to-one or composition) or array of child elements (one-to-many or
202
- # aggregation) of this type. Default is one-to-one. Use :array option for one-to-many, or
203
- # simply pass the class in an array.
204
- #
205
- # Composition example:
206
- # <book>
207
- # <publisher>
208
- # <name>Pragmatic Bookshelf</name>
209
- # </publisher>
210
- # </book>
211
- #
212
- # Can be mapped using the following code:
213
- # class Book
214
- # xml_reader :publisher, Publisher
215
- # end
216
- #
217
- # Aggregation example:
218
- # <library>
219
- # <books>
220
- # <book/>
221
- # <book/>
222
- # </books>
223
- # </library>
224
- #
225
- # Can be mapped using the following code:
226
- # class Library
227
- # xml_reader :books, [Book], :in => "books"
228
- # end
229
- #
230
- # If you don't have the <books> tag to wrap around the list of <book> tags:
231
- # <library>
232
- # <name>Ruby books</name>
233
- # <book/>
234
- # <book/>
235
- # </library>
236
- #
237
- # You can skip the wrapper argument:
238
- # xml_reader :books, [Book]
239
- #
240
- # == Blocks
241
- # For readonly attributes, you may pass a block which manipulates the associated parsed value.
242
- #
243
- # class Muffins
244
- # include ROXML
245
- #
246
- # xml_reader :count, :from => 'bakers_dozens' {|val| val.to_i * 13 }
247
- # end
248
- #
249
- # For hash types, the block recieves the key and value as arguments, and they should
250
- # be returned as an array of [key, value]
251
- #
252
- # == Other options
253
- # [:from] The name by which the xml value will be found, either an attribute or tag name in XML. Default is sym, or the singular form of sym, in the case of arrays and hashes.
254
- # [:as] :cdata for character data
255
- # [:in] An optional name of a wrapping tag for this XML accessor
256
- # [:else] Default value for attribute, if missing
257
- #
258
- def xml(sym, writable = false, type_and_or_opts = :text, opts = nil, &block)
259
- opts = Opts.new(sym, *[type_and_or_opts, opts].compact)
284
+ tag_refs << case opts.type
285
+ when :attr then XMLAttributeRef
286
+ when :content then XMLTextRef
287
+ when :text then XMLTextRef
288
+ when :hash then XMLHashRef
289
+ when Symbol then raise ArgumentError, "Invalid type argument #{opts.type}"
290
+ else XMLObjectRef
291
+ end.new(sym, opts)
260
292
 
261
- tag_refs << case opts.type
262
- when :attr then XMLAttributeRef
263
- when :content then XMLTextRef
264
- when :text then XMLTextRef
265
- when :hash then XMLHashRef
266
- when Symbol then raise ArgumentError, "Invalid type argument #{opts.type}"
267
- else XMLObjectRef
268
- end.new(sym, opts, &block)
293
+ add_accessor(sym, writable, opts.array?, opts.default)
294
+ end
269
295
 
270
- add_accessor(sym, writable, opts.array?, opts.default)
271
- end
296
+ # Declares a read-only xml reference. See xml for details.
297
+ def xml_reader(sym, type_and_or_opts = :text, opts = nil, &block)
298
+ xml sym, false, type_and_or_opts, opts, &block
299
+ end
272
300
 
273
- # Declares a read-only xml reference. See xml for details.
274
- def xml_reader(sym, type_and_or_opts = :text, opts = nil, &block)
275
- xml sym, false, type_and_or_opts, opts, &block
276
- end
301
+ # Declares a writable xml reference. See xml for details.
302
+ def xml_accessor(sym, type_and_or_opts = :text, opts = nil, &block)
303
+ xml sym, true, type_and_or_opts, opts, &block
304
+ end
277
305
 
278
- # Declares a writable xml reference. See xml for details.
279
- def xml_accessor(sym, type_and_or_opts = :text, opts = nil, &block)
280
- xml sym, true, type_and_or_opts, opts, &block
281
- end
306
+ # On parse, call the target object's initialize function with the listed arguments
307
+ def xml_construct(*args)
308
+ if missing_tag = args.detect {|arg| !tag_refs.map(&:name).include?(arg.to_s) }
309
+ raise ArgumentError, "All construction tags must be declared first using xml, " +
310
+ "xml_reader, or xml_accessor. #{missing_tag} is missing. " +
311
+ tag_refs.map(&:name).join(', ') + ' are declared.'
312
+ end
313
+ @xml_construction_args = args
314
+ end
282
315
 
283
- def xml_construction_args # ::nodoc::
284
- @xml_construction_args ||= []
285
- end
316
+ private
317
+ def assert_accessor(name)
318
+ @tag_accessors ||= []
319
+ raise "Accessor #{name} is already defined as XML accessor in class #{self}" if @tag_accessors.include?(name)
320
+ @tag_accessors << name
321
+ end
322
+
323
+ def add_accessor(name, writable, as_array, default = nil)
324
+ assert_accessor(name)
325
+ unless instance_methods.include?(name)
326
+ default ||= Array.new if as_array
286
327
 
287
- # On parse, call the target object's initialize function with the listed arguments
288
- def xml_construct(*args)
289
- if missing_tag = args.detect {|arg| !tag_refs.map(&:name).include?(arg.to_s) }
290
- raise ArgumentError, "All construction tags must be declared first using xml, " +
291
- "xml_reader, or xml_accessor. #{missing_tag} is missing. " +
292
- tag_refs.map(&:name).join(', ') + ' are declared.'
328
+ define_method(name) do
329
+ val = instance_variable_get("@#{name}")
330
+ if val.nil?
331
+ val = default.duplicable? ? default.dup : default
332
+ instance_variable_set("@#{name}", val)
333
+ end
334
+ val
335
+ end
336
+ end
337
+ if writable && !instance_methods.include?("#{name}=")
338
+ define_method("#{name}=") do |v|
339
+ instance_variable_set("@#{name}", v)
340
+ end
341
+ end
293
342
  end
294
- @xml_construction_args = args
295
343
  end
296
344
 
297
- # Returns the tag name (also known as xml_name) of the class.
298
- # If no tag name is set with xml_name method, returns default class name
299
- # in lowercase.
300
- def tag_name
301
- @tag_name ||= name.split('::').last.downcase
302
- end
345
+ module Accessors
346
+ def xml_construction_args # :nodoc:
347
+ @xml_construction_args ||= []
348
+ end
303
349
 
304
- # Returns array of internal reference objects, such as attributes
305
- # and composed XML objects
306
- def tag_refs
307
- @xml_refs ||= []
308
- end
309
350
 
310
- private
311
- def assert_accessor(name)
312
- @tag_accessors ||= []
313
- raise "Accessor #{name} is already defined as XML accessor in class #{self}" if @tag_accessors.include?(name)
314
- @tag_accessors << name
351
+ # Returns the tag name (also known as xml_name) of the class.
352
+ # If no tag name is set with xml_name method, returns default class name
353
+ # in lowercase.
354
+ def tag_name
355
+ @tag_name ||= name.split('::').last.downcase
356
+ end
357
+
358
+ # Returns array of internal reference objects, such as attributes
359
+ # and composed XML objects
360
+ def tag_refs
361
+ @xml_refs ||= []
362
+ end
315
363
  end
316
364
 
317
- def add_accessor(name, writable, as_array, default = nil)
318
- assert_accessor(name)
319
- unless instance_methods.include?(name)
320
- default ||= Array.new if as_array
365
+ module Operations
366
+ #
367
+ # Creates a new Ruby object from XML using mapping information
368
+ # annotated in the class.
369
+ #
370
+ # The input data is either an XML::Node or a String representing
371
+ # the XML document.
372
+ #
373
+ # Example
374
+ # book = Book.from_xml(File.read("book.xml"))
375
+ # or
376
+ # book = Book.from_xml("<book><name>Beyond Java</name></book>")
377
+ #
378
+ # See also: xml_construct
379
+ #
380
+ def from_xml(data)
381
+ xml = (data.kind_of?(XML::Node) ? data : XML::Parser.parse(data).root)
321
382
 
322
- define_method(name) do
323
- val = instance_variable_get("@#{name}")
324
- if val.nil?
325
- val = default
326
- instance_variable_set("@#{name}", val)
383
+ unless xml_construction_args.empty?
384
+ args = xml_construction_args.map do |arg|
385
+ tag_refs.find {|ref| ref.name == arg.to_s }
386
+ end.map {|ref| ref.value(xml) }
387
+ new(*args)
388
+ else
389
+ returning allocate do |inst|
390
+ tag_refs.each do |ref|
391
+ ref.populate(xml, inst)
392
+ end
327
393
  end
328
- val
329
394
  end
330
395
  end
331
- if writable && !instance_methods.include?("#{name}=")
332
- define_method("#{name}=") do |v|
333
- instance_variable_set("@#{name}", v)
334
- end
335
- end
336
- end
337
- end
338
-
339
- class << self
340
- #
341
- # Extends the klass with the ROXML_Class module methods.
342
- #
343
- def included(klass) # ::nodoc::
344
- super
345
- klass.__send__(:extend, ROXML_Class)
346
- end
347
- end
348
396
 
349
- #
350
- # To make it easier to reference the class's
351
- # attributes all method calls to the instance that
352
- # doesn't match an instance method are forwarded to the
353
- # class's singleton instance. Only methods 'tag_name' and 'tag_refs' are delegated.
354
- def method_missing(name, *args)
355
- if [:tag_name, :tag_refs].include? name
356
- self.class.__send__(name, *args)
357
- else
358
- super
397
+ # Deprecated in favor of #from_xml
398
+ def parse(data)
399
+ ActiveSupport::Deprecation.warn '#parse has been deprecated, please use #from_xml instead'
400
+ from_xml(data)
401
+ end
359
402
  end
360
403
  end
361
404
  end