qoobaa-stree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,199 @@
1
+ module Stree
2
+
3
+ # Class responsible for handling connections to amazon hosts
4
+ class Connection
5
+ attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug
6
+ alias :use_ssl? :use_ssl
7
+
8
+ # ==== Parameters:
9
+ # +options+:: Hash of options
10
+ #
11
+ # ==== Options:
12
+ # +access_key_id+:: access key id
13
+ # +secret_access_key+:: secret access key
14
+ # +use_ssl+:: optional, defaults to false
15
+ # +debug+:: optional, defaults to false
16
+ # +timeout+:: optional, for Net::HTTP
17
+ def initialize(options = {})
18
+ @access_key_id = options[:access_key_id]
19
+ @secret_access_key = options[:secret_access_key]
20
+ @use_ssl = options[:use_ssl] || false
21
+ @debug = options[:debug]
22
+ @timeout = options[:timeout]
23
+ end
24
+
25
+ # Makes request with given HTTP method, sets missing parameters,
26
+ # adds signature to request header and returns response object
27
+ # (Net::HTTPResponse)
28
+ #
29
+ # ==== Parameters:
30
+ # +method+:: HTTP Method symbol, can be :get, :put, :delete
31
+ # +options+:: hash of options
32
+ #
33
+ # ==== Options:
34
+ # +host+:: hostname to connecto to, optional, defaults to s3.amazonaws.com[s3.amazonaws.com]
35
+ # +path+:: path to send request to, required, throws ArgumentError if not given
36
+ # +body+:: request body, only meaningful for :put request
37
+ # +params+:: parameters to add to query string for request, can be String or Hash
38
+ # +headers+:: Hash of headers fields to add to request header
39
+ #
40
+ # ==== Returns:
41
+ # Net::HTTPResponse object -- response from remote server
42
+ def request(method, options)
43
+ host = options[:host] || HOST
44
+ path = options[:path] or raise ArgumentError.new("no path given")
45
+ body = options[:body]
46
+ params = options[:params]
47
+ headers = options[:headers]
48
+
49
+ if params
50
+ params = params.is_a?(String) ? params : self.class.parse_params(params)
51
+ path << "?#{params}"
52
+ end
53
+
54
+ path = URI.escape(path)
55
+ request = request_class(method).new(path)
56
+
57
+ headers = self.class.parse_headers(headers)
58
+ headers.each do |key, value|
59
+ request[key] = value
60
+ end
61
+
62
+ request.body = body
63
+
64
+ send_request(host, request)
65
+ end
66
+
67
+ # Helper function to parser parameters and create single string of params
68
+ # added to questy string
69
+ #
70
+ # ==== Parameters:
71
+ # +params+: Hash of parameters if form <tt>key => value|nil</tt>
72
+ #
73
+ # ==== Returns:
74
+ # String -- containing all parameters joined in one params string,
75
+ # i.e. <tt>param1=val&param2&param3=0</tt>
76
+ def self.parse_params(params)
77
+ interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location]
78
+
79
+ result = []
80
+ params.each do |key, value|
81
+ if interesting_keys.include?(key)
82
+ parsed_key = key.to_s.gsub("_", "-")
83
+ case value
84
+ when nil
85
+ result << parsed_key
86
+ else
87
+ result << "#{parsed_key}=#{value}"
88
+ end
89
+ end
90
+ end
91
+ result.join("&")
92
+ end
93
+
94
+ # Helper function to change headers from symbols, to in correct
95
+ # form (i.e. with '-' instead of '_')
96
+ #
97
+ # ==== Parameters:
98
+ # +headers+:: Hash of pairs <tt>headername => value</tt>,
99
+ # where value can be Range (for Range header) or any other
100
+ # value which can be translated to string
101
+ #
102
+ # ==== Returns:
103
+ # Hash of headers translated from symbol to string,
104
+ # containing only interesting headers
105
+ def self.parse_headers(headers)
106
+ interesting_keys = [:content_type, :x_amz_acl, :range,
107
+ :if_modified_since, :if_unmodified_since,
108
+ :if_match, :if_none_match,
109
+ :content_disposition, :content_encoding,
110
+ :x_amz_copy_source, :x_amz_metadata_directive,
111
+ :x_amz_copy_source_if_match,
112
+ :x_amz_copy_source_if_none_match,
113
+ :x_amz_copy_source_if_unmodified_since,
114
+ :x_amz_copy_source_if_modified_since]
115
+
116
+ parsed_headers = {}
117
+ if headers
118
+ headers.each do |key, value|
119
+ if interesting_keys.include?(key)
120
+ parsed_key = key.to_s.gsub("_", "-")
121
+ parsed_value = value
122
+ case value
123
+ when Range
124
+ parsed_value = "bytes=#{value.first}-#{value.last}"
125
+ end
126
+ parsed_headers[parsed_key] = parsed_value
127
+ end
128
+ end
129
+ end
130
+ parsed_headers
131
+ end
132
+
133
+ private
134
+
135
+ def request_class(method)
136
+ case method
137
+ when :get
138
+ request_class = Net::HTTP::Get
139
+ when :put
140
+ request_class = Net::HTTP::Put
141
+ when :delete
142
+ request_class = Net::HTTP::Delete
143
+ end
144
+ end
145
+
146
+ def port
147
+ use_ssl ? 443 : 80
148
+ end
149
+
150
+ def http(host)
151
+ http = Net::HTTP.new(host, port)
152
+ http.set_debug_output(STDOUT) if @debug
153
+ http.use_ssl = @use_ssl
154
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
155
+ http.read_timeout = @timeout if @timeout
156
+ http
157
+ end
158
+
159
+ def send_request(host, request)
160
+ response = http(host).start do |http|
161
+ host = http.address
162
+
163
+ request['Date'] ||= Time.now.httpdate
164
+
165
+ if request.body
166
+ request["Content-Type"] ||= "application/octet-stream"
167
+ request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp
168
+ end
169
+
170
+ request["Authorization"] = Signature.generate(:host => host,
171
+ :request => request,
172
+ :access_key_id => access_key_id,
173
+ :secret_access_key => secret_access_key)
174
+ http.request(request)
175
+ end
176
+
177
+ handle_response(response)
178
+ end
179
+
180
+ def handle_response(response)
181
+ case response.code.to_i
182
+ when 200...300
183
+ response
184
+ when 300...600
185
+ if response.body.nil? || response.body.empty?
186
+ raise Error::ResponseError.new(nil, response)
187
+ else
188
+ xml = XmlSimple.xml_in(response.body)
189
+ message = xml["Message"].first
190
+ code = xml["Code"].first
191
+ raise Error::ResponseError.exception(code).new(message, response)
192
+ end
193
+ else
194
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
195
+ end
196
+ response
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,108 @@
1
+ module Stree
2
+ module Error
3
+
4
+ # All responses with a code between 300 and 599 that contain an
5
+ # <Error></Error> body are wrapped in an ErrorResponse which
6
+ # contains an Error object. This Error class generates a custom
7
+ # exception with the name of the xml Error and its message. All
8
+ # such runtime generated exception classes descend from
9
+ # ResponseError and contain the ErrorResponse object so that all
10
+ # code that makes a request can rescue ResponseError and get
11
+ # access to the ErrorResponse.
12
+ class ResponseError < StandardError
13
+ attr_reader :response
14
+
15
+ # ==== Parameters:
16
+ # +message+:: what went wrong
17
+ # +response+:: Net::HTTPResponse object or nil
18
+ def initialize(message, response)
19
+ @response = response
20
+ super(message)
21
+ end
22
+
23
+ # Factory for all other Exception classes in module, each for every
24
+ # error response available from AmazonAWS
25
+ #
26
+ # ==== Parameters:
27
+ # +code+:: code name of exception
28
+ #
29
+ # ==== Returns:
30
+ # Descendant of ResponseError suitable for that exception code or ResponseError class
31
+ # if no class found
32
+ def self.exception(code)
33
+ Stree::Error.const_get(code)
34
+ rescue NameError
35
+ ResponseError
36
+ end
37
+ end
38
+
39
+ #:stopdoc:
40
+
41
+ class AccessDenied < ResponseError; end
42
+ class AccountProblem < ResponseError; end
43
+ class AmbiguousGrantByEmailAddress < ResponseError; end
44
+ class BadDigest < ResponseError; end
45
+ class BucketAlreadyExists < ResponseError; end
46
+ class BucketAlreadyOwnedByYou < ResponseError; end
47
+ class BucketNotEmpty < ResponseError; end
48
+ class CredentialsNotSupported < ResponseError; end
49
+ class CrossLocationLoggingProhibited < ResponseError; end
50
+ class EntityTooSmall < ResponseError; end
51
+ class EntityTooLarge < ResponseError; end
52
+ class ExpiredToken < ResponseError; end
53
+ class IncompleteBody < ResponseError; end
54
+ class IncorrectNumberOfFilesInPostRequestPOST < ResponseError; end
55
+ class InlineDataTooLarge < ResponseError; end
56
+ class InternalError < ResponseError; end
57
+ class InvalidAccessKeyId < ResponseError; end
58
+ class InvalidAddressingHeader < ResponseError; end
59
+ class InvalidArgument < ResponseError; end
60
+ class InvalidBucketName < ResponseError; end
61
+ class InvalidDigest < ResponseError; end
62
+ class InvalidLocationConstraint < ResponseError; end
63
+ class InvalidPayer < ResponseError; end
64
+ class InvalidPolicyDocument < ResponseError; end
65
+ class InvalidRange < ResponseError; end
66
+ class InvalidSecurity < ResponseError; end
67
+ class InvalidSOAPRequest < ResponseError; end
68
+ class InvalidStorageClass < ResponseError; end
69
+ class InvalidTargetBucketForLogging < ResponseError; end
70
+ class InvalidToken < ResponseError; end
71
+ class InvalidURI < ResponseError; end
72
+ class KeyTooLong < ResponseError; end
73
+ class MalformedACLError < ResponseError; end
74
+ class MalformedACLError < ResponseError; end
75
+ class MalformedPOSTRequest < ResponseError; end
76
+ class MalformedXML < ResponseError; end
77
+ class MaxMessageLengthExceeded < ResponseError; end
78
+ class MaxPostPreDataLengthExceededErrorYour < ResponseError; end
79
+ class MetadataTooLarge < ResponseError; end
80
+ class MethodNotAllowed < ResponseError; end
81
+ class MissingAttachment < ResponseError; end
82
+ class MissingContentLength < ResponseError; end
83
+ class MissingRequestBodyError < ResponseError; end
84
+ class MissingSecurityElement < ResponseError; end
85
+ class MissingSecurityHeader < ResponseError; end
86
+ class NoLoggingStatusForKey < ResponseError; end
87
+ class NoSuchBucket < ResponseError; end
88
+ class NoSuchKey < ResponseError; end
89
+ class NotImplemented < ResponseError; end
90
+ class NotSignedUp < ResponseError; end
91
+ class OperationAborted < ResponseError; end
92
+ class PermanentRedirect < ResponseError; end
93
+ class PreconditionFailed < ResponseError; end
94
+ class Redirect < ResponseError; end
95
+ class RequestIsNotMultiPartContent < ResponseError; end
96
+ class RequestTimeout < ResponseError; end
97
+ class RequestTimeTooSkewed < ResponseError; end
98
+ class RequestTorrentOfBucketError < ResponseError; end
99
+ class SignatureDoesNotMatch < ResponseError; end
100
+ class SlowDown < ResponseError; end
101
+ class TemporaryRedirect < ResponseError; end
102
+ class TokenRefreshRequired < ResponseError; end
103
+ class TooManyBuckets < ResponseError; end
104
+ class UnexpectedContent < ResponseError; end
105
+ class UnresolvableGrantByEmailAddress < ResponseError; end
106
+ class UserKeyMustBeSpecified < ResponseError; end
107
+ end
108
+ end
@@ -0,0 +1,196 @@
1
+ module Stree
2
+
3
+ # Class responsible for handling objects stored in S3 buckets
4
+ class Object
5
+ extend Forwardable
6
+
7
+ attr_accessor :content_type, :content_disposition, :content_encoding
8
+ attr_reader :last_modified, :etag, :size, :bucket, :key, :acl
9
+ attr_writer :content
10
+
11
+ def_instance_delegators :bucket, :name, :service, :bucket_request, :vhost?, :host, :path_prefix
12
+ def_instance_delegators :service, :protocol, :port
13
+
14
+ # Compares the object with other object. Returns true if the key
15
+ # of the objects are the same, and both have the same buckets (see
16
+ # bucket equality)
17
+ def ==(other)
18
+ self.key == other.key and self.bucket == other.bucket
19
+ end
20
+
21
+ # Returns full key of the object: e.g. +bucket-name/object/key.ext+
22
+ def full_key
23
+ [name, key].join("/")
24
+ end
25
+
26
+ # Assigns a new +key+ to the object, raises ArgumentError if given
27
+ # key is not valid key name
28
+ def key=(key)
29
+ raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
30
+ @key ||= key
31
+ end
32
+
33
+ # Assigns a new ACL to the object. Please note that ACL is not
34
+ # retrieved from the server and set to "public-read" by default.
35
+ # ==== Example
36
+ # object.acl = :public_read
37
+ def acl=(acl)
38
+ @acl = acl.to_s.gsub("_", "-") if acl
39
+ end
40
+
41
+ # Retrieves the object from the server. Method is used to download
42
+ # object information only (content-type, size and so on). It does
43
+ # NOT download the content of the object (use the content method
44
+ # to do it).
45
+ def retrieve
46
+ response = object_request(:get, :headers => { :range => 0..0 })
47
+ parse_headers(response)
48
+ self
49
+ end
50
+
51
+ # Retrieves the object from the server, returns true if the
52
+ # object exists or false otherwise. Uses retrieve method, but
53
+ # catches NoSuchKey exception and returns false when it happens
54
+ def exists?
55
+ retrieve
56
+ true
57
+ rescue Error::NoSuchKey
58
+ false
59
+ end
60
+
61
+ # Download the content of the object, and caches it. Pass true
62
+ # to clear the cache and download the object again.
63
+ def content(reload = false)
64
+ if reload or @content.nil?
65
+ response = object_request(:get)
66
+ parse_headers(response)
67
+ self.content = response.body
68
+ end
69
+ @content
70
+ end
71
+
72
+ # Saves the object, returns true if successfull.
73
+ def save
74
+ body = content.is_a?(IO) ? content.read : content
75
+ response = object_request(:put, :body => body, :headers => dump_headers)
76
+ parse_headers(response)
77
+ true
78
+ end
79
+
80
+ # Copies the file to another key and/or bucket.
81
+ # ==== Options:
82
+ # +key+:: new key to store object in
83
+ # +bucket+:: new bucket to store object in (instance of Stree::Bucket)
84
+ # +acl+:: acl of the copied object (default: "public-read")
85
+ # +content_type+:: content type of the copied object (default: "application/octet-stream")
86
+ def copy(options = {})
87
+ key = options[:key] || self.key
88
+ bucket = options[:bucket] || self.bucket
89
+
90
+ headers = {}
91
+ headers[:x_amz_acl] = options[:acl] || acl || "public-read"
92
+ headers[:content_type] = options[:content_type] || content_type || "application/octet-stream"
93
+ headers[:content_encoding] = options[:content_encoding] if options[:content_encoding]
94
+ headers[:content_disposition] = options[:content_disposition] if options[:content_disposition]
95
+ headers[:x_amz_copy_source] = full_key
96
+ headers[:x_amz_metadata_directive] = "REPLACE"
97
+ headers[:x_amz_copy_source_if_match] = options[:if_match] if options[:if_match]
98
+ headers[:x_amz_copy_source_if_none_match] = options[:if_none_match] if options[:if_none_match]
99
+ headers[:x_amz_copy_source_if_unmodified_since] = options[:if_modified_since] if options[:if_modified_since]
100
+ headers[:x_amz_copy_source_if_modified_since] = options[:if_unmodified_since] if options[:if_unmodified_since]
101
+
102
+ response = bucket.send(:bucket_request, :put, :path => key, :headers => headers)
103
+ self.class.parse_copied(:object => self, :bucket => bucket, :key => key, :body => response.body, :headers => headers)
104
+ end
105
+
106
+ # Destroys the file on the server
107
+ def destroy
108
+ object_request(:delete)
109
+ true
110
+ end
111
+
112
+ # Returns Object's URL using protocol specified in Service,
113
+ # e.g. http://domain.com.s3.amazonaws.com/key/with/path.extension
114
+ def url
115
+ URI.escape("#{protocol}#{host}/#{path_prefix}#{key}")
116
+ end
117
+
118
+ # Returns Object's CNAME URL (without s3.amazonaws.com suffix)
119
+ # using protocol specified in Service,
120
+ # e.g. http://domain.com/key/with/path.extension. (you have to set
121
+ # the CNAME in your DNS before you use the CNAME URL schema).
122
+ def cname_url
123
+ URI.escape("#{protocol}#{name}/#{key}") if bucket.vhost?
124
+ end
125
+
126
+ def inspect #:nodoc:
127
+ "#<#{self.class}:/#{name}/#{key}>"
128
+ end
129
+
130
+ def initialize(bucket, key, options = {}) #:nodoc:
131
+ self.bucket = bucket
132
+ self.key = key
133
+ self.last_modified = options[:last_modified]
134
+ self.etag = options[:etag]
135
+ self.size = options[:size]
136
+ end
137
+
138
+ private
139
+
140
+ attr_writer :last_modified, :etag, :size, :original_key, :bucket
141
+
142
+ def object_request(method, options = {})
143
+ bucket_request(method, options.merge(:path => key))
144
+ end
145
+
146
+ def last_modified=(last_modified)
147
+ @last_modified = Time.parse(last_modified) if last_modified
148
+ end
149
+
150
+ def etag=(etag)
151
+ @etag = etag[1..-2] if etag
152
+ end
153
+
154
+ def dump_headers
155
+ headers = {}
156
+ headers[:x_amz_acl] = @acl || "public-read"
157
+ headers[:content_type] = @content_type || "application/octet-stream"
158
+ headers[:content_encoding] = @content_encoding if @content_encoding
159
+ headers[:content_disposition] = @content_disposition if @content_disposition
160
+ headers
161
+ end
162
+
163
+ def key_valid?(key)
164
+ if (key.nil? or key.empty? or key =~ %r#//#)
165
+ false
166
+ else
167
+ true
168
+ end
169
+ end
170
+
171
+ def parse_headers(response)
172
+ self.etag = response["etag"]
173
+ self.content_type = response["content-type"]
174
+ self.content_disposition = response["content-disposition"]
175
+ self.content_encoding = response["content-encoding"]
176
+ self.last_modified = response["last-modified"]
177
+ self.size = response["content-length"]
178
+ if response["content-range"]
179
+ self.size = response["content-range"].sub(/[^\/]+\//, "").to_i
180
+ end
181
+ end
182
+
183
+ def self.parse_copied(options)
184
+ xml = XmlSimple.xml_in(options[:body])
185
+ etag = xml["ETag"].first
186
+ last_modified = xml["LastModified"].first
187
+ size = options[:object].size
188
+ object = Object.new(options[:bucket], options[:key], :etag => etag, :last_modified => last_modified, :size => size)
189
+ object.acl = options[:headers][:x_amz_acl]
190
+ object.content_type = options[:headers][:content_type]
191
+ object.content_encoding = options[:headers][:content_encoding]
192
+ object.content_disposition = options[:headers][:content_disposition]
193
+ object
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,58 @@
1
+ # Copyright (c) 2008 Ryan Daigle
2
+
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation files
5
+ # (the "Software"), to deal in the Software without restriction,
6
+ # including without limitation the rights to use, copy, modify, merge,
7
+ # publish, distribute, sublicense, and/or sell copies of the Software,
8
+ # and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18
+ # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19
+ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ module Stree
24
+ module Roxy # :nodoc:all
25
+ module Moxie
26
+ # Set up this class to proxy on the given name
27
+ def proxy(name, options = {}, &block)
28
+
29
+ # Make sure args are OK
30
+ original_method = method_defined?(name) ? instance_method(name) : nil
31
+ raise "Cannot proxy an existing method, \"#{name}\", and also have a :to option. Please use one or the other." if
32
+ original_method and options[:to]
33
+
34
+ # If we're proxying an existing method, we need to store
35
+ # the original method and move it out of the way so
36
+ # we can take over
37
+ if original_method
38
+ new_method = "proxied_#{name}"
39
+ alias_method new_method, "#{name}"
40
+ options[:to] = original_method
41
+ end
42
+
43
+ # Thanks to Jerry for this simplification of my original class_eval approach
44
+ # http://ryandaigle.com/articles/2008/11/10/implement-ruby-proxy-objects-with-roxy/comments/8059#comment-8059
45
+ if !original_method or original_method.arity == 0
46
+ define_method name do
47
+ @proxy_for ||= {}
48
+ @proxy_for[name] ||= Proxy.new(self, options, nil, &block)
49
+ end
50
+ else
51
+ define_method name do |*args|
52
+ Proxy.new(self, options, args, &block)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,72 @@
1
+ # Copyright (c) 2008 Ryan Daigle
2
+
3
+ # Permission is hereby granted, free of charge, to any person
4
+ # obtaining a copy of this software and associated documentation files
5
+ # (the "Software"), to deal in the Software without restriction,
6
+ # including without limitation the rights to use, copy, modify, merge,
7
+ # publish, distribute, sublicense, and/or sell copies of the Software,
8
+ # and to permit persons to whom the Software is furnished to do so,
9
+ # subject to the following conditions:
10
+
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18
+ # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19
+ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ module Stree
24
+ module Roxy # :nodoc:all
25
+ # The very simple proxy class that provides a basic pass-through
26
+ # mechanism between the proxy owner and the proxy target.
27
+ class Proxy
28
+
29
+ alias :proxy_instance_eval :instance_eval
30
+ alias :proxy_extend :extend
31
+
32
+ # Make sure the proxy is as dumb as it can be.
33
+ # Blatanly taken from Jim Wierich's BlankSlate post:
34
+ # http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc
35
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^proxy_|^object_id)/ }
36
+
37
+ def initialize(owner, options, args, &block)
38
+ @owner = owner
39
+ @target = options[:to]
40
+ @args = args
41
+
42
+ # Adorn with user-provided proxy methods
43
+ [options[:extend]].flatten.each { |ext| proxy_extend(ext) } if options[:extend]
44
+ proxy_instance_eval &block if block_given?
45
+ end
46
+
47
+ def proxy_owner
48
+ @owner
49
+ end
50
+
51
+ def proxy_target
52
+ if @target.is_a?(Proc)
53
+ @target.call(@owner)
54
+ elsif @target.is_a?(UnboundMethod)
55
+ bound_method = @target.bind(proxy_owner)
56
+ bound_method.arity == 0 ? bound_method.call : bound_method.call(*@args)
57
+ else
58
+ @target
59
+ end
60
+ end
61
+
62
+ # def inspect
63
+ # "#<S3::Roxy::Proxy:0x#{object_id.to_s(16)}>"
64
+ # end
65
+
66
+ # Delegate all method calls we don't know about to target object
67
+ def method_missing(sym, *args, &block)
68
+ proxy_target.__send__(sym, *args, &block)
69
+ end
70
+ end
71
+ end
72
+ end