aliyun-oss-ex 0.7.0.1402831795

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +19 -0
  3. data/INSTALL +35 -0
  4. data/README +443 -0
  5. data/Rakefile +334 -0
  6. data/bin/oss +6 -0
  7. data/bin/setup.rb +11 -0
  8. data/lib/aliyun/oss.rb +55 -0
  9. data/lib/aliyun/oss/acl.rb +132 -0
  10. data/lib/aliyun/oss/authentication.rb +222 -0
  11. data/lib/aliyun/oss/base.rb +241 -0
  12. data/lib/aliyun/oss/bucket.rb +320 -0
  13. data/lib/aliyun/oss/connection.rb +279 -0
  14. data/lib/aliyun/oss/error.rb +70 -0
  15. data/lib/aliyun/oss/exceptions.rb +134 -0
  16. data/lib/aliyun/oss/extensions.rb +405 -0
  17. data/lib/aliyun/oss/logging.rb +304 -0
  18. data/lib/aliyun/oss/object.rb +612 -0
  19. data/lib/aliyun/oss/owner.rb +45 -0
  20. data/lib/aliyun/oss/parsing.rb +100 -0
  21. data/lib/aliyun/oss/response.rb +181 -0
  22. data/lib/aliyun/oss/service.rb +52 -0
  23. data/lib/aliyun/oss/version.rb +14 -0
  24. data/support/faster-xml-simple/lib/faster_xml_simple.rb +188 -0
  25. data/support/faster-xml-simple/test/regression_test.rb +48 -0
  26. data/support/faster-xml-simple/test/test_helper.rb +18 -0
  27. data/support/faster-xml-simple/test/xml_simple_comparison_test.rb +47 -0
  28. data/support/rdoc/code_info.rb +212 -0
  29. data/test/acl_test.rb +70 -0
  30. data/test/authentication_test.rb +114 -0
  31. data/test/base_test.rb +137 -0
  32. data/test/bucket_test.rb +75 -0
  33. data/test/connection_test.rb +218 -0
  34. data/test/error_test.rb +71 -0
  35. data/test/extensions_test.rb +346 -0
  36. data/test/fixtures.rb +90 -0
  37. data/test/fixtures/buckets.yml +133 -0
  38. data/test/fixtures/errors.yml +34 -0
  39. data/test/fixtures/headers.yml +3 -0
  40. data/test/fixtures/logging.yml +15 -0
  41. data/test/fixtures/loglines.yml +5 -0
  42. data/test/fixtures/logs.yml +7 -0
  43. data/test/fixtures/policies.yml +16 -0
  44. data/test/logging_test.rb +90 -0
  45. data/test/mocks/fake_response.rb +27 -0
  46. data/test/object_test.rb +221 -0
  47. data/test/parsing_test.rb +67 -0
  48. data/test/remote/acl_test.rb +28 -0
  49. data/test/remote/bucket_test.rb +147 -0
  50. data/test/remote/logging_test.rb +86 -0
  51. data/test/remote/object_test.rb +350 -0
  52. data/test/remote/test_file.data +0 -0
  53. data/test/remote/test_helper.rb +34 -0
  54. data/test/response_test.rb +69 -0
  55. data/test/service_test.rb +24 -0
  56. data/test/test_helper.rb +110 -0
  57. metadata +177 -0
@@ -0,0 +1,70 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun
3
+ module OSS
4
+ # Anything you do that makes a request to OSS could result in an error. If it does, the Aliyun::OSS library will raise an exception
5
+ # specific to the error. All exception that are raised as a result of a request returning an error response inherit from the
6
+ # ResponseError exception. So should you choose to rescue any such exception, you can simple rescue ResponseError.
7
+ #
8
+ # Say you go to delete a bucket, but the bucket turns out to not be empty. This results in a BucketNotEmpty error (one of the many
9
+ # errors listed at http://docs.aliyunwebservices.com/AliyunOSS/2006-03-01/ErrorCodeList.html):
10
+ #
11
+ # begin
12
+ # Bucket.delete('jukebox')
13
+ # rescue ResponseError => error
14
+ # # ...
15
+ # end
16
+ #
17
+ # Once you've captured the exception, you can extract the error message from OSS, as well as the full error response, which includes
18
+ # things like the HTTP response code:
19
+ #
20
+ # error
21
+ # # => #<Aliyun::OSS::BucketNotEmpty The bucket you tried to delete is not empty>
22
+ # error.message
23
+ # # => "The bucket you tried to delete is not empty"
24
+ # error.response.code
25
+ # # => 409
26
+ #
27
+ # You could use this information to redisplay the error in a way you see fit, or just to log the error and continue on.
28
+ class Error
29
+ #:stopdoc:
30
+ attr_accessor :response
31
+ def initialize(error, response = nil)
32
+ @error = error
33
+ @response = response
34
+ @container = Aliyun::OSS
35
+ find_or_create_exception!
36
+ end
37
+
38
+ def raise
39
+ Kernel.raise exception.new(message, response)
40
+ end
41
+
42
+ private
43
+ attr_reader :error, :exception, :container
44
+
45
+ def find_or_create_exception!
46
+ @exception = container.const_defined?(code) ? find_exception : create_exception
47
+ end
48
+
49
+ def find_exception
50
+ exception_class = container.const_get(code)
51
+ Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError)
52
+ exception_class
53
+ end
54
+
55
+ def create_exception
56
+ container.const_set(code, Class.new(ResponseError))
57
+ end
58
+
59
+ def method_missing(method, *args, &block)
60
+ # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||.
61
+ if error.has_key?(method.to_s)
62
+ error[method.to_s]
63
+ else
64
+ super
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ #:startdoc:
@@ -0,0 +1,134 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun
3
+ module OSS
4
+
5
+ # Abstract super class of all Aliyun::OSS exceptions
6
+ class OSSException < StandardError
7
+ end
8
+
9
+ # All responses with a code between 300 and 599 that contain an <Error></Error> body are wrapped in an
10
+ # ErrorResponse which contains an Error object. This Error class generates a custom exception with the name
11
+ # of the xml Error and its message. All such runtime generated exception classes descend from ResponseError
12
+ # and contain the ErrorResponse object so that all code that makes a request can rescue ResponseError and get
13
+ # access to the ErrorResponse.
14
+ class ResponseError < OSSException
15
+ attr_reader :response
16
+ def initialize(message, response)
17
+ @response = response
18
+ super(message)
19
+ end
20
+ end
21
+
22
+ #:stopdoc:
23
+
24
+ # Most ResponseError's are created just time on a need to have basis, but we explicitly define the
25
+ # InternalError exception because we want to explicitly rescue InternalError in some cases.
26
+ class InternalError < ResponseError
27
+ end
28
+
29
+ class NoSuchKey < ResponseError
30
+ end
31
+
32
+ class RequestTimeout < ResponseError
33
+ end
34
+
35
+ # Abstract super class for all invalid options.
36
+ class InvalidOption < OSSException
37
+ end
38
+
39
+ # Raised if an invalid value is passed to the <tt>:access</tt> option when creating a Bucket or an OSSObject.
40
+ class InvalidAccessControlLevel < InvalidOption
41
+ def initialize(valid_levels, access_level)
42
+ super("Valid access control levels are #{valid_levels.inspect}. You specified `#{access_level}'.")
43
+ end
44
+ end
45
+
46
+ # Raised if either the access key id or secret access key arguments are missing when establishing a connection.
47
+ class MissingAccessKey < InvalidOption
48
+ def initialize(missing_keys)
49
+ key_list = missing_keys.map {|key| key.to_s}.join(' and the ')
50
+ super("You did not provide both required access keys. Please provide the #{key_list}.")
51
+ end
52
+ end
53
+
54
+ # Raised if a request is attempted before any connections have been established.
55
+ class NoConnectionEstablished < OSSException
56
+ end
57
+
58
+ # Raised if an unrecognized option is passed when establishing a connection.
59
+ class InvalidConnectionOption < InvalidOption
60
+ def initialize(invalid_options)
61
+ message = "The following connection options are invalid: #{invalid_options.join(', ')}. " +
62
+ "The valid connection options are: #{Connection::Options::VALID_OPTIONS.join(', ')}."
63
+ super(message)
64
+ end
65
+ end
66
+
67
+ # Raised if an invalid bucket name is passed when creating a new Bucket.
68
+ class InvalidBucketName < OSSException
69
+ def initialize(invalid_name)
70
+ message = "`#{invalid_name}' is not a valid bucket name. " +
71
+ "Bucket names must be between 3 and 255 bytes and " +
72
+ "can contain letters, numbers, dashes and underscores."
73
+ super(message)
74
+ end
75
+ end
76
+
77
+ # Raised if an invalid key name is passed when creating an OSSObject.
78
+ class InvalidKeyName < OSSException
79
+ def initialize(invalid_name)
80
+ message = "`#{invalid_name}' is not a valid key name. " +
81
+ "Key names must be no more than 1024 bytes long."
82
+ super(message)
83
+ end
84
+ end
85
+
86
+ # Raised if an invalid value is assigned to an OSSObject's specific metadata name.
87
+ class InvalidMetadataValue < OSSException
88
+ def initialize(invalid_names)
89
+ message = "The following metadata names have invalid values: #{invalid_names.join(', ')}. " +
90
+ "Metadata can not be larger than 2kilobytes."
91
+ super(message)
92
+ end
93
+ end
94
+
95
+ # Raised if the current bucket can not be inferred when not explicitly specifying the target bucket in the calling
96
+ # method's arguments.
97
+ class CurrentBucketNotSpecified < OSSException
98
+ def initialize(address)
99
+ message = "No bucket name can be inferred from your current connection's address (`#{address}')"
100
+ super(message)
101
+ end
102
+ end
103
+
104
+ # Raised when an orphaned OSSObject belonging to no bucket tries to access its (non-existant) bucket.
105
+ class NoBucketSpecified < OSSException
106
+ def initialize
107
+ super('The current object must have its bucket set')
108
+ end
109
+ end
110
+
111
+ # Raised if an attempt is made to save an OSSObject that does not have a key set.
112
+ class NoKeySpecified < OSSException
113
+ def initialize
114
+ super('The current object must have its key set')
115
+ end
116
+ end
117
+
118
+ # Raised if you try to save a deleted object.
119
+ class DeletedObject < OSSException
120
+ def initialize
121
+ super('You can not save a deleted object')
122
+ end
123
+ end
124
+
125
+ class ExceptionClassClash < OSSException #:nodoc:
126
+ def initialize(klass)
127
+ message = "The exception class you tried to create (`#{klass}') exists and is not an exception"
128
+ super(message)
129
+ end
130
+ end
131
+
132
+ #:startdoc:
133
+ end
134
+ end
@@ -0,0 +1,405 @@
1
+ # -*- encoding : utf-8 -*-
2
+ #:stopdoc:
3
+
4
+ class Hash
5
+ # By default, only instances of Hash itself are extractable.
6
+ # Subclasses of Hash may implement this method and return
7
+ # true to declare themselves as extractable. If a Hash
8
+ # is extractable, Array#extract_options! pops it from
9
+ # the Array when it is the last element of the Array.
10
+ def extractable_options?
11
+ instance_of?(Hash)
12
+ end
13
+
14
+ def to_query_string(include_question_mark = true)
15
+ query_string = ''
16
+ unless empty?
17
+ query_string << '?' if include_question_mark
18
+ query_string << inject([]) do |params, (key, value)|
19
+ params << "#{key}=#{value}"
20
+ end.join('&')
21
+ end
22
+ query_string
23
+ end
24
+
25
+ def to_normalized_options
26
+ # Convert all option names to downcased strings, and replace underscores with hyphens
27
+ inject({}) do |normalized_options, (name, value)|
28
+ normalized_options[name.to_header] = value.to_s
29
+ normalized_options
30
+ end
31
+ end
32
+
33
+ def to_normalized_options!
34
+ replace(to_normalized_options)
35
+ end
36
+ end
37
+
38
+ class Array
39
+ # Extracts options from a set of arguments. Removes and returns the last
40
+ # element in the array if it's a hash, otherwise returns a blank hash.
41
+ #
42
+ # def options(*args)
43
+ # args.extract_options!
44
+ # end
45
+ #
46
+ # options(1, 2) # => {}
47
+ # options(1, 2, a: :b) # => {:a=>:b}
48
+ def extract_options!
49
+ if last.is_a?(Hash) && last.extractable_options?
50
+ pop
51
+ else
52
+ {}
53
+ end
54
+ end
55
+ end
56
+
57
+ class String
58
+ if RUBY_VERSION <= '1.9'
59
+ def previous!
60
+ self[-1] -= 1
61
+ self
62
+ end
63
+ else
64
+ def previous!
65
+ self[-1] = (self[-1].ord - 1).chr
66
+ self
67
+ end
68
+ end
69
+
70
+ def tap
71
+ yield(self)
72
+ self
73
+ end unless ''.respond_to?(:tap)
74
+
75
+ def previous
76
+ dup.previous!
77
+ end
78
+
79
+ def to_header
80
+ downcase.tr('_', '-')
81
+ end
82
+
83
+ # ActiveSupport adds an underscore method to String so let's just use that one if
84
+ # we find that the method is already defined
85
+ def underscore
86
+ gsub(/::/, '/').
87
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
88
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
89
+ tr("-", "_").downcase
90
+ end unless public_method_defined? :underscore
91
+
92
+ if RUBY_VERSION >= '1.9'
93
+ def valid_utf8?
94
+ dup.force_encoding('UTF-8').valid_encoding?
95
+ end
96
+ else
97
+ def valid_utf8?
98
+ scan(Regexp.new('[^\x00-\xa0]', nil, 'u')) { |s| s.unpack('U') }
99
+ true
100
+ rescue ArgumentError
101
+ false
102
+ end
103
+ end
104
+
105
+ # All paths in in OSS have to be valid unicode so this takes care of
106
+ # cleaning up any strings that aren't valid utf-8 according to String#valid_utf8?
107
+ if RUBY_VERSION >= '1.9'
108
+ def remove_extended!
109
+ sanitized_string = ''
110
+ each_byte do |byte|
111
+ character = byte.chr
112
+ sanitized_string << character if character.ascii_only?
113
+ end
114
+ sanitized_string
115
+ end
116
+ else
117
+ def remove_extended!
118
+ #gsub!(/[\x80-\xFF]/) { "%02X" % $&[0] }
119
+ gsub!(Regexp.new('[\x80-\xFF]')) { "%02X" % $&[0] }
120
+ end
121
+ end
122
+
123
+ def remove_extended
124
+ dup.remove_extended!
125
+ end
126
+ end
127
+
128
+ class CoercibleString < String
129
+ class << self
130
+ def coerce(string)
131
+ new(string).coerce
132
+ end
133
+ end
134
+
135
+ def coerce
136
+ case self
137
+ when 'true'; true
138
+ when 'false'; false
139
+ # Don't coerce numbers that start with zero
140
+ when /^[1-9]+\d*$/; Integer(self)
141
+ when datetime_format; Time.parse(self)
142
+ else
143
+ self
144
+ end
145
+ end
146
+
147
+ private
148
+ # Lame hack since Date._parse is so accepting. OSS dates are of the form: '2006-10-29T23:14:47.000Z'
149
+ # so unless the string looks like that, don't even try, otherwise it might convert an object's
150
+ # key from something like '03 1-2-3-Apple-Tree.mp3' to Sat Feb 03 00:00:00 CST 2001.
151
+ def datetime_format
152
+ /^\d{4}-\d{2}-\d{2}\w\d{2}:\d{2}:\d{2}/
153
+ end
154
+ end
155
+
156
+ class Symbol
157
+ def to_header
158
+ to_s.to_header
159
+ end
160
+ end
161
+
162
+ module Kernel
163
+ def __method__(depth = 0)
164
+ caller[depth][/`([^']+)'/, 1]
165
+ end if RUBY_VERSION <= '1.8.7'
166
+
167
+ def __called_from__
168
+ caller[1][/`([^']+)'/, 1]
169
+ end if RUBY_VERSION > '1.8.7'
170
+
171
+ def expirable_memoize(reload = false, storage = nil)
172
+ current_method = RUBY_VERSION > '1.8.7' ? __called_from__ : __method__(1)
173
+ storage = "@#{storage || current_method}"
174
+ if reload
175
+ instance_variable_set(storage, nil)
176
+ else
177
+ if cache = instance_variable_get(storage)
178
+ return cache
179
+ end
180
+ end
181
+ instance_variable_set(storage, yield)
182
+ end
183
+
184
+ def require_library_or_gem(library, gem_name = nil)
185
+ if RUBY_VERSION >= '1.9'
186
+ gem(gem_name || library, '>=0')
187
+ end
188
+ require library
189
+ rescue LoadError => library_not_installed
190
+ begin
191
+ require 'rubygems'
192
+ require library
193
+ rescue LoadError
194
+ raise library_not_installed
195
+ end
196
+ end
197
+ end
198
+
199
+ class Object
200
+ def returning(value)
201
+ yield(value)
202
+ value
203
+ end
204
+ end
205
+
206
+ class Module
207
+ def memoized(method_name)
208
+ original_method = "unmemoized_#{method_name}_#{Time.now.to_i}"
209
+ alias_method original_method, method_name
210
+ module_eval(<<-EVAL, __FILE__, __LINE__)
211
+ def #{method_name}(reload = false, *args, &block)
212
+ expirable_memoize(reload) do
213
+ send(:#{original_method}, *args, &block)
214
+ end
215
+ end
216
+ EVAL
217
+ end
218
+
219
+ def constant(name, value)
220
+ unless const_defined?(name)
221
+ const_set(name, value)
222
+ module_eval(<<-EVAL, __FILE__, __LINE__)
223
+ def self.#{name.to_s.downcase}
224
+ #{name.to_s}
225
+ end
226
+ EVAL
227
+ end
228
+ end
229
+
230
+ # Transforms MarcelBucket into
231
+ #
232
+ # class MarcelBucket < Aliyun::OSS::Bucket
233
+ # set_current_bucket_to 'marcel'
234
+ # end
235
+ def const_missing_from_oss_library(sym)
236
+ if sym.to_s =~ /^(\w+)(Bucket|OSSObject)$/
237
+ const = const_set(sym, Class.new(Aliyun::OSS.const_get($2)))
238
+ const.current_bucket = $1.underscore
239
+ const
240
+ else
241
+ const_missing_not_from_oss_library(sym)
242
+ end
243
+ end
244
+ alias_method :const_missing_not_from_oss_library, :const_missing
245
+ alias_method :const_missing, :const_missing_from_oss_library
246
+ end
247
+
248
+
249
+ class Class
250
+ def mattr_reader(*syms)
251
+ options = syms.extract_options!
252
+ syms.each do |sym|
253
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
254
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
255
+ @@#{sym} = nil unless defined? @@#{sym}
256
+
257
+ def self.#{sym}
258
+ @@#{sym}
259
+ end
260
+ EOS
261
+
262
+ unless options[:instance_reader] == false || options[:instance_accessor] == false
263
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
264
+ def #{sym}
265
+ @@#{sym}
266
+ end
267
+ EOS
268
+ end
269
+ class_variable_set("@@#{sym}", yield) if block_given?
270
+ end
271
+ end
272
+ alias :cattr_reader :mattr_reader
273
+
274
+ def mattr_writer(*syms)
275
+ options = syms.extract_options!
276
+ syms.each do |sym|
277
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
278
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
279
+ @@#{sym} = nil unless defined? @@#{sym}
280
+
281
+ def self.#{sym}=(obj)
282
+ @@#{sym} = obj
283
+ end
284
+ EOS
285
+
286
+ unless options[:instance_writer] == false || options[:instance_accessor] == false
287
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
288
+ def #{sym}=(obj)
289
+ @@#{sym} = obj
290
+ end
291
+ EOS
292
+ end
293
+ send("#{sym}=", yield) if block_given?
294
+ end
295
+ end
296
+ alias :cattr_writer :mattr_writer
297
+
298
+ def mattr_accessor(*syms, &blk)
299
+ mattr_reader(*syms, &blk)
300
+ mattr_writer(*syms, &blk)
301
+ end
302
+ alias :cattr_accessor :mattr_accessor
303
+ end if Class.instance_methods(false).grep(/^cattr_(?:reader|writer|accessor)$/).empty?
304
+
305
+ module SelectiveAttributeProxy
306
+ def self.included(klass)
307
+ klass.extend(ClassMethods)
308
+ klass.class_eval(<<-EVAL, __FILE__, __LINE__)
309
+ cattr_accessor :attribute_proxy
310
+ cattr_accessor :attribute_proxy_options
311
+
312
+ # Default name for attribute storage
313
+ self.attribute_proxy = :attributes
314
+ self.attribute_proxy_options = {:exclusively => true}
315
+
316
+ private
317
+ # By default proxy all attributes
318
+ def proxiable_attribute?(name)
319
+ return true unless self.class.attribute_proxy_options[:exclusively]
320
+ send(self.class.attribute_proxy).has_key?(name)
321
+ end
322
+
323
+ def method_missing(method, *args, &block)
324
+ # Autovivify attribute storage
325
+ if method == self.class.attribute_proxy
326
+ ivar = "@\#{method}"
327
+ instance_variable_set(ivar, {}) unless instance_variable_get(ivar).is_a?(Hash)
328
+ instance_variable_get(ivar)
329
+ # Delegate to attribute storage
330
+ elsif method.to_s =~ /^(\\w+)(=?)$/ && proxiable_attribute?($1)
331
+ attributes_hash_name = self.class.attribute_proxy
332
+ $2.empty? ? send(attributes_hash_name)[$1] : send(attributes_hash_name)[$1] = args.first
333
+ else
334
+ super
335
+ end
336
+ end
337
+ EVAL
338
+ end
339
+
340
+ module ClassMethods
341
+ def proxy_to(attribute_name, options = {})
342
+ if attribute_name.is_a?(Hash)
343
+ options = attribute_name
344
+ else
345
+ self.attribute_proxy = attribute_name
346
+ end
347
+ self.attribute_proxy_options = options
348
+ end
349
+ end
350
+ end
351
+
352
+ # When streaming data up, Net::HTTPGenericRequest hard codes a chunk size of 1k. For large files this
353
+ # is an unfortunately low chunk size, so here we make it use a much larger default size and move it into a method
354
+ # so that the implementation of send_request_with_body_stream doesn't need to be changed to change the chunk size (at least not anymore
355
+ # than I've already had to...).
356
+ module Net
357
+ class HTTPGenericRequest
358
+ def send_request_with_body_stream(sock, ver, path, f)
359
+ raise ArgumentError, "Content-Length not given and Transfer-Encoding is not `chunked'" unless content_length() or chunked?
360
+ unless content_type()
361
+ warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE
362
+ set_content_type 'application/x-www-form-urlencoded'
363
+ end
364
+ write_header sock, ver, path
365
+ if chunked?
366
+ while s = f.read(chunk_size)
367
+ sock.write(sprintf("%x\r\n", s.length) << s << "\r\n")
368
+ end
369
+ sock.write "0\r\n\r\n"
370
+ else
371
+ while s = f.read(chunk_size)
372
+ sock.write s
373
+ end
374
+ end
375
+ end
376
+
377
+ def chunk_size
378
+ 1048576 # 1 megabyte
379
+ end
380
+ end
381
+
382
+ # Net::HTTP before 1.8.4 doesn't have the use_ssl? method or the Delete request type
383
+ class HTTP
384
+ def use_ssl?
385
+ @use_ssl
386
+ end unless public_method_defined? :use_ssl?
387
+
388
+ class Delete < HTTPRequest
389
+ METHOD = 'DELETE'
390
+ REQUEST_HAS_BODY = false
391
+ RESPONSE_HAS_BODY = true
392
+ end unless const_defined? :Delete
393
+ end
394
+ end
395
+
396
+ class XmlGenerator < String #:nodoc:
397
+ attr_reader :xml
398
+ def initialize
399
+ @xml = Builder::XmlMarkup.new(:indent => 2, :target => self)
400
+ super()
401
+ build
402
+ end
403
+ end
404
+ #:startdoc:
405
+