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.
- data/History.txt +54 -0
- data/Manifest.txt +9 -6
- data/README.rdoc +24 -17
- data/Rakefile +2 -1
- data/TODO +30 -31
- data/examples/active_record.rb +69 -0
- data/examples/amazon.rb +1 -1
- data/examples/current_weather.rb +1 -1
- data/examples/posts.rb +8 -8
- data/examples/twitter.rb +2 -2
- data/examples/xml/active_record.xml +70 -0
- data/lib/roxml.rb +174 -174
- data/lib/roxml/definition.rb +165 -89
- data/lib/roxml/extensions/deprecation.rb +5 -0
- data/lib/roxml/extensions/string/conversions.rb +2 -3
- data/lib/roxml/hash_definition.rb +26 -25
- data/lib/roxml/xml.rb +15 -6
- data/lib/roxml/xml/parsers/libxml.rb +9 -6
- data/lib/roxml/xml/parsers/rexml.rb +1 -1
- data/lib/roxml/xml/references.rb +14 -17
- data/roxml.gemspec +8 -5
- data/spec/definition_spec.rb +563 -0
- data/spec/examples/active_record_spec.rb +43 -0
- data/spec/roxml_spec.rb +372 -0
- data/spec/shared_specs.rb +15 -0
- data/spec/spec_helper.rb +21 -4
- data/spec/string_spec.rb +15 -0
- data/spec/xml/parser_spec.rb +22 -0
- data/test/fixtures/book_valid.xml +1 -1
- data/test/fixtures/person_with_guarded_mothers.xml +3 -3
- data/test/mocks/mocks.rb +57 -45
- data/test/unit/definition_test.rb +161 -12
- data/test/unit/deprecations_test.rb +97 -0
- data/test/unit/to_xml_test.rb +30 -1
- data/test/unit/xml_bool_test.rb +15 -3
- data/test/unit/xml_construct_test.rb +6 -6
- data/test/unit/xml_hash_test.rb +18 -0
- data/test/unit/xml_initialize_test.rb +6 -3
- data/test/unit/xml_object_test.rb +66 -5
- data/test/unit/xml_text_test.rb +3 -0
- metadata +23 -15
- data/test/unit/array_test.rb +0 -16
- data/test/unit/freeze_test.rb +0 -71
- data/test/unit/inheritance_test.rb +0 -63
- data/test/unit/overriden_output_test.rb +0 -33
- data/test/unit/roxml_test.rb +0 -60
- data/test/unit/string_test.rb +0 -11
data/lib/roxml/definition.rb
CHANGED
@@ -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
|
-
@
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
29
|
-
|
37
|
+
opts = extract_options!(args)
|
38
|
+
opts[:as] ||= :bool if @accessor.to_s.ends_with?('?')
|
30
39
|
|
31
|
-
@
|
32
|
-
@opts[:as]
|
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
|
-
|
42
|
-
else
|
43
|
-
@opts[:from] ||= variable_name
|
55
|
+
opts[:from] ||= @type.tag_name
|
44
56
|
end
|
45
57
|
|
46
|
-
|
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 =
|
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.
|
80
|
+
accessor.to_s.chomp('?')
|
59
81
|
end
|
60
82
|
|
61
83
|
def hash
|
62
|
-
|
84
|
+
if hash?
|
85
|
+
@type.wrapper ||= name
|
86
|
+
@type
|
87
|
+
end
|
63
88
|
end
|
64
89
|
|
65
90
|
def hash?
|
66
|
-
@type
|
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
|
-
@
|
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
|
103
|
-
|
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
|
110
|
-
when :
|
111
|
-
when
|
112
|
-
when
|
113
|
-
|
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
|
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
|
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
|
-
|
152
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
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
|
191
|
-
ActiveSupport::Deprecation.warn ":as => :integer is deprecated. Use :as => Integer instead" if as
|
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
|
-
|
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
|
-
|
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
|
-
|
220
|
+
as = (block ? :bool_combined : :bool_standalone)
|
203
221
|
end
|
204
|
-
|
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
|
217
|
-
|
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
|
-
|
223
|
-
|
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
|
-
|
226
|
-
return
|
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
|
-
|
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
|
6
|
+
attr_reader :key, :value
|
7
|
+
attr_accessor :wrapper
|
7
8
|
|
8
|
-
def initialize(opts
|
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
|
-
|
16
|
-
@
|
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,
|
19
|
-
@value = to_hash_args opts,
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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,
|
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
|
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
|