qoobaa-stree 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,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