aliyun-ess 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: