Empact-roxml 2.0

Sign up to get free protection for your applications and to get access to all the features.
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