aliyun-ess 0.1.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.
@@ -0,0 +1,84 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun
3
+ module ESS
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
+ attr_reader :error, :exception, :container
32
+
33
+ def initialize(error, response = nil)
34
+ @error = error
35
+ @response = response
36
+ @container = Aliyun::ESS
37
+ find_or_create_exception!
38
+ end
39
+
40
+ def raise
41
+ Kernel.raise exception.new(message, response)
42
+ end
43
+
44
+ def code
45
+ @code ||= error['code'].sub('.', '::')
46
+ end
47
+
48
+ private
49
+
50
+ def find_or_create_module!(modul)
51
+ container.const_defined?(modul) ? container.const_get(modul) : container.const_set(modul, Module.new)
52
+ end
53
+
54
+ def find_or_create_exception!
55
+ @exception = container.const_defined?(code) ? find_exception : create_exception
56
+ end
57
+
58
+ def find_exception
59
+ exception_class = container.const_get(code)
60
+ Kernel.raise ExceptionClassClash.new(exception_class) unless exception_class.ancestors.include?(ResponseError)
61
+ exception_class
62
+ end
63
+
64
+ def create_exception
65
+ modul_or_clazz, clazz = code.split('::')
66
+ if clazz
67
+ find_or_create_module!(modul_or_clazz).const_set(clazz, Class.new(ResponseError))
68
+ else
69
+ container.const_set(modul_or_clazz, Class.new(ResponseError))
70
+ end
71
+ end
72
+
73
+ def method_missing(method, *args, &block)
74
+ # We actually want nil if the attribute is nil. So we use has_key? rather than [] + ||.
75
+ if error.has_key?(method.to_s)
76
+ error[method.to_s]
77
+ else
78
+ super
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ #:startdoc:
@@ -0,0 +1,82 @@
1
+ # -*- encoding : utf-8 -*-
2
+ module Aliyun
3
+ module ESS
4
+
5
+ # Abstract super class of all Aliyun::OSS exceptions
6
+ class Exception < 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 < Exception
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
+ class InvalidParameter < ResponseError
36
+ end
37
+
38
+ # Abstract super class for all invalid options.
39
+ class InvalidOption < Exception
40
+ end
41
+
42
+ module IncorrectCapacity
43
+ end
44
+
45
+ # Raised if an invalid value is passed to the <tt>:access</tt> option when creating a Bucket or an OSSObject.
46
+ class InvalidAccessControlLevel < InvalidOption
47
+ def initialize(valid_levels, access_level)
48
+ super("Valid access control levels are #{valid_levels.inspect}. You specified `#{access_level}'.")
49
+ end
50
+ end
51
+
52
+ # Raised if either the access key id or secret access key arguments are missing when establishing a connection.
53
+ class MissingAccessKey < InvalidOption
54
+ def initialize(missing_keys)
55
+ key_list = missing_keys.map {|key| key.to_s}.join(' and the ')
56
+ super("You did not provide both required access keys. Please provide the #{key_list}.")
57
+ end
58
+ end
59
+
60
+ # Raised if a request is attempted before any connections have been established.
61
+ class NoConnectionEstablished < Exception
62
+ end
63
+
64
+ # Raised if an unrecognized option is passed when establishing a connection.
65
+ class InvalidConnectionOption < InvalidOption
66
+ def initialize(invalid_options)
67
+ message = "The following connection options are invalid: #{invalid_options.join(', ')}. " +
68
+ "The valid connection options are: #{Connection::Options::VALID_OPTIONS.join(', ')}."
69
+ super(message)
70
+ end
71
+ end
72
+
73
+ class ExceptionClassClash < Exception #:nodoc:
74
+ def initialize(klass)
75
+ message = "The exception class you tried to create (`#{klass}') exists and is not an exception"
76
+ super(message)
77
+ end
78
+ end
79
+
80
+ #:startdoc:
81
+ end
82
+ end
@@ -0,0 +1,174 @@
1
+ # -*- encoding : utf-8 -*-
2
+ #:stopdoc:
3
+
4
+ class Hash
5
+ def slice(*keys)
6
+ keys.map! { |key| key.to_s }
7
+ keys.inject(Hash.new) { |hash, k| hash[k] = self[k] if has_key?(k); hash }
8
+ end unless public_method_defined? :slice
9
+ end
10
+
11
+ class String
12
+ # ActiveSupport adds an underscore method to String so let's just use that one if
13
+ # we find that the method is already defined
14
+ def underscore
15
+ gsub(/::/, '/').
16
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
17
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
18
+ tr("-", "_").downcase
19
+ end unless public_method_defined? :underscore
20
+
21
+ def camelize
22
+ string = to_s.sub(/^[a-z\d]*/) { $&.capitalize }
23
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
24
+ string.gsub!(/\//, '::')
25
+ string
26
+ end unless public_method_defined? :camelize
27
+
28
+ if RUBY_VERSION >= '1.9'
29
+ def valid_utf8?
30
+ dup.force_encoding('UTF-8').valid_encoding?
31
+ end
32
+ else
33
+ def valid_utf8?
34
+ scan(Regexp.new('[^\x00-\xa0]', nil, 'u')) { |s| s.unpack('U') }
35
+ true
36
+ rescue ArgumentError
37
+ false
38
+ end
39
+ end
40
+
41
+ # All paths in in OSS have to be valid unicode so this takes care of
42
+ # cleaning up any strings that aren't valid utf-8 according to String#valid_utf8?
43
+ if RUBY_VERSION >= '1.9'
44
+ def remove_extended!
45
+ sanitized_string = ''
46
+ each_byte do |byte|
47
+ character = byte.chr
48
+ sanitized_string << character if character.ascii_only?
49
+ end
50
+ sanitized_string
51
+ end
52
+ else
53
+ def remove_extended!
54
+ #gsub!(/[\x80-\xFF]/) { "%02X" % $&[0] }
55
+ gsub!(Regext.new('/[\x80-\xFF]')) { "%02X" % $&[0] }
56
+ end
57
+ end
58
+
59
+ def remove_extended
60
+ dup.remove_extended!
61
+ end
62
+ end
63
+
64
+ class CoercibleString < String
65
+ class << self
66
+ def coerce(string)
67
+ new(string).coerce
68
+ end
69
+ end
70
+
71
+ def coerce
72
+ case self
73
+ when 'true'; true
74
+ when 'false'; false
75
+ # Don't coerce numbers that start with zero
76
+ when /^[1-9]+\d*$/; Integer(self)
77
+ when datetime_format; Time.parse(self)
78
+ else
79
+ self
80
+ end
81
+ end
82
+
83
+ private
84
+ # Lame hack since Date._parse is so accepting. OSS dates are of the form: '2006-10-29T23:14:47.000Z'
85
+ # so unless the string looks like that, don't even try, otherwise it might convert an object's
86
+ # key from something like '03 1-2-3-Apple-Tree.mp3' to Sat Feb 03 00:00:00 CST 2001.
87
+ def datetime_format
88
+ /^\d{4}-\d{2}-\d{2}\w\d{2}:\d{2}:\d{2}/
89
+ end
90
+ end
91
+
92
+ class Class # :nodoc:
93
+ def cattr_reader(*syms)
94
+ syms.flatten.each do |sym|
95
+ class_eval(<<-EOS, __FILE__, __LINE__)
96
+ unless defined? @@#{sym}
97
+ @@#{sym} = nil
98
+ end
99
+
100
+ def self.#{sym}
101
+ @@#{sym}
102
+ end
103
+
104
+ def #{sym}
105
+ @@#{sym}
106
+ end
107
+ EOS
108
+ end
109
+ end
110
+
111
+ def cattr_writer(*syms)
112
+ syms.flatten.each do |sym|
113
+ class_eval(<<-EOS, __FILE__, __LINE__)
114
+ unless defined? @@#{sym}
115
+ @@#{sym} = nil
116
+ end
117
+
118
+ def self.#{sym}=(obj)
119
+ @@#{sym} = obj
120
+ end
121
+
122
+ def #{sym}=(obj)
123
+ @@#{sym} = obj
124
+ end
125
+ EOS
126
+ end
127
+ end
128
+
129
+ def cattr_accessor(*syms)
130
+ cattr_reader(*syms)
131
+ cattr_writer(*syms)
132
+ end
133
+ end if Class.instance_methods(false).grep(/^cattr_(?:reader|writer|accessor)$/).empty?
134
+
135
+ module SelectiveAttributeProxy
136
+ def self.included(klass)
137
+ klass.extend(ClassMethods)
138
+ klass.class_eval(<<-EVAL, __FILE__, __LINE__)
139
+ cattr_accessor :attribute_proxy
140
+ cattr_accessor :attribute_proxy_options
141
+
142
+ # Default name for attribute storage
143
+ self.attribute_proxy = :attributes
144
+ self.attribute_proxy_options = {:exclusively => true}
145
+
146
+ private
147
+ # By default proxy all attributes
148
+ def proxiable_attribute?(name)
149
+ return true unless self.class.attribute_proxy_options[:exclusively]
150
+ send(self.class.attribute_proxy).has_key?(name)
151
+ end
152
+
153
+ def method_missing(method, *args, &block)
154
+ # Autovivify attribute storage
155
+ if method == self.class.attribute_proxy
156
+ ivar = "@\#{method}"
157
+ instance_variable_set(ivar, {}) unless instance_variable_get(ivar).is_a?(Hash)
158
+ instance_variable_get(ivar)
159
+ # Delegate to attribute storage
160
+ elsif method.to_s =~ /^(\\w+)(=?)$/ && proxiable_attribute?($1)
161
+ attributes_hash_name = self.class.attribute_proxy
162
+ $2.empty? ? send(attributes_hash_name)[$1] : send(attributes_hash_name)[$1] = args.first
163
+ else
164
+ super
165
+ end
166
+ end
167
+ EVAL
168
+ end
169
+
170
+ module ClassMethods
171
+ end
172
+ end
173
+
174
+ #:startdoc:
@@ -0,0 +1,67 @@
1
+ # -*- encoding : utf-8 -*-
2
+ #:stopdoc:
3
+ module Aliyun
4
+ module ESS
5
+ module Parsing
6
+
7
+ module Typecasting
8
+ def typecast(object)
9
+ case object
10
+ when Hash
11
+ typecast_hash(object)
12
+ when Array
13
+ object.map {|element| typecast(element)}
14
+ when String
15
+ CoercibleString.coerce(object)
16
+ else
17
+ object
18
+ end
19
+ end
20
+
21
+ def typecast_hash(hash)
22
+ keys = hash.keys.map {|key| key.underscore}
23
+ values = hash.values.map {|value| typecast(value)}
24
+ keys.inject({}) do |new_hash, key|
25
+ new_hash[key] = values.slice!(0)
26
+ new_hash
27
+ end
28
+ end
29
+ end
30
+
31
+ class JsonParser < Hash
32
+ include Typecasting
33
+
34
+ attr_reader :body, :code
35
+
36
+ def initialize(body)
37
+ @body = body
38
+ unless body.strip.empty?
39
+ parse
40
+ typecast_json_parsed
41
+ set_code
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def parse
48
+ @json_parsed = JSON.parse(body)
49
+ end
50
+
51
+ def set_code
52
+ @code = self['code']
53
+ end
54
+
55
+ def typecast_json_parsed
56
+ typecast_json = {}
57
+ @json_parsed.dup.each do |key, value| # Some typecasting is destructive so we dup
58
+ typecast_json[key.underscore] = typecast(value)
59
+ end
60
+ # An empty body will try to update with a string so only update if the result is a hash
61
+ update(typecast_json) unless typecast_json.keys.size==0
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ #:startdoc:
@@ -0,0 +1,161 @@
1
+ # -*- encoding : utf-8 -*-
2
+ #:stopdoc:
3
+ module Aliyun
4
+ module ESS
5
+ class Base
6
+ class Response < String
7
+ attr_reader :response, :body, :parsed
8
+ def initialize(response)
9
+ @response = response
10
+ @body = response.body.to_s
11
+ super(body)
12
+ end
13
+
14
+ def headers
15
+ @headers ||= begin
16
+ headers = {}
17
+ response.each do |header, value|
18
+ headers[header] = value
19
+ end
20
+ headers
21
+ end
22
+ end
23
+
24
+ def [](header)
25
+ headers[header]
26
+ end
27
+
28
+ def each(&block)
29
+ headers.each(&block)
30
+ end
31
+
32
+ def code
33
+ response.code.to_i
34
+ end
35
+
36
+ {:success => 200..299, :redirect => 300..399,
37
+ :client_error => 400..499, :server_error => 500..599}.each do |result, code_range|
38
+ class_eval(<<-EVAL, __FILE__, __LINE__)
39
+ def #{result}?
40
+ return false unless response
41
+ (#{code_range}).include? code
42
+ end
43
+ EVAL
44
+ end
45
+
46
+ def error?
47
+ !success? && !parsed.code.nil?
48
+ end
49
+
50
+ def error
51
+ @error ||= Error.new(parsed, self)
52
+ end
53
+
54
+ def parsed
55
+ # XmlSimple is picky about what kind of object it parses, so we pass in body rather than self
56
+ @parsed ||= Parsing::JsonParser.new(body)
57
+ end
58
+
59
+ def inspect
60
+ "#<%s:0x%s %s %s>" % [self.class, object_id, response.code, response.message]
61
+ end
62
+ end
63
+ end
64
+
65
+ class ScalingGroup
66
+ class Response < Base::Response
67
+ def items
68
+ (parsed['scaling_groups'] && parsed['scaling_groups']['scaling_group']) || []
69
+ end
70
+ end
71
+ end
72
+
73
+ class ScalingRule
74
+ class Response < Base::Response
75
+ def items
76
+ (parsed['scaling_rules'] && parsed['scaling_rules']['scaling_rule']) || []
77
+ end
78
+ end
79
+ end
80
+
81
+
82
+ # Requests whose response code is between 300 and 599 and contain an <Error></Error> in their body
83
+ # are wrapped in an Error::Response. This Error::Response contains an Error object which raises an exception
84
+ # that corresponds to the error in the response body. The exception object contains the ErrorResponse, so
85
+ # in all cases where a request happens, you can rescue ResponseError and have access to the ErrorResponse and
86
+ # its Error object which contains information about the ResponseError.
87
+ #
88
+ # begin
89
+ # Bucket.create(..)
90
+ # rescue ResponseError => exception
91
+ # exception.response
92
+ # # => <Error::Response>
93
+ # exception.response.error
94
+ # # => <Error>
95
+ # end
96
+ class Error
97
+ class Response < Base::Response
98
+ def error?
99
+ true
100
+ end
101
+
102
+ def inspect
103
+ "#<%s:0x%s %s %s: '%s'>" % [self.class.name, object_id, response.code, error.code, error.message]
104
+ end
105
+ end
106
+ end
107
+
108
+ # Guess response class name from current class name. If the guessed response class doesn't exist
109
+ # do the same thing to the current class's parent class, up the inheritance heirarchy until either
110
+ # a response class is found or until we get to the top of the heirarchy in which case we just use
111
+ # the the Base response class.
112
+ #
113
+ # Important: This implemantation assumes that the Base class has a corresponding Base::Response.
114
+ class FindResponseClass #:nodoc:
115
+ class << self
116
+ def for(start)
117
+ new(start).find
118
+ end
119
+ end
120
+
121
+ def initialize(start)
122
+ @container = Aliyun::ESS
123
+ @current_class = start
124
+ end
125
+
126
+ def find
127
+ self.current_class = current_class.superclass until response_class_found?
128
+ target.const_get(class_to_find)
129
+ end
130
+
131
+ private
132
+ attr_reader :container
133
+ attr_accessor :current_class
134
+
135
+ def target
136
+ container.const_get(current_name)
137
+ end
138
+
139
+ def target?
140
+ container.const_defined?(current_name)
141
+ end
142
+
143
+ def response_class_found?
144
+ target? && target.const_defined?(class_to_find)
145
+ end
146
+
147
+ def class_to_find
148
+ :Response
149
+ end
150
+
151
+ def current_name
152
+ truncate(current_class)
153
+ end
154
+
155
+ def truncate(klass)
156
+ klass.name[/[^:]+$/]
157
+ end
158
+ end
159
+ end
160
+ end
161
+ #:startdoc: