Empact-roxml 2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +18 -0
- data/README.rdoc +122 -0
- data/Rakefile +104 -0
- data/lib/roxml.rb +362 -0
- data/lib/roxml/array.rb +15 -0
- data/lib/roxml/options.rb +175 -0
- data/lib/roxml/string.rb +35 -0
- data/lib/roxml/xml.rb +243 -0
- data/lib/roxml/xml/libxml.rb +63 -0
- data/lib/roxml/xml/rexml.rb +59 -0
- data/roxml.gemspec +78 -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_publisher.xml +7 -0
- data/test/fixtures/dictionary_of_attrs.xml +6 -0
- data/test/fixtures/dictionary_of_mixeds.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/nameless_ageless_youth.xml +2 -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 +56 -0
- data/test/mocks/mocks.rb +212 -0
- data/test/test_helper.rb +16 -0
- data/test/unit/options_test.rb +62 -0
- data/test/unit/roxml_test.rb +24 -0
- data/test/unit/string_test.rb +11 -0
- data/test/unit/to_xml_test.rb +75 -0
- data/test/unit/xml_attribute_test.rb +34 -0
- data/test/unit/xml_construct_test.rb +19 -0
- data/test/unit/xml_hash_test.rb +54 -0
- data/test/unit/xml_name_test.rb +14 -0
- data/test/unit/xml_namespace_test.rb +36 -0
- data/test/unit/xml_object_test.rb +94 -0
- data/test/unit/xml_text_test.rb +57 -0
- metadata +110 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2004-2008 by Ben Woosley, Zak Mandhro and Anders Engstrom
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
6
|
+
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
7
|
+
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
8
|
+
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
12
|
+
portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
ROXML Ruby Object to XML mapping library. For more information
|
2
|
+
visit http://roxml.rubyforge.org
|
3
|
+
|
4
|
+
=Quick Start Guide
|
5
|
+
|
6
|
+
This is a short usage example. See ROXML::ROXML_Class and packaged test cases for more information.
|
7
|
+
|
8
|
+
==Basic Mapping
|
9
|
+
|
10
|
+
Consider an XML document representing a Library containing a number of Books. You
|
11
|
+
can map this structure to Ruby classes that provide addition useful behavior. With
|
12
|
+
ROXML, you can annotate the Ruby classes as follows:
|
13
|
+
|
14
|
+
class Book
|
15
|
+
include ROXML
|
16
|
+
|
17
|
+
xml_reader :isbn, :attr => "ISBN" # attribute with name 'ISBN'
|
18
|
+
xml_reader :title
|
19
|
+
xml_reader :description, :as => :cdata # text node with cdata protection
|
20
|
+
xml_reader :author
|
21
|
+
end
|
22
|
+
|
23
|
+
class Library
|
24
|
+
include ROXML
|
25
|
+
|
26
|
+
xml_accessor :name, :from => "NAME", :as => :cdata
|
27
|
+
xml_accessor :books, [Book], :in => "books"
|
28
|
+
end
|
29
|
+
|
30
|
+
To create a library and put a number of books in it we could run the following code:
|
31
|
+
|
32
|
+
book = Book.new()
|
33
|
+
book.isbn = "0201710897"
|
34
|
+
book.title = "The PickAxe"
|
35
|
+
book.description = "Best Ruby book out there!"
|
36
|
+
book.author = "David Thomas, Andrew Hunt, Dave Thomas"
|
37
|
+
|
38
|
+
lib = Library.new()
|
39
|
+
lib.name = "Favorite Books"
|
40
|
+
lib << book
|
41
|
+
|
42
|
+
To save this information to an XML file:
|
43
|
+
|
44
|
+
File.open("library.xml", "w") do |f|
|
45
|
+
lib.to_xml.write(f, 0)
|
46
|
+
end
|
47
|
+
|
48
|
+
To later populate the library object from the XML file:
|
49
|
+
|
50
|
+
lib = Library.parse(File.read("library.xml"))
|
51
|
+
|
52
|
+
Similarly, to do a one-to-one mapping between XML objects, such as book and publisher,
|
53
|
+
you would add a reference to another ROXML class. For example:
|
54
|
+
|
55
|
+
<book isbn="0974514055">
|
56
|
+
<title>Programming Ruby - 2nd Edition</title>
|
57
|
+
<description>Second edition of the great book.</description>
|
58
|
+
<publisher>
|
59
|
+
<name>Pragmatic Bookshelf</name>
|
60
|
+
</publisher>
|
61
|
+
</book>
|
62
|
+
|
63
|
+
can be mapped using the following code:
|
64
|
+
|
65
|
+
class BookWithPublisher
|
66
|
+
include ROXML
|
67
|
+
|
68
|
+
xml_name :book
|
69
|
+
xml_reader :publisher, Publisher
|
70
|
+
end
|
71
|
+
|
72
|
+
Note: In the above example, _xml_name_ annotation tells ROXML to set the element
|
73
|
+
name to "book" for mapping to XML. The default is XML element name is the class name in lowercase; "bookwithpublisher"
|
74
|
+
in this case.
|
75
|
+
|
76
|
+
== Manipulation
|
77
|
+
|
78
|
+
Extending the above examples, say you want to parse a book's page count and have it available as an Integer.
|
79
|
+
In such a case, you can extend any object with a block to manipulate it's value at parse time. For example:
|
80
|
+
|
81
|
+
class Child
|
82
|
+
include ROXML
|
83
|
+
|
84
|
+
xml_reader :age, :attr do |val|
|
85
|
+
Integer(val)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
The result of the block above is stored, rather than the actual value parsed from the document.
|
90
|
+
|
91
|
+
== Construction
|
92
|
+
|
93
|
+
Complicated initialization may require action on multiple attributes of an object. As such, you can
|
94
|
+
use xml_construct to cause your ROXML object to call its own constructor. For example:
|
95
|
+
|
96
|
+
class Measurement
|
97
|
+
include ROXML
|
98
|
+
|
99
|
+
xml_reader :units, :attr
|
100
|
+
xml_reader :value, :content
|
101
|
+
|
102
|
+
xml_construct :value, :units
|
103
|
+
|
104
|
+
def initialize(value, units)
|
105
|
+
# translate units & value into metric, for example
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
Will, on parse, read all listed xml attributes (units and value, in this case), then call initialize
|
110
|
+
with the arguments listed after the xml_construct call.
|
111
|
+
|
112
|
+
== Selecting a parser ==
|
113
|
+
|
114
|
+
By default, ROXML will use LibXML if it is available, or otherwise REXML. If you'd like to
|
115
|
+
explicitly require one or the other, you may do the following:
|
116
|
+
|
117
|
+
module ROXML
|
118
|
+
XML_PARSER = 'libxml' # or 'rexml'
|
119
|
+
end
|
120
|
+
require 'roxml'
|
121
|
+
|
122
|
+
For more information on available annotations, see ROXML::ROXML_Class
|
data/Rakefile
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# Rake libraries used
|
2
|
+
require "rubygems"
|
3
|
+
require "rails_plugin_package_task"
|
4
|
+
require "rake/rdoctask"
|
5
|
+
require "rake/contrib/rubyforgepublisher"
|
6
|
+
require "rake/contrib/publisher"
|
7
|
+
require 'rake/gempackagetask'
|
8
|
+
require 'rake/testtask'
|
9
|
+
|
10
|
+
# load settings
|
11
|
+
spec = eval(IO.read("roxml.gemspec"))
|
12
|
+
|
13
|
+
# Provide the username used to upload website etc.
|
14
|
+
RubyForgeConfig = {
|
15
|
+
:unix_name=>"roxml",
|
16
|
+
:user_name=>"zakmandhro"
|
17
|
+
}
|
18
|
+
|
19
|
+
task :default => :test
|
20
|
+
|
21
|
+
Rake::RDocTask.new do |rd|
|
22
|
+
rd.rdoc_dir = "doc"
|
23
|
+
rd.rdoc_files.include('MIT-LICENSE', 'README.rdoc', "lib/**/*.rb")
|
24
|
+
rd.options << '--main' << 'README.rdoc' << '--title' << 'ROXML Documentation'
|
25
|
+
end
|
26
|
+
|
27
|
+
Rake::RailsPluginPackageTask.new(spec.name, spec.version) do |p|
|
28
|
+
p.package_files = FileList[
|
29
|
+
"lib/**/*.rb", "*.txt", "README.rdoc", "Rakefile",
|
30
|
+
"rake/**/*", "test/**/*.rb", "test/**/*.xml"]
|
31
|
+
p.plugin_files = FileList["rails_plugin/**/*"]
|
32
|
+
p.extra_links = {"Project page" => spec.homepage,
|
33
|
+
"Author: Zak Mandhro" => 'http://rubyforge.org/users/zakmandhro/'}
|
34
|
+
p.verbose = true
|
35
|
+
end
|
36
|
+
task :rails_plugin=>:clobber
|
37
|
+
|
38
|
+
desc "Publish Ruby on Rails plug-in on RubyForge"
|
39
|
+
task :release_plugin=>:rails_plugin do |task|
|
40
|
+
pub = Rake::SshDirPublisher.new("#{RubyForgeConfig[:user_name]}@rubyforge.org",
|
41
|
+
"/var/www/gforge-projects/#{RubyForgeConfig[:unix_name]}",
|
42
|
+
"pkg/rails_plugin")
|
43
|
+
pub.upload()
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "Publish and plugin site on RubyForge"
|
47
|
+
task :publish do |task|
|
48
|
+
pub = Rake::RubyForgePublisher.new(RubyForgeConfig[:unix_name], RubyForgeConfig[:user_name])
|
49
|
+
pub.upload()
|
50
|
+
end
|
51
|
+
|
52
|
+
desc "Install the gem"
|
53
|
+
task :install => [:package] do
|
54
|
+
sh %{sudo gem install pkg/#{spec.name}-#{spec.version}}
|
55
|
+
end
|
56
|
+
|
57
|
+
Rake::TestTask.new(:bugs) do |t|
|
58
|
+
t.libs << 'test'
|
59
|
+
t.test_files = FileList['test/bugs/*_bugs.rb']
|
60
|
+
t.verbose = true
|
61
|
+
end
|
62
|
+
|
63
|
+
task :test => :'test:rexml'
|
64
|
+
|
65
|
+
namespace :test do
|
66
|
+
desc "Test ROXML under the LibXML parser"
|
67
|
+
task :libxml do
|
68
|
+
module ROXML
|
69
|
+
XML_PARSER = 'libxml'
|
70
|
+
end
|
71
|
+
require 'lib/roxml'
|
72
|
+
require 'rake/runtest'
|
73
|
+
Rake.run_tests 'test/unit/*_test.rb'
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "Test ROXML under the REXML parser"
|
77
|
+
task :rexml do
|
78
|
+
module ROXML
|
79
|
+
XML_PARSER = 'rexml'
|
80
|
+
end
|
81
|
+
require 'lib/roxml'
|
82
|
+
require 'rake/runtest'
|
83
|
+
Rake.run_tests 'test/unit/*_test.rb'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
desc "Create the ZIP package"
|
88
|
+
Rake::PackageTask.new(spec.name, spec.version) do |p|
|
89
|
+
p.need_zip = true
|
90
|
+
p.package_files = FileList[
|
91
|
+
"lib/**/*.rb", "*.txt", "README.rdoc", "Rakefile",
|
92
|
+
"rake/**/*","test/**/*.rb", "test/**/*.xml", "html/**/*"]
|
93
|
+
end
|
94
|
+
|
95
|
+
desc "Create the plugin package"
|
96
|
+
|
97
|
+
task :package=>:rdoc
|
98
|
+
task :rdoc=>:test
|
99
|
+
|
100
|
+
desc "Create a RubyGem project"
|
101
|
+
Rake::GemPackageTask.new(spec).define
|
102
|
+
|
103
|
+
desc "Clobber generated files"
|
104
|
+
task :clobber=>[:clobber_package, :clobber_rdoc]
|
data/lib/roxml.rb
ADDED
@@ -0,0 +1,362 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'extensions/enumerable'
|
3
|
+
require 'extensions/array'
|
4
|
+
require 'activesupport'
|
5
|
+
|
6
|
+
%w(array string options xml).each do |file|
|
7
|
+
require File.join(File.dirname(__FILE__), 'roxml', file)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ROXML
|
11
|
+
# This class defines the annotation methods that are mixed into your
|
12
|
+
# Ruby classes for XML mapping information and behavior.
|
13
|
+
#
|
14
|
+
# See xml_name, xml_construct, xml, xml_reader and xml_accessor for
|
15
|
+
# available annotations.
|
16
|
+
#
|
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
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
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
|
62
|
+
|
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)
|
260
|
+
|
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)
|
269
|
+
|
270
|
+
add_accessor(sym, writable, opts.array?, opts.default)
|
271
|
+
end
|
272
|
+
|
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
|
277
|
+
|
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
|
282
|
+
|
283
|
+
def xml_construction_args # ::nodoc::
|
284
|
+
@xml_construction_args ||= []
|
285
|
+
end
|
286
|
+
|
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.'
|
293
|
+
end
|
294
|
+
@xml_construction_args = args
|
295
|
+
end
|
296
|
+
|
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
|
303
|
+
|
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
|
+
|
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
|
315
|
+
end
|
316
|
+
|
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
|
321
|
+
|
322
|
+
define_method(name) do
|
323
|
+
val = instance_variable_get("@#{name}")
|
324
|
+
if val.nil?
|
325
|
+
val = default
|
326
|
+
instance_variable_set("@#{name}", val)
|
327
|
+
end
|
328
|
+
val
|
329
|
+
end
|
330
|
+
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
|
+
|
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
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|