roxml 2.4.1 → 2.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/History.txt +21 -0
  2. data/MIT-LICENSE +1 -1
  3. data/Manifest.txt +27 -6
  4. data/Rakefile +2 -2
  5. data/TODO +35 -14
  6. data/config/website.yml +2 -0
  7. data/examples/amazon.rb +33 -0
  8. data/examples/current_weather.rb +27 -0
  9. data/examples/dashed_elements.rb +20 -0
  10. data/examples/posts.rb +27 -0
  11. data/examples/twitter.rb +37 -0
  12. data/examples/xml/amazon.xml +133 -0
  13. data/examples/xml/current_weather.xml +89 -0
  14. data/examples/xml/dashed_elements.xml +52 -0
  15. data/examples/xml/posts.xml +23 -0
  16. data/examples/xml/twitter.xml +422 -0
  17. data/lib/roxml.rb +35 -6
  18. data/lib/roxml/{options.rb → definition.rb} +60 -88
  19. data/lib/roxml/extensions.rb +3 -0
  20. data/lib/roxml/hash_definition.rb +59 -0
  21. data/lib/roxml/xml.rb +15 -272
  22. data/lib/roxml/xml/{libxml.rb → parsers/libxml.rb} +13 -5
  23. data/lib/roxml/xml/{rexml.rb → parsers/rexml.rb} +13 -4
  24. data/lib/roxml/xml/references.rb +290 -0
  25. data/roxml.gemspec +4 -4
  26. data/spec/examples/amazon_spec.rb +53 -0
  27. data/spec/examples/current_weather_spec.rb +37 -0
  28. data/spec/examples/dashed_elements_spec.rb +20 -0
  29. data/spec/examples/post_spec.rb +24 -0
  30. data/spec/examples/twitter_spec.rb +32 -0
  31. data/spec/spec.opts +1 -0
  32. data/spec/spec_helper.rb +16 -0
  33. data/tasks/rspec.rake +21 -0
  34. data/test/unit/definition_test.rb +160 -0
  35. data/test/unit/inheritance_test.rb +22 -1
  36. data/test/unit/roxml_test.rb +30 -1
  37. data/test/unit/xml_name_test.rb +29 -0
  38. data/test/unit/xml_namespace_test.rb +38 -1
  39. data/website/index.html +98 -0
  40. metadata +30 -9
  41. data/html/index.html +0 -278
  42. data/html/style.css +0 -79
  43. data/test/unit/options_test.rb +0 -103
@@ -1,9 +1,12 @@
1
- %w(extensions/active_support extensions/deprecation extensions/array extensions/string options xml).each do |file|
2
- require File.join(File.dirname(__FILE__), 'roxml', file)
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__)) unless
2
+ $LOAD_PATH.include?(File.dirname(__FILE__)) || $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ %w(extensions definition xml).each do |file|
5
+ require File.join('roxml', file)
3
6
  end
4
7
 
5
8
  module ROXML # :nodoc:
6
- VERSION = '2.4.1'
9
+ VERSION = '2.4.2'
7
10
 
8
11
  def self.included(base) # :nodoc:
9
12
  base.extend ClassMethods::Accessors,
@@ -108,6 +111,28 @@ module ROXML # :nodoc:
108
111
  @roxml_tag_name = name
109
112
  end
110
113
 
114
+ # Sets the namemespace for attributes and elements of this class. You can override
115
+ # this value on individual elements via the :from option
116
+ #
117
+ # Example:
118
+ # class Book
119
+ # xml_namespace :aws
120
+ #
121
+ # xml_reader :default_namespace
122
+ # xml_reader :different_namespace, :from => 'different:namespace'
123
+ # xml_reader :no_namespace, :from => ':no_namespace'
124
+ # end
125
+ #
126
+ # <aws:book xmlns:aws="http://www.aws.com/aws" xmlns:different="http://www.aws.com/different">
127
+ # <aws:default_namespace>value</aws:default_namespace>
128
+ # <different:namespace>value</different:namespace>
129
+ # <no_namespace>value</no_namespace>
130
+ # </aws:book>
131
+ #
132
+ def xml_namespace(namespace)
133
+ @roxml_namespace = namespace.to_s
134
+ end
135
+
111
136
  # Most xml documents have a consistent naming convention, for example, the node and
112
137
  # and attribute names might appear in CamelCase. xml_convention enables you to adapt
113
138
  # the roxml default names for this object to suit this convention. For example,
@@ -410,14 +435,14 @@ module ROXML # :nodoc:
410
435
  #
411
436
  # == Other options
412
437
  # [: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.
413
- # [:as] :cdata for character data; :integer, Integer, :float, Float to coerce to Integer or Float respectively
438
+ # [:as] one or more of the following: :cdata for character data; Integer, Float, Date, Time or DateTime to coerce to the respective type
414
439
  # [:in] An optional name of a wrapping tag for this XML accessor
415
440
  # [:else] Default value for attribute, if missing
416
441
  # [:required] If true, throws RequiredElementMissing when the element isn't present
417
442
  # [:frozen] If true, all results are frozen (using #freeze) at parse-time.
418
443
  #
419
444
  def xml_reference(writable, sym, type_and_or_opts = :text, opts = nil, &block)
420
- opts = Opts.new(sym, *[type_and_or_opts, opts].compact, &block)
445
+ opts = Definition.new(sym, *[type_and_or_opts, opts].compact, &block)
421
446
 
422
447
  add_accessor(opts, writable)
423
448
  end
@@ -512,6 +537,10 @@ module ROXML # :nodoc:
512
537
  @roxml_tag_name || superclass.try(:roxml_tag_name)
513
538
  end
514
539
 
540
+ def roxml_namespace # :nodoc:
541
+ @roxml_namespace || superclass.try(:roxml_namespace)
542
+ end
543
+
515
544
  # Returns array of internal reference objects, such as attributes
516
545
  # and composed XML objects
517
546
  def roxml_attrs
@@ -544,7 +573,7 @@ module ROXML # :nodoc:
544
573
  # See also: xml_initialize
545
574
  #
546
575
  def from_xml(data, *initialization_args)
547
- xml = (data.kind_of?(XML::Node) ? data : XML::Parser.parse(data).root)
576
+ xml = XML::Node.from(data)
548
577
 
549
578
  unless xml_construction_args_without_deprecation.empty?
550
579
  args = xml_construction_args_without_deprecation.map do |arg|
@@ -1,63 +1,7 @@
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_hash_args(opts, :attr, opts[:attrs][0])
16
- @value = to_hash_args(opts, :attr, opts[:attrs][1])
17
- else
18
- @key = to_hash_args opts, *fetch_element(opts, :key)
19
- @value = to_hash_args opts, *fetch_element(opts, :value)
20
- end
21
- end
1
+ require File.join(File.dirname(__FILE__), 'hash_definition')
22
2
 
23
- private
24
- def fetch_element(opts, what)
25
- case opts[what]
26
- when Hash
27
- raise ArgumentError, "Hash #{what} is over-specified: #{opts[what].inspect}" unless opts[what].keys.one?
28
- type = opts[what].keys.first
29
- [type, opts[what][type]]
30
- when :content
31
- [:content, opts[:name]]
32
- when :name
33
- [:name, '*']
34
- when String
35
- [:text, opts[what]]
36
- when Symbol
37
- [:text, opts[what]]
38
- else
39
- raise ArgumentError, "unrecognized hash parameter: #{what} => #{opts[what]}"
40
- end
41
- end
42
-
43
- def to_hash_args(args, type, name)
44
- args = [args] unless args.is_a? Array
45
-
46
- if args.one? && !(args.first.keys & HASH_KEYS).empty?
47
- opts = {type => name}
48
- if type == :content
49
- opts[:type] = :text
50
- (opts[:as] ||= []) << :content
51
- end
52
- Opts.new(name, opts)
53
- else
54
- opts = args.extract_options!
55
- raise opts.inspect
56
- end
57
- end
58
- end
59
-
60
- class Opts # :nodoc:
3
+ module ROXML
4
+ class Definition # :nodoc:
61
5
  attr_reader :name, :type, :hash, :blocks, :accessor, :to_xml
62
6
 
63
7
  class << self
@@ -75,6 +19,7 @@ module ROXML
75
19
  @opts = extract_options!(args)
76
20
  @default = @opts.delete(:else)
77
21
  @to_xml = @opts.delete(:to_xml)
22
+ @name_explicit = @opts.has_key?(:from)
78
23
 
79
24
  if @opts.has_key?(:readonly)
80
25
  raise ArgumentError, "There is no 'readonly' option. You probably mean to use :frozen => true"
@@ -114,7 +59,7 @@ module ROXML
114
59
  end
115
60
 
116
61
  def hash
117
- @hash ||= HashDesc.new(@opts.delete(:hash), name) if hash?
62
+ @hash ||= HashDefinition.new(@opts.delete(:hash), name) if hash?
118
63
  end
119
64
 
120
65
  def hash?
@@ -125,6 +70,10 @@ module ROXML
125
70
  @name == '*'
126
71
  end
127
72
 
73
+ def name_explicit?
74
+ @name_explicit
75
+ end
76
+
128
77
  def content?
129
78
  @type == :content
130
79
  end
@@ -167,57 +116,80 @@ module ROXML
167
116
  end
168
117
 
169
118
  private
119
+ def self.all(items, &block)
120
+ array = items.is_a?(Array)
121
+ results = (array ? items : [items]).map do |item|
122
+ yield item
123
+ end
124
+
125
+ array ? results : results.first
126
+ end
127
+
170
128
  BLOCK_TO_FLOAT = lambda do |val|
171
- if val.is_a? Array
172
- val.collect do |v|
173
- Float(v)
174
- end
175
- else
176
- Float(val)
129
+ all(val) do |v|
130
+ Float(v) unless blank_string?(v)
177
131
  end
178
132
  end
179
133
 
180
134
  BLOCK_TO_INT = lambda do |val|
181
- if val.is_a? Array
182
- val.collect do |v|
183
- Integer(v)
184
- end
135
+ all(val) do |v|
136
+ Integer(v) unless blank_string?(v)
137
+ end
138
+ end
139
+
140
+ def self.fetch_bool(value, default)
141
+ value = value.try(:downcase)
142
+ if %w{true yes 1}.include? value
143
+ true
144
+ elsif %w{false no 0}.include? value
145
+ false
185
146
  else
186
- Integer(val)
147
+ default
187
148
  end
188
149
  end
189
150
 
190
- TRUE_VALS = %w{TRUE True true 1}
191
- FALSE_VALS = %w{FALSE False false 0}
151
+ def self.blank_string?(value)
152
+ value.is_a?(String) && value.blank?
153
+ end
192
154
 
193
155
  BLOCK_SHORTHANDS = {
194
- :integer => BLOCK_TO_INT,
156
+ :integer => BLOCK_TO_INT, # deprecated
195
157
  Integer => BLOCK_TO_INT,
196
- :float => BLOCK_TO_FLOAT,
158
+ :float => BLOCK_TO_FLOAT, # deprecated
197
159
  Float => BLOCK_TO_FLOAT,
160
+ Date => lambda do |val|
161
+ if defined?(Date)
162
+ all(val) {|v| Date.parse(v) unless blank_string?(v) }
163
+ end
164
+ end,
165
+ DateTime => lambda do |val|
166
+ if defined?(DateTime)
167
+ all(val) {|v| DateTime.parse(v) unless blank_string?(v) }
168
+ end
169
+ end,
170
+ Time => lambda do |val|
171
+ if defined?(Time)
172
+ all(val) {|v| Time.parse(v) unless blank_string?(v) }
173
+ end
174
+ end,
175
+
198
176
  :bool => nil,
199
177
  :bool_standalone => lambda do |val|
200
- if TRUE_VALS.include? val
201
- true
202
- elsif FALSE_VALS.include? val
203
- false
204
- else
205
- nil
178
+ all(val) do |v|
179
+ fetch_bool(v, nil)
206
180
  end
207
181
  end,
208
-
209
182
  :bool_combined => lambda do |val|
210
- if TRUE_VALS.include? val
211
- true
212
- elsif FALSE_VALS.include? val
213
- false
214
- else
215
- val
183
+ all(val) do |v|
184
+ fetch_bool(v, v)
216
185
  end
217
186
  end
218
187
  }
219
188
 
220
189
  def collect_blocks(block, as)
190
+ ActiveSupport::Deprecation.warn ":as => :float is deprecated. Use :as => Float instead" if as.include?(:float)
191
+ ActiveSupport::Deprecation.warn ":as => :integer is deprecated. Use :as => Integer instead" if as.include?(:integer)
192
+
221
193
  shorthands = as & BLOCK_SHORTHANDS.keys
222
194
  if shorthands.size > 1
223
195
  raise ArgumentError, "multiple block shorthands supplied #{shorthands.map(&:to_s).join(', ')}"
@@ -0,0 +1,3 @@
1
+ %w(active_support deprecation array string).each do |file|
2
+ require File.join(File.dirname(__FILE__), 'extensions', file)
3
+ end
@@ -0,0 +1,59 @@
1
+ module ROXML
2
+ HASH_KEYS = [:attrs, :key, :value].freeze
3
+ TYPE_KEYS = [:attr, :text, :hash, :content].freeze
4
+
5
+ class HashDefinition # :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_hash_args(opts, :attr, opts[:attrs][0])
16
+ @value = to_hash_args(opts, :attr, opts[:attrs][1])
17
+ else
18
+ @key = to_hash_args opts, *fetch_element(opts, :key)
19
+ @value = to_hash_args opts, *fetch_element(opts, :value)
20
+ end
21
+ end
22
+
23
+ private
24
+ def fetch_element(opts, what)
25
+ case opts[what]
26
+ when Hash
27
+ raise ArgumentError, "Hash #{what} is over-specified: #{opts[what].inspect}" unless opts[what].keys.one?
28
+ type = opts[what].keys.first
29
+ [type, opts[what][type]]
30
+ when :content
31
+ [:content, opts[:name]]
32
+ when :name
33
+ [:name, '*']
34
+ when String
35
+ [:text, opts[what]]
36
+ when Symbol
37
+ [:text, opts[what]]
38
+ else
39
+ raise ArgumentError, "unrecognized hash parameter: #{what} => #{opts[what]}"
40
+ end
41
+ end
42
+
43
+ def to_hash_args(args, type, name)
44
+ args = [args] unless args.is_a? Array
45
+
46
+ if args.one? && !(args.first.keys & HASH_KEYS).empty?
47
+ opts = {type => name}
48
+ if type == :content
49
+ opts[:type] = :text
50
+ (opts[:as] ||= []) << :content
51
+ end
52
+ Definition.new(name, opts)
53
+ else
54
+ opts = args.extract_options!
55
+ raise opts.inspect
56
+ end
57
+ end
58
+ end
59
+ end
@@ -7,282 +7,25 @@ module ROXML
7
7
  XML_PARSER = 'rexml' # :nodoc:
8
8
  end
9
9
  end
10
- require File.join(File.dirname(__FILE__), 'xml', XML_PARSER)
11
10
 
12
- class RequiredElementMissing < Exception # :nodoc:
13
- end
14
-
15
- #
16
- # Internal base class that represents an XML - Class binding.
17
- #
18
- class XMLRef # :nodoc:
19
- delegate :required?, :array?, :blocks, :accessor, :variable_name, :default, :to => :opts
20
-
21
- def initialize(opts, instance)
22
- @opts = opts
23
- @instance = instance
24
- end
25
-
26
- def to_xml
27
- val = @instance.__send__(accessor)
28
- opts.to_xml.respond_to?(:call) ? opts.to_xml.call(val) : val
29
- end
30
-
31
- def update_xml(xml, value)
32
- returning wrap(xml) do |xml|
33
- write_xml(xml, value)
34
- end
35
- end
36
-
37
- def name
38
- conventionize(opts.name)
39
- end
40
- alias_method :xpath_name, :name
41
-
42
- def default
43
- @default ||= @opts.default || (@opts.array? ? Array.new : nil)
44
- @default.duplicable? ? @default.dup : @default
45
- end
46
-
47
- def value_in(xml)
48
- value = fetch_value(xml)
49
- freeze(apply_blocks(value))
50
- end
51
-
52
- private
53
- attr_reader :opts
54
-
55
- def conventionize(what)
56
- if !what.blank? && @instance.try(:class).try(:roxml_naming_convention).respond_to?(:call)
57
- @instance.class.roxml_naming_convention.call(what)
58
- else
59
- what
60
- end
61
- end
62
-
63
- def wrapper
64
- conventionize(opts.wrapper)
65
- end
66
-
67
- def apply_blocks(val)
68
- blocks.apply_to(val)
69
- end
70
-
71
- def freeze(val)
72
- if opts.freeze?
73
- val.each(&:freeze) if val.is_a?(Enumerable)
74
- val.freeze
75
- else
76
- val
77
- end
78
- end
79
-
80
- def xpath
81
- wrapper ? "#{wrapper}/#{xpath_name}" : xpath_name.to_s
82
- end
83
-
84
- def auto_xpath
85
- "#{conventionize(opts.name.pluralize)}/#{xpath_name}" if array?
86
- end
87
-
88
- def wrap(xml)
89
- return xml if !wrapper || xml.name == wrapper
90
- if child = xml.children.find {|c| c.name == wrapper }
91
- return child
92
- end
93
- xml.child_add(XML::Node.new_element(wrapper))
94
- end
95
-
96
- def nodes_in(xml)
97
- vals = xml.search(xpath)
98
-
99
- if (opts.hash? || opts.array?) && vals.empty? && !wrapper && auto_xpath
100
- vals = xml.search(auto_xpath)
101
- @auto_vals = !vals.empty?
102
- end
103
-
104
- if vals.empty?
105
- raise RequiredElementMissing, "#{name} from #{xml} for #{accessor}" if required?
106
- default
107
- else
108
- yield(vals)
109
- end
110
- end
111
- end
112
-
113
- # Interal class representing an XML attribute binding
114
- #
115
- # In context:
116
- # <element attribute="XMLAttributeRef">
117
- # XMLTextRef
118
- # </element>
119
- class XMLAttributeRef < XMLRef # :nodoc:
120
- private
121
- # Updates the attribute in the given XML block to
122
- # the value provided.
123
- def write_xml(xml, value)
124
- xml.attributes[name] = value.to_s.to_utf
125
- end
126
-
127
- def fetch_value(xml)
128
- nodes_in(xml) do |nodes|
129
- nodes.first.value
130
- end
131
- end
132
-
133
- def xpath_name
134
- "@#{name}"
135
- end
136
- end
137
-
138
- # Interal class representing XML content text binding
139
- #
140
- # In context:
141
- # <element attribute="XMLAttributeRef">
142
- # XMLTextRef
143
- # </element>
144
- class XMLTextRef < XMLRef # :nodoc:
145
- delegate :cdata?, :content?, :name?, :to => :opts
146
-
147
- private
148
- # Updates the text in the given _xml_ block to
149
- # the _value_ provided.
150
- def write_xml(xml, value)
151
- if content?
152
- add(xml, value)
153
- elsif name?
154
- xml.name = value
155
- elsif array?
156
- value.each do |v|
157
- add(xml.child_add(XML::Node.new_element(name)), v)
158
- end
159
- else
160
- add(xml.child_add(XML::Node.new_element(name)), value)
161
- end
162
- end
163
-
164
- def fetch_value(xml)
165
- if content? || name?
166
- value =
167
- if content?
168
- xml.content.to_s.strip
169
- elsif name?
170
- xml.name
171
- end
172
-
173
- if value.empty?
174
- raise RequiredElementMissing, "#{name} from #{xml} for #{accessor}" if required?
175
- default
11
+ require File.join(File.dirname(__FILE__), 'xml/parsers', XML_PARSER)
12
+
13
+ module XML
14
+ class Node
15
+ def self.from(data)
16
+ if data.is_a?(XML::Node)
17
+ data
18
+ elsif data.is_a?(File) || data.is_a?(IO)
19
+ Parser.parse_io(data).root
20
+ elsif (defined?(URI) && data.is_a?(URI::Generic)) ||
21
+ (defined?(Pathname) && data.is_a?(Pathname))
22
+ Parser.parse_file(data.to_s).root
176
23
  else
177
- value
178
- end
179
- else
180
- nodes_in(xml) do |nodes|
181
- if array?
182
- nodes.collect do |e|
183
- e.content.strip.to_latin
184
- end
185
- else
186
- nodes.first.content
187
- end
24
+ Parser.parse(data).root
188
25
  end
189
26
  end
190
27
  end
191
-
192
- def add(dest, value)
193
- if cdata?
194
- dest.child_add(XML::Node.new_cdata(value.to_s.to_utf))
195
- else
196
- dest.content = value.to_s.to_utf
197
- end
198
- end
199
28
  end
29
+ end
200
30
 
201
- class XMLHashRef < XMLTextRef # :nodoc:
202
- delegate :hash, :to => :opts
203
-
204
- def initialize(opts, inst)
205
- super(opts, inst)
206
- @key = opts.hash.key.to_ref(inst)
207
- @value = opts.hash.value.to_ref(inst)
208
- end
209
-
210
- private
211
- # Updates the composed XML object in the given XML block to
212
- # the value provided.
213
- def write_xml(xml, value)
214
- value.each_pair do |k, v|
215
- node = xml.child_add(XML::Node.new_element(hash.wrapper))
216
- @key.update_xml(node, k)
217
- @value.update_xml(node, v)
218
- end
219
- end
220
-
221
- def fetch_value(xml)
222
- nodes_in(xml) do |nodes|
223
- nodes.collect do |e|
224
- [@key.value_in(e), @value.value_in(e)]
225
- end
226
- end
227
- end
228
-
229
- def apply_blocks(vals)
230
- unless blocks.empty?
231
- vals.collect! do |kvp|
232
- super(kvp)
233
- end
234
- end
235
- vals.to_hash if vals
236
- end
237
-
238
- def freeze(vals)
239
- if opts.freeze?
240
- vals.each_pair{|k, v| k.freeze; v.freeze }
241
- vals.freeze
242
- else
243
- vals
244
- end
245
- end
246
- end
247
-
248
- class XMLObjectRef < XMLTextRef # :nodoc:
249
- delegate :type, :to => :opts
250
-
251
- private
252
- # Updates the composed XML object in the given XML block to
253
- # the value provided.
254
- def write_xml(xml, value)
255
- if array?
256
- value.each do |v|
257
- xml.child_add(v.to_xml(name))
258
- end
259
- elsif value.is_a?(ROXML)
260
- xml.child_add(value.to_xml(name))
261
- else
262
- node = XML::Node.new_element(name)
263
- node.content = value.to_xml
264
- xml.child_add(node)
265
- end
266
- end
267
-
268
- def fetch_value(xml)
269
- nodes_in(xml) do |nodes|
270
- unless array?
271
- instantiate(nodes.first)
272
- else
273
- nodes.collect do |e|
274
- instantiate(e)
275
- end
276
- end
277
- end
278
- end
279
-
280
- def instantiate(elem)
281
- if type.respond_to? :from_xml
282
- type.from_xml(elem)
283
- else
284
- type.new(elem)
285
- end
286
- end
287
- end
288
- end
31
+ require File.join(File.dirname(__FILE__), 'xml/references')