Empact-roxml 2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/lib/roxml/array.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class Array
|
2
|
+
# Translates an array into a hash, where each element of the array is
|
3
|
+
# an array with 2 elements:
|
4
|
+
#
|
5
|
+
# >> [[:key, :value], [1, 2], ['key', 'value']].to_h
|
6
|
+
# => {:key => :value, 1 => 2, 'key' => 'value}
|
7
|
+
#
|
8
|
+
def to_h
|
9
|
+
returning({}) do |result|
|
10
|
+
each do |(k, v)|
|
11
|
+
result[k] = v
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module ROXML
|
2
|
+
HASH_KEYS = [:attrs, :key, :value].freeze
|
3
|
+
TYPE_KEYS = [:attr, :text, :hash, :content].freeze
|
4
|
+
|
5
|
+
class HashDesc # ::nodoc::
|
6
|
+
attr_reader :key, :value, :wrapper
|
7
|
+
|
8
|
+
def initialize(opts, wrapper)
|
9
|
+
unless (invalid_keys = opts.keys - HASH_KEYS).empty?
|
10
|
+
raise ArgumentError, "Invalid Hash description keys: #{invalid_keys.join(', ')}"
|
11
|
+
end
|
12
|
+
|
13
|
+
@wrapper = wrapper
|
14
|
+
if opts.has_key? :attrs
|
15
|
+
@key = to_ref(opts, :attr, opts[:attrs][0])
|
16
|
+
@value = to_ref(opts, :attr, opts[:attrs][1])
|
17
|
+
else
|
18
|
+
@key = to_ref opts, *fetch_element(opts, :key)
|
19
|
+
@value = to_ref opts, *fetch_element(opts, :value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def types
|
24
|
+
[@key.class, @value.class]
|
25
|
+
end
|
26
|
+
|
27
|
+
def names
|
28
|
+
[@key.name, @value.name]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def fetch_element(opts, what)
|
33
|
+
case opts[what]
|
34
|
+
when Hash
|
35
|
+
raise ArgumentError, "Hash #{what} is over-specified: #{opts[what].pp_s}" unless opts[what].keys.one?
|
36
|
+
type = opts[what].keys.first
|
37
|
+
[type, opts[what][type]]
|
38
|
+
when :content
|
39
|
+
[:content, opts[:name]]
|
40
|
+
when :name
|
41
|
+
[:name, '*']
|
42
|
+
when String
|
43
|
+
[:text, opts[what]]
|
44
|
+
when Symbol
|
45
|
+
[:text, opts[what]]
|
46
|
+
else
|
47
|
+
raise ArgumentError, "unrecognized hash parameter: #{what} => #{opts[what]}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_ref(args, type, name)
|
52
|
+
case type
|
53
|
+
when :attr
|
54
|
+
XMLAttributeRef.new(nil, to_hash_args(args, type, name))
|
55
|
+
when :text
|
56
|
+
XMLTextRef.new(nil, to_hash_args(args, type, name))
|
57
|
+
when Symbol
|
58
|
+
XMLTextRef.new(nil, to_hash_args(args, type, name))
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Missing key description #{{:type => type, :name => name}.pp_s}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_hash_args(args, type, name)
|
65
|
+
args = [args] unless args.is_a? Array
|
66
|
+
|
67
|
+
if args.one? && !(args.only.keys & HASH_KEYS).empty?
|
68
|
+
opts = {type => name}
|
69
|
+
if type == :content
|
70
|
+
opts[:type] = :text
|
71
|
+
(opts[:as] ||= []) << :content
|
72
|
+
end
|
73
|
+
Opts.new(name, opts)
|
74
|
+
else
|
75
|
+
opts = args.extract_options!
|
76
|
+
raise opts.to_s
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Opts # ::nodoc::
|
82
|
+
attr_reader :type, :hash
|
83
|
+
|
84
|
+
def initialize(sym, *args)
|
85
|
+
@opts = extract_options!(args)
|
86
|
+
|
87
|
+
@opts.reverse_merge!(:from => sym.to_s, :as => [], :else => nil, :in => nil)
|
88
|
+
@opts[:as] = [*@opts[:as]]
|
89
|
+
@type = extract_type(args)
|
90
|
+
|
91
|
+
@name = @opts[:from].to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
def name=(n)
|
95
|
+
@name = n.to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
def name
|
99
|
+
enumerable? ? @name.singularize : @name
|
100
|
+
end
|
101
|
+
|
102
|
+
def hash
|
103
|
+
@hash ||= HashDesc.new(@opts.delete(:hash), name) if hash?
|
104
|
+
end
|
105
|
+
|
106
|
+
def default
|
107
|
+
@opts[:else]
|
108
|
+
end
|
109
|
+
|
110
|
+
def enumerable?
|
111
|
+
hash? || array?
|
112
|
+
end
|
113
|
+
|
114
|
+
def hash?
|
115
|
+
@type == :hash
|
116
|
+
end
|
117
|
+
|
118
|
+
def content?
|
119
|
+
@type == :content
|
120
|
+
end
|
121
|
+
|
122
|
+
def array?
|
123
|
+
@opts[:as].include? :array
|
124
|
+
end
|
125
|
+
|
126
|
+
def cdata?
|
127
|
+
@opts[:as].include? :cdata
|
128
|
+
end
|
129
|
+
|
130
|
+
def wrapper
|
131
|
+
@opts[:in]
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
def extract_options!(args)
|
136
|
+
opts = args.extract_options!
|
137
|
+
unless (opts.keys & HASH_KEYS).empty?
|
138
|
+
args.push(opts)
|
139
|
+
opts = {}
|
140
|
+
end
|
141
|
+
opts
|
142
|
+
end
|
143
|
+
|
144
|
+
def extract_type(args)
|
145
|
+
types = (@opts.keys & TYPE_KEYS)
|
146
|
+
# type arg
|
147
|
+
if args.one? && types.empty?
|
148
|
+
if args.only.is_a? Array
|
149
|
+
@opts[:as] << :array
|
150
|
+
return args.only.only
|
151
|
+
elsif args.only.is_a? Hash
|
152
|
+
@opts[:hash] = args.only
|
153
|
+
return :hash
|
154
|
+
else
|
155
|
+
return args.only
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
unless args.empty?
|
160
|
+
raise ArgumentError, "too many arguments (#{(args + types).join(', ')}). Should be name, type, and " +
|
161
|
+
"an options hash, with the type and options optional"
|
162
|
+
end
|
163
|
+
|
164
|
+
# type options
|
165
|
+
if types.one?
|
166
|
+
@opts[:from] = @opts.delete(types.only)
|
167
|
+
types.only
|
168
|
+
elsif types.empty?
|
169
|
+
:text
|
170
|
+
else
|
171
|
+
raise ArgumentError, "more than one type option specified: #{types.join(', ')}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
data/lib/roxml/string.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Extension of String class to handle conversion from/to
|
2
|
+
# UTF-8/ISO-8869-1
|
3
|
+
class Object
|
4
|
+
require 'iconv'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Return an utf-8 representation of this string.
|
8
|
+
#
|
9
|
+
def to_utf
|
10
|
+
begin
|
11
|
+
Iconv.new("utf-8", "iso-8859-1").iconv(to_s)
|
12
|
+
rescue Iconv::IllegalSequence
|
13
|
+
STDERR << "!! Failed converting from UTF-8 -> ISO-8859-1 (#{self}). Already the right charset?"
|
14
|
+
self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Convert this string to iso-8850-1
|
20
|
+
#
|
21
|
+
def to_latin
|
22
|
+
begin
|
23
|
+
Iconv.new("iso-8859-1", "utf-8").iconv(to_s)
|
24
|
+
rescue Iconv::IllegalSequence
|
25
|
+
STDERR << "!! Failed converting from ISO-8859-1 -> UTF-8 (#{self}). Already the right charset?"
|
26
|
+
self
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class String
|
32
|
+
def between(separator, &block)
|
33
|
+
split(separator).collect(&block).join(separator)
|
34
|
+
end
|
35
|
+
end
|
data/lib/roxml/xml.rb
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
module ROXML
|
2
|
+
unless const_defined? 'XML_PARSER'
|
3
|
+
begin
|
4
|
+
require 'libxml'
|
5
|
+
XML_PARSER = 'libxml'
|
6
|
+
rescue LoadError
|
7
|
+
XML_PARSER = 'rexml'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
require File.join(File.dirname(__FILE__), 'xml', XML_PARSER)
|
11
|
+
|
12
|
+
#
|
13
|
+
# Internal base class that represents an XML - Class binding.
|
14
|
+
#
|
15
|
+
class XMLRef # ::nodoc::
|
16
|
+
attr_reader :accessor, :name, :array, :default, :block, :wrapper
|
17
|
+
|
18
|
+
def initialize(accessor, args, &block)
|
19
|
+
@accessor = accessor
|
20
|
+
@array = args.array?
|
21
|
+
@name = args.name
|
22
|
+
@default = args.default
|
23
|
+
@block = block
|
24
|
+
@wrapper = args.wrapper
|
25
|
+
end
|
26
|
+
|
27
|
+
# Reads data from the XML element and populates the instance
|
28
|
+
# accordingly.
|
29
|
+
def populate(xml, instance)
|
30
|
+
data = value(xml)
|
31
|
+
instance.instance_variable_set("@#{accessor}", data) if data
|
32
|
+
instance
|
33
|
+
end
|
34
|
+
|
35
|
+
def name?
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def xpath
|
41
|
+
wrapper ? "#{wrapper}#{xpath_separator}#{name}" : name.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def wrap(xml)
|
45
|
+
(wrapper && xml.name != wrapper) ? xml.child_add(XML::Node.new_element(wrapper)) : xml
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Interal class representing an XML attribute binding
|
50
|
+
#
|
51
|
+
# In context:
|
52
|
+
# <element attribute="XMLAttributeRef">
|
53
|
+
# XMLTextRef
|
54
|
+
# </element>
|
55
|
+
class XMLAttributeRef < XMLRef # ::nodoc::
|
56
|
+
# Updates the attribute in the given XML block to
|
57
|
+
# the value provided.
|
58
|
+
def update_xml(xml, value)
|
59
|
+
xml.attributes[name] = value.to_utf
|
60
|
+
xml
|
61
|
+
end
|
62
|
+
|
63
|
+
def value(xml)
|
64
|
+
parent = wrap(xml)
|
65
|
+
val = xml.attributes[name] || default
|
66
|
+
block ? block.call(val) : val
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def xpath_separator
|
71
|
+
'@'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Interal class representing XML content text binding
|
76
|
+
#
|
77
|
+
# In context:
|
78
|
+
# <element attribute="XMLAttributeRef">
|
79
|
+
# XMLTextRef
|
80
|
+
# </element>
|
81
|
+
class XMLTextRef < XMLRef # ::nodoc::
|
82
|
+
attr_reader :cdata, :content
|
83
|
+
|
84
|
+
def initialize(accessor, args, &block)
|
85
|
+
super(accessor, args, &block)
|
86
|
+
@content = args.content?
|
87
|
+
@cdata = args.cdata?
|
88
|
+
end
|
89
|
+
|
90
|
+
# Updates the text in the given _xml_ block to
|
91
|
+
# the _value_ provided.
|
92
|
+
def update_xml(xml, value)
|
93
|
+
parent = wrap(xml)
|
94
|
+
if content
|
95
|
+
add(parent, value)
|
96
|
+
elsif name?
|
97
|
+
parent.name = value
|
98
|
+
elsif array
|
99
|
+
value.each do |v|
|
100
|
+
add(parent.child_add(XML::Node.new_element(name)), v)
|
101
|
+
end
|
102
|
+
else
|
103
|
+
add(parent.child_add(XML::Node.new_element(name)), value)
|
104
|
+
end
|
105
|
+
xml
|
106
|
+
end
|
107
|
+
|
108
|
+
def value(xml)
|
109
|
+
val = if content
|
110
|
+
xml.content.strip
|
111
|
+
elsif name?
|
112
|
+
xml.name
|
113
|
+
elsif array
|
114
|
+
arr = xml.search(xpath).collect do |e|
|
115
|
+
e.content.strip.to_latin if e.content
|
116
|
+
end
|
117
|
+
arr unless arr.empty?
|
118
|
+
else
|
119
|
+
child = xml.search(name).first
|
120
|
+
child.content if child
|
121
|
+
end
|
122
|
+
val = default unless val && !val.blank?
|
123
|
+
block ? block.call(val) : val
|
124
|
+
end
|
125
|
+
|
126
|
+
def name?
|
127
|
+
name == '*'
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
def xpath_separator
|
132
|
+
'/'
|
133
|
+
end
|
134
|
+
|
135
|
+
def add(dest, value)
|
136
|
+
if cdata
|
137
|
+
dest.child_add(XML::Node.new_cdata(value.to_utf))
|
138
|
+
else
|
139
|
+
dest.content = value.to_utf
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class XMLHashRef < XMLTextRef # ::nodoc::
|
145
|
+
attr_reader :hash
|
146
|
+
|
147
|
+
def initialize(accessor, args, &block)
|
148
|
+
super(accessor, args, &block)
|
149
|
+
@hash = args.hash
|
150
|
+
if @hash.key.name? || @hash.value.name?
|
151
|
+
@name = '*'
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Updates the composed XML object in the given XML block to
|
156
|
+
# the value provided.
|
157
|
+
def update_xml(xml, value)
|
158
|
+
parent = wrap(xml)
|
159
|
+
value.each_pair do |k, v|
|
160
|
+
node = add_node(parent)
|
161
|
+
hash.key.update_xml(node, k)
|
162
|
+
hash.value.update_xml(node, v)
|
163
|
+
end
|
164
|
+
xml
|
165
|
+
end
|
166
|
+
|
167
|
+
def value(xml)
|
168
|
+
vals = xml.search(xpath).collect do |e|
|
169
|
+
[@hash.key.value(e), @hash.value.value(e)]
|
170
|
+
end
|
171
|
+
if block
|
172
|
+
vals.collect! do |(key, val)|
|
173
|
+
block.call(key, val)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
vals.to_h
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
def add_node(xml)
|
181
|
+
xml.child_add(XML::Node.new_element(hash.wrapper))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
class XMLObjectRef < XMLTextRef # ::nodoc::
|
186
|
+
attr_reader :klass
|
187
|
+
|
188
|
+
def initialize(accessor, args, &block)
|
189
|
+
super(accessor, args, &block)
|
190
|
+
@klass = args.type
|
191
|
+
end
|
192
|
+
|
193
|
+
# Updates the composed XML object in the given XML block to
|
194
|
+
# the value provided.
|
195
|
+
def update_xml(xml, value)
|
196
|
+
parent = wrap(xml)
|
197
|
+
unless array
|
198
|
+
parent.child_add(value.to_xml(name))
|
199
|
+
else
|
200
|
+
value.each do |v|
|
201
|
+
parent.child_add(v.to_xml(name))
|
202
|
+
end
|
203
|
+
end
|
204
|
+
xml
|
205
|
+
end
|
206
|
+
|
207
|
+
def value(xml)
|
208
|
+
val = unless array
|
209
|
+
if child = xml.search(xpath).first
|
210
|
+
instantiate(child)
|
211
|
+
end
|
212
|
+
else
|
213
|
+
arr = xml.search(xpath).collect do |e|
|
214
|
+
instantiate(e)
|
215
|
+
end
|
216
|
+
arr unless arr.empty?
|
217
|
+
end || default
|
218
|
+
block ? block.call(val) : val
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
def instantiate(elem)
|
223
|
+
if klass.respond_to? :parse
|
224
|
+
klass.parse(elem)
|
225
|
+
else
|
226
|
+
klass.new(elem)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
#
|
232
|
+
# Returns an XML::Node representing this object.
|
233
|
+
#
|
234
|
+
def to_xml(name = nil)
|
235
|
+
returning XML::Node.new_element(name || tag_name) do |root|
|
236
|
+
tag_refs.each do |ref|
|
237
|
+
if v = __send__(ref.accessor)
|
238
|
+
ref.update_xml(root, v)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|