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.
Files changed (46) hide show
  1. data/MIT-LICENSE +18 -0
  2. data/README.rdoc +122 -0
  3. data/Rakefile +104 -0
  4. data/lib/roxml.rb +362 -0
  5. data/lib/roxml/array.rb +15 -0
  6. data/lib/roxml/options.rb +175 -0
  7. data/lib/roxml/string.rb +35 -0
  8. data/lib/roxml/xml.rb +243 -0
  9. data/lib/roxml/xml/libxml.rb +63 -0
  10. data/lib/roxml/xml/rexml.rb +59 -0
  11. data/roxml.gemspec +78 -0
  12. data/test/fixtures/book_malformed.xml +5 -0
  13. data/test/fixtures/book_pair.xml +8 -0
  14. data/test/fixtures/book_text_with_attribute.xml +5 -0
  15. data/test/fixtures/book_valid.xml +5 -0
  16. data/test/fixtures/book_with_authors.xml +7 -0
  17. data/test/fixtures/book_with_contributions.xml +9 -0
  18. data/test/fixtures/book_with_contributors.xml +7 -0
  19. data/test/fixtures/book_with_contributors_attrs.xml +7 -0
  20. data/test/fixtures/book_with_default_namespace.xml +9 -0
  21. data/test/fixtures/book_with_depth.xml +6 -0
  22. data/test/fixtures/book_with_publisher.xml +7 -0
  23. data/test/fixtures/dictionary_of_attrs.xml +6 -0
  24. data/test/fixtures/dictionary_of_mixeds.xml +4 -0
  25. data/test/fixtures/dictionary_of_texts.xml +10 -0
  26. data/test/fixtures/library.xml +30 -0
  27. data/test/fixtures/library_uppercase.xml +30 -0
  28. data/test/fixtures/nameless_ageless_youth.xml +2 -0
  29. data/test/fixtures/person.xml +1 -0
  30. data/test/fixtures/person_with_guarded_mothers.xml +13 -0
  31. data/test/fixtures/person_with_mothers.xml +10 -0
  32. data/test/mocks/dictionaries.rb +56 -0
  33. data/test/mocks/mocks.rb +212 -0
  34. data/test/test_helper.rb +16 -0
  35. data/test/unit/options_test.rb +62 -0
  36. data/test/unit/roxml_test.rb +24 -0
  37. data/test/unit/string_test.rb +11 -0
  38. data/test/unit/to_xml_test.rb +75 -0
  39. data/test/unit/xml_attribute_test.rb +34 -0
  40. data/test/unit/xml_construct_test.rb +19 -0
  41. data/test/unit/xml_hash_test.rb +54 -0
  42. data/test/unit/xml_name_test.rb +14 -0
  43. data/test/unit/xml_namespace_test.rb +36 -0
  44. data/test/unit/xml_object_test.rb +94 -0
  45. data/test/unit/xml_text_test.rb +57 -0
  46. metadata +110 -0
@@ -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
@@ -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