roxml 2.4.3 → 2.5.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 (47) hide show
  1. data/History.txt +54 -0
  2. data/Manifest.txt +9 -6
  3. data/README.rdoc +24 -17
  4. data/Rakefile +2 -1
  5. data/TODO +30 -31
  6. data/examples/active_record.rb +69 -0
  7. data/examples/amazon.rb +1 -1
  8. data/examples/current_weather.rb +1 -1
  9. data/examples/posts.rb +8 -8
  10. data/examples/twitter.rb +2 -2
  11. data/examples/xml/active_record.xml +70 -0
  12. data/lib/roxml.rb +174 -174
  13. data/lib/roxml/definition.rb +165 -89
  14. data/lib/roxml/extensions/deprecation.rb +5 -0
  15. data/lib/roxml/extensions/string/conversions.rb +2 -3
  16. data/lib/roxml/hash_definition.rb +26 -25
  17. data/lib/roxml/xml.rb +15 -6
  18. data/lib/roxml/xml/parsers/libxml.rb +9 -6
  19. data/lib/roxml/xml/parsers/rexml.rb +1 -1
  20. data/lib/roxml/xml/references.rb +14 -17
  21. data/roxml.gemspec +8 -5
  22. data/spec/definition_spec.rb +563 -0
  23. data/spec/examples/active_record_spec.rb +43 -0
  24. data/spec/roxml_spec.rb +372 -0
  25. data/spec/shared_specs.rb +15 -0
  26. data/spec/spec_helper.rb +21 -4
  27. data/spec/string_spec.rb +15 -0
  28. data/spec/xml/parser_spec.rb +22 -0
  29. data/test/fixtures/book_valid.xml +1 -1
  30. data/test/fixtures/person_with_guarded_mothers.xml +3 -3
  31. data/test/mocks/mocks.rb +57 -45
  32. data/test/unit/definition_test.rb +161 -12
  33. data/test/unit/deprecations_test.rb +97 -0
  34. data/test/unit/to_xml_test.rb +30 -1
  35. data/test/unit/xml_bool_test.rb +15 -3
  36. data/test/unit/xml_construct_test.rb +6 -6
  37. data/test/unit/xml_hash_test.rb +18 -0
  38. data/test/unit/xml_initialize_test.rb +6 -3
  39. data/test/unit/xml_object_test.rb +66 -5
  40. data/test/unit/xml_text_test.rb +3 -0
  41. metadata +23 -15
  42. data/test/unit/array_test.rb +0 -16
  43. data/test/unit/freeze_test.rb +0 -71
  44. data/test/unit/inheritance_test.rb +0 -63
  45. data/test/unit/overriden_output_test.rb +0 -33
  46. data/test/unit/roxml_test.rb +0 -60
  47. data/test/unit/string_test.rb +0 -11
@@ -1,8 +1,19 @@
1
1
  require File.join(File.dirname(__FILE__), 'hash_definition')
2
2
 
3
+ class Module
4
+ def bool_attr_reader(*attrs)
5
+ attrs.each do |attr|
6
+ define_method :"#{attr}?" do
7
+ instance_variable_get(:"@#{attr}") || false
8
+ end
9
+ end
10
+ end
11
+ end
12
+
3
13
  module ROXML
4
14
  class Definition # :nodoc:
5
- attr_reader :name, :type, :hash, :blocks, :accessor, :to_xml
15
+ attr_reader :name, :type, :wrapper, :hash, :blocks, :accessor, :to_xml
16
+ bool_attr_reader :name_explicit, :array, :cdata, :required, :frozen
6
17
 
7
18
  class << self
8
19
  def silence_xml_name_warning?
@@ -16,21 +27,24 @@ module ROXML
16
27
 
17
28
  def initialize(sym, *args, &block)
18
29
  @accessor = sym
19
- @opts = extract_options!(args)
20
- @default = @opts.delete(:else)
21
- @to_xml = @opts.delete(:to_xml)
22
- @name_explicit = @opts.has_key?(:from)
23
-
24
- if @opts.has_key?(:readonly)
25
- raise ArgumentError, "There is no 'readonly' option. You probably mean to use :frozen => true"
30
+ if @accessor.to_s.ends_with?('_on')
31
+ ActiveSupport::Deprecation.warn "In 3.0, attributes with names ending with _on will default to Date type, rather than :text"
32
+ end
33
+ if @accessor.to_s.ends_with?('_at')
34
+ ActiveSupport::Deprecation.warn "In 3.0, attributes with names ending with _at will default to DateTime type, rather than :text"
26
35
  end
27
36
 
28
- @opts.reverse_merge!(:as => [], :in => nil)
29
- @opts[:as] = [*@opts[:as]]
37
+ opts = extract_options!(args)
38
+ opts[:as] ||= :bool if @accessor.to_s.ends_with?('?')
30
39
 
31
- @type = extract_type(args)
32
- @opts[:as] << :bool if @accessor.to_s.ends_with?('?')
40
+ @array = opts[:as].is_a?(Array) || extract_from_as(opts, :array, "Please use [] around your usual type declaration")
41
+ @blocks = collect_blocks(block, opts[:as])
33
42
 
43
+ if opts.has_key?(:readonly)
44
+ raise ArgumentError, "There is no 'readonly' option. You probably mean to use :frozen => true"
45
+ end
46
+
47
+ @type = extract_type(args, opts)
34
48
  if @type.try(:xml_name_without_deprecation?)
35
49
  unless self.class.silence_xml_name_warning?
36
50
  warn "WARNING: As of 2.3, a breaking change has been in the naming of sub-objects. " +
@@ -38,14 +52,22 @@ module ROXML
38
52
  "Use :from on the parent declaration to override this behavior. Set ROXML::SILENCE_XML_NAME_WARNING to avoid this message."
39
53
  self.class.silence_xml_name_warning!
40
54
  end
41
- @opts[:from] ||= @type.tag_name
42
- else
43
- @opts[:from] ||= variable_name
55
+ opts[:from] ||= @type.tag_name
44
56
  end
45
57
 
46
- @blocks = collect_blocks(block, @opts[:as])
58
+ if opts[:from] == :content
59
+ opts[:from] = '.'
60
+ elsif opts[:from] == :name
61
+ opts[:from] = '*'
62
+ elsif opts[:from] == :attr
63
+ @type = :attr
64
+ opts[:from] = nil
65
+ elsif opts[:from].to_s.starts_with?('@')
66
+ @type = :attr
67
+ opts[:from].sub!('@', '')
68
+ end
47
69
 
48
- @name = @opts[:from].to_s
70
+ @name = (opts[:from] || variable_name).to_s
49
71
  @name = @name.singularize if hash? || array?
50
72
  if hash? && (hash.key.name? || hash.value.name?)
51
73
  @name = '*'
@@ -55,63 +77,43 @@ module ROXML
55
77
  end
56
78
 
57
79
  def variable_name
58
- accessor.to_s.ends_with?('?') ? accessor.to_s.chomp('?') : accessor.to_s
80
+ accessor.to_s.chomp('?')
59
81
  end
60
82
 
61
83
  def hash
62
- @hash ||= HashDefinition.new(@opts.delete(:hash), name) if hash?
84
+ if hash?
85
+ @type.wrapper ||= name
86
+ @type
87
+ end
63
88
  end
64
89
 
65
90
  def hash?
66
- @type == :hash
91
+ @type.is_a?(HashDefinition)
67
92
  end
68
93
 
69
94
  def name?
70
95
  @name == '*'
71
96
  end
72
97
 
73
- def name_explicit?
74
- @name_explicit
75
- end
76
-
77
98
  def content?
78
- @type == :content
79
- end
80
-
81
- def array?
82
- @opts[:as].include? :array
83
- end
84
-
85
- def cdata?
86
- @opts[:as].include? :cdata
87
- end
88
-
89
- def wrapper
90
- @opts[:in]
91
- end
92
-
93
- def required?
94
- @opts[:required]
95
- end
96
-
97
- def freeze?
98
- @opts[:frozen]
99
+ @name == '.'
99
100
  end
100
101
 
101
102
  def default
102
- @default ||= [] if array?
103
- @default ||= {} if hash?
103
+ if @default.nil?
104
+ @default = [] if array?
105
+ @default = {} if hash?
106
+ end
104
107
  @default.duplicable? ? @default.dup : @default
105
108
  end
106
109
 
107
110
  def to_ref(inst)
108
111
  case type
109
- when :attr then XMLAttributeRef
110
- when :content then XMLTextRef
111
- when :text then XMLTextRef
112
- when :hash then XMLHashRef
113
- when Symbol then raise ArgumentError, "Invalid type argument #{opts.type}"
114
- else XMLObjectRef
112
+ when :attr then XMLAttributeRef
113
+ when :text then XMLTextRef
114
+ when HashDefinition then XMLHashRef
115
+ when Symbol then raise ArgumentError, "Invalid type argument #{type}"
116
+ else XMLObjectRef
115
117
  end.new(self, inst)
116
118
  end
117
119
 
@@ -127,13 +129,13 @@ module ROXML
127
129
 
128
130
  BLOCK_TO_FLOAT = lambda do |val|
129
131
  all(val) do |v|
130
- Float(v) unless blank_string?(v)
132
+ Float(v) unless v.blank?
131
133
  end
132
134
  end
133
135
 
134
136
  BLOCK_TO_INT = lambda do |val|
135
137
  all(val) do |v|
136
- Integer(v) unless blank_string?(v)
138
+ Integer(v) unless v.blank?
137
139
  end
138
140
  end
139
141
 
@@ -147,30 +149,20 @@ module ROXML
147
149
  default
148
150
  end
149
151
  end
150
-
151
- def self.blank_string?(value)
152
- value.is_a?(String) && value.blank?
153
- end
154
-
155
- BLOCK_SHORTHANDS = {
152
+
153
+ CORE_BLOCK_SHORTHANDS = {
154
+ # Core Shorthands
156
155
  :integer => BLOCK_TO_INT, # deprecated
157
156
  Integer => BLOCK_TO_INT,
158
157
  :float => BLOCK_TO_FLOAT, # deprecated
159
158
  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) }
159
+ Fixnum => lambda do |val|
160
+ all(val) do |v|
161
+ v.to_i unless v.blank?
168
162
  end
169
163
  end,
170
164
  Time => lambda do |val|
171
- if defined?(Time)
172
- all(val) {|v| Time.parse(v) unless blank_string?(v) }
173
- end
165
+ all(val) {|v| Time.parse(v) unless v.blank? }
174
166
  end,
175
167
 
176
168
  :bool => nil,
@@ -186,22 +178,54 @@ module ROXML
186
178
  end
187
179
  }
188
180
 
181
+ def self.block_shorthands
182
+ # dynamically load these shorthands at class definition time, but
183
+ # only if they're already availbable
184
+ returning CORE_BLOCK_SHORTHANDS do |blocks|
185
+ blocks.reverse_merge!(BigDecimal => lambda do |val|
186
+ all(val) do |v|
187
+ BigDecimal.new(v) unless v.blank?
188
+ end
189
+ end) if defined?(BigDecimal)
190
+
191
+ blocks.reverse_merge!(DateTime => lambda do |val|
192
+ if defined?(DateTime)
193
+ all(val) {|v| DateTime.parse(v) unless v.blank? }
194
+ end
195
+ end) if defined?(DateTime)
196
+
197
+ blocks.reverse_merge!(Date => lambda do |val|
198
+ if defined?(Date)
199
+ all(val) {|v| Date.parse(v) unless v.blank? }
200
+ end
201
+ end) if defined?(Date)
202
+ end
203
+ end
204
+
189
205
  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)
206
+ ActiveSupport::Deprecation.warn ":as => :float is deprecated. Use :as => Float instead" if as == :float
207
+ ActiveSupport::Deprecation.warn ":as => :integer is deprecated. Use :as => Integer instead" if as == :integer
208
+
209
+ if as.is_a?(Array)
210
+ unless as.one? || as.empty?
211
+ raise ArgumentError, "multiple :as types (#{as.map(&:inspect).join(', ')}) is not supported. Use a block if you want more complicated behavior."
212
+ end
192
213
 
193
- shorthands = as & BLOCK_SHORTHANDS.keys
194
- if shorthands.size > 1
195
- raise ArgumentError, "multiple block shorthands supplied #{shorthands.map(&:to_s).join(', ')}"
214
+ as = as.first
196
215
  end
197
216
 
198
- shorthand = shorthands.first
199
- if shorthand == :bool
217
+ if as == :bool
200
218
  # if a second block is present, and we can't coerce the xml value
201
219
  # to bool, we need to be able to pass it to the user-provided block
202
- shorthand = block ? :bool_combined : :bool_standalone
220
+ as = (block ? :bool_combined : :bool_standalone)
203
221
  end
204
- [BLOCK_SHORTHANDS[shorthand], block].compact
222
+ as = self.class.block_shorthands.fetch(as) do
223
+ unless as.respond_to?(:from_xml) || as.try(:first).respond_to?(:from_xml) || (as.is_a?(Hash) && !(as.keys & HASH_KEYS).empty?)
224
+ ActiveSupport::Deprecation.warn "#{as.inspect} is not a valid type declaration. ROXML will raise in this case in version 3.0" unless as.nil?
225
+ end
226
+ nil
227
+ end
228
+ [as, block].compact
205
229
  end
206
230
 
207
231
  def extract_options!(args)
@@ -210,21 +234,60 @@ module ROXML
210
234
  args.push(opts)
211
235
  opts = {}
212
236
  end
237
+
238
+ @default = opts.delete(:else)
239
+ @to_xml = opts.delete(:to_xml)
240
+ @name_explicit = opts.has_key?(:from) && opts[:from].is_a?(String)
241
+ @cdata = opts.delete(:cdata)
242
+ @required = opts.delete(:required)
243
+ @frozen = opts.delete(:frozen)
244
+ @wrapper = opts.delete(:in)
245
+
246
+ @cdata ||= extract_from_as(opts, :cdata, "Please use :cdata => true")
247
+
248
+ if opts[:as].is_a?(Array) && opts[:as].size > 1
249
+ ActiveSupport::Deprecation.warn ":as should point to a single item. #{opts[:as].join(', ')} should be declared some other way."
250
+ end
251
+
213
252
  opts
214
253
  end
215
254
 
216
- def extract_type(args)
217
- types = (@opts.keys & TYPE_KEYS)
255
+ def extract_from_as(opts, entry, message)
256
+ # remove with deprecateds...
257
+ if [*opts[:as]].include?(entry)
258
+ ActiveSupport::Deprecation.warn ":as => #{entry.inspect} is deprecated. #{message}"
259
+ if opts[:as] == entry
260
+ opts[:as] = nil
261
+ else
262
+ opts[:as].delete(entry)
263
+ end
264
+ true
265
+ end
266
+ end
267
+
268
+ def extract_type(args, opts)
269
+ types = (opts.keys & TYPE_KEYS)
218
270
  # type arg
219
271
  if args.one? && types.empty?
220
272
  type = args.first
221
273
  if type.is_a? Array
222
- @opts[:as] << :array
223
- return type.first
274
+ ActiveSupport::Deprecation.warn "Array declarations should be passed as the :as parameter, for future release."
275
+ @array = true
276
+ return type.first || :text
224
277
  elsif type.is_a? Hash
225
- @opts[:hash] = type
226
- return :hash
278
+ ActiveSupport::Deprecation.warn "Hash declarations should be passed as the :as parameter, for future release."
279
+ return HashDefinition.new(type)
280
+ elsif type == :content
281
+ ActiveSupport::Deprecation.warn ":content as a type declaration is deprecated. Use :from => '.' or :from => :content instead"
282
+ opts[:from] = :content
283
+ return :text
284
+ elsif type == :attr
285
+ ActiveSupport::Deprecation.warn ":attr as a type declaration is deprecated. Use :from => '@attr_name' or :from => :attr instead"
286
+ opts[:from].sub!('@', '') if opts[:from].to_s.starts_with?('@') # this is added back next line...
287
+ opts[:from] = opts[:from].nil? ? :attr : "@#{opts[:from]}"
288
+ return :attr
227
289
  else
290
+ ActiveSupport::Deprecation.warn "Type declarations should be passed as the :as parameter, for future release."
228
291
  return type
229
292
  end
230
293
  end
@@ -234,9 +297,22 @@ module ROXML
234
297
  "an options hash, with the type and options optional"
235
298
  end
236
299
 
300
+ if opts[:as].is_a?(Hash)
301
+ return HashDefinition.new(opts[:as])
302
+ elsif opts[:as].respond_to?(:from_xml)
303
+ return opts[:as]
304
+ elsif opts[:as].is_a?(Array) && opts[:as].first.respond_to?(:from_xml)
305
+ @array = true
306
+ return opts[:as].first
307
+ end
308
+
237
309
  # type options
238
310
  if types.one?
239
- @opts[:from] = @opts.delete(types.first)
311
+ opts[:from] = opts.delete(types.first)
312
+ if opts[:from] == :content
313
+ opts[:from] = 'content'
314
+ ActiveSupport::Deprecation.warn ":content is now a reserved as an alias for '.'. Use the string 'content' instead"
315
+ end
240
316
  types.first
241
317
  elsif types.empty?
242
318
  :text
@@ -245,4 +321,4 @@ module ROXML
245
321
  end
246
322
  end
247
323
  end
248
- end
324
+ end
@@ -6,6 +6,11 @@ require 'active_support/version'
6
6
  module ActiveSupport # :nodoc:all
7
7
  module Deprecation
8
8
  class << self
9
+ def warn_with_internals_exclusion(message = nil, callstack = caller)
10
+ warn_without_internals_exclusion(message, callstack.reject {|line| line =~ /\/roxml(-[\d\.]+)?\/lib\// })
11
+ end
12
+ alias_method_chain :warn, :internals_exclusion
13
+
9
14
  if VERSION::MAJOR <= 2 && VERSION::MINOR <= 1
10
15
  def deprecation_message(callstack, message = nil)
11
16
  message ||= "You are using deprecated behavior which will be removed from the next major or minor release"
@@ -17,6 +17,7 @@ module ROXML
17
17
  self
18
18
  end
19
19
  end
20
+ deprecate :to_utf
20
21
 
21
22
  #
22
23
  # Convert this string to iso-8850-1
@@ -29,14 +30,12 @@ module ROXML
29
30
  self
30
31
  end
31
32
  end
33
+ deprecate :to_latin
32
34
  end
33
35
  end
34
36
  end
35
37
  end
36
38
 
37
- class Object
38
- end
39
-
40
39
  class String
41
40
  def between(separator, &block)
42
41
  split(separator).collect(&block).join(separator)
@@ -3,20 +3,21 @@ module ROXML
3
3
  TYPE_KEYS = [:attr, :text, :hash, :content].freeze
4
4
 
5
5
  class HashDefinition # :nodoc:
6
- attr_reader :key, :value, :wrapper
6
+ attr_reader :key, :value
7
+ attr_accessor :wrapper
7
8
 
8
- def initialize(opts, wrapper)
9
+ def initialize(opts)
9
10
  unless (invalid_keys = opts.keys - HASH_KEYS).empty?
10
11
  raise ArgumentError, "Invalid Hash description keys: #{invalid_keys.join(', ')}"
11
12
  end
12
13
 
13
- @wrapper = wrapper
14
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])
15
+ ActiveSupport::Deprecation.warn(":as => {:attrs} is going away in 3.0. Use explicit :key and :value instead.")
16
+ @key = to_hash_args(opts, :from => "@#{opts[:attrs][0]}")
17
+ @value = to_hash_args(opts, :from => "@#{opts[:attrs][1]}")
17
18
  else
18
- @key = to_hash_args opts, *fetch_element(opts, :key)
19
- @value = to_hash_args opts, *fetch_element(opts, :value)
19
+ @key = to_hash_args opts, fetch_element(opts, :key)
20
+ @value = to_hash_args opts, fetch_element(opts, :value)
20
21
  end
21
22
  end
22
23
 
@@ -24,32 +25,32 @@ module ROXML
24
25
  def fetch_element(opts, what)
25
26
  case opts[what]
26
27
  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]]
28
+ if opts[what].keys.one?
29
+ ActiveSupport::Deprecation.warn(":as => {:key => {Type => 'name'} ... } is going away in 3.0. Use explicit :key => {:from => 'name', :as => Type} instead.")
30
+ type = opts[what].keys.first
31
+ case type
32
+ when :attr
33
+ {:from => "@#{opts[what][type]}"}
34
+ when :text
35
+ {:from => opts[what][type]}
36
+ else
37
+ {:as => type, :from => opts[what][type]}
38
+ end
39
+ else
40
+ opts[what]
41
+ end
42
+ when String, Symbol
43
+ {:from => opts[what]}
38
44
  else
39
45
  raise ArgumentError, "unrecognized hash parameter: #{what} => #{opts[what]}"
40
46
  end
41
47
  end
42
48
 
43
- def to_hash_args(args, type, name)
49
+ def to_hash_args(args, opts)
44
50
  args = [args] unless args.is_a? Array
45
51
 
46
52
  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
+ Definition.new(nil, opts)
53
54
  else
54
55
  opts = args.extract_options!
55
56
  raise opts.inspect