s3 0.2.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
+ include Parser
6
+
7
+ attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug
8
+ alias :use_ssl? :use_ssl
9
+
10
+ # ==== Parameters:
11
+ # +options+:: Hash of options
12
+ #
13
+ # ==== Options:
14
+ # +access_key_id+:: access key id
15
+ # +secret_access_key+:: secret access key
16
+ # +use_ssl+:: optional, defaults to false
17
+ # +debug+:: optional, defaults to false
18
+ # +timeout+:: optional, for Net::HTTP
19
+ def initialize(options = {})
20
+ @access_key_id = options[:access_key_id]
21
+ @secret_access_key = options[:secret_access_key]
22
+ @use_ssl = options[:use_ssl] || false
23
+ @debug = options[:debug]
24
+ @timeout = options[:timeout]
25
+ end
26
+
27
+ # Makes request with given HTTP method, sets missing parameters,
28
+ # adds signature to request header and returns response object
29
+ # (Net::HTTPResponse)
30
+ #
31
+ # ==== Parameters:
32
+ # +method+:: HTTP Method symbol, can be :get, :put, :delete
33
+ # +options+:: hash of options
34
+ #
35
+ # ==== Options:
36
+ # +host+:: hostname to connecto to, optional, defaults to s3.amazonaws.com[s3.amazonaws.com]
37
+ # +path+:: path to send request to, required, throws ArgumentError if not given
38
+ # +body+:: request body, only meaningful for :put request
39
+ # +params+:: parameters to add to query string for request, can be String or Hash
40
+ # +headers+:: Hash of headers fields to add to request header
41
+ #
42
+ # ==== Returns:
43
+ # Net::HTTPResponse object -- response from remote server
44
+ def request(method, options)
45
+ host = options[:host] || HOST
46
+ path = options[:path] or raise ArgumentError.new("no path given")
47
+ body = options[:body]
48
+ params = options[:params]
49
+ headers = options[:headers]
50
+
51
+ if params
52
+ params = params.is_a?(String) ? params : self.class.parse_params(params)
53
+ path << "?#{params}"
54
+ end
55
+
56
+ path = URI.escape(path)
57
+ request = request_class(method).new(path)
58
+
59
+ headers = self.class.parse_headers(headers)
60
+ headers.each do |key, value|
61
+ request[key] = value
62
+ end
63
+
64
+ request.body = body
65
+
66
+ send_request(host, request)
67
+ end
68
+
69
+ # Helper function to parser parameters and create single string of params
70
+ # added to questy string
71
+ #
72
+ # ==== Parameters:
73
+ # +params+: Hash of parameters if form <tt>key => value|nil</tt>
74
+ #
75
+ # ==== Returns:
76
+ # String -- containing all parameters joined in one params string,
77
+ # i.e. <tt>param1=val&param2&param3=0</tt>
78
+ def self.parse_params(params)
79
+ interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location]
80
+
81
+ result = []
82
+ params.each do |key, value|
83
+ if interesting_keys.include?(key)
84
+ parsed_key = key.to_s.gsub("_", "-")
85
+ case value
86
+ when nil
87
+ result << parsed_key
88
+ else
89
+ result << "#{parsed_key}=#{value}"
90
+ end
91
+ end
92
+ end
93
+ result.join("&")
94
+ end
95
+
96
+ # Helper function to change headers from symbols, to in correct
97
+ # form (i.e. with '-' instead of '_')
98
+ #
99
+ # ==== Parameters:
100
+ # +headers+:: Hash of pairs <tt>headername => value</tt>,
101
+ # where value can be Range (for Range header) or any other
102
+ # value which can be translated to string
103
+ #
104
+ # ==== Returns:
105
+ # Hash of headers translated from symbol to string,
106
+ # containing only interesting headers
107
+ def self.parse_headers(headers)
108
+ interesting_keys = [:content_type, :x_amz_acl, :range,
109
+ :if_modified_since, :if_unmodified_since,
110
+ :if_match, :if_none_match,
111
+ :content_disposition, :content_encoding,
112
+ :x_amz_copy_source, :x_amz_metadata_directive,
113
+ :x_amz_copy_source_if_match,
114
+ :x_amz_copy_source_if_none_match,
115
+ :x_amz_copy_source_if_unmodified_since,
116
+ :x_amz_copy_source_if_modified_since]
117
+
118
+ parsed_headers = {}
119
+ if headers
120
+ headers.each do |key, value|
121
+ if interesting_keys.include?(key)
122
+ parsed_key = key.to_s.gsub("_", "-")
123
+ parsed_value = value
124
+ case value
125
+ when Range
126
+ parsed_value = "bytes=#{value.first}-#{value.last}"
127
+ end
128
+ parsed_headers[parsed_key] = parsed_value
129
+ end
130
+ end
131
+ end
132
+ parsed_headers
133
+ end
134
+
135
+ private
136
+
137
+ def request_class(method)
138
+ case method
139
+ when :get
140
+ request_class = Net::HTTP::Get
141
+ when :put
142
+ request_class = Net::HTTP::Put
143
+ when :delete
144
+ request_class = Net::HTTP::Delete
145
+ end
146
+ end
147
+
148
+ def port
149
+ use_ssl ? 443 : 80
150
+ end
151
+
152
+ def http(host)
153
+ http = Net::HTTP.new(host, port)
154
+ http.set_debug_output(STDOUT) if @debug
155
+ http.use_ssl = @use_ssl
156
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
157
+ http.read_timeout = @timeout if @timeout
158
+ http
159
+ end
160
+
161
+ def send_request(host, request)
162
+ response = http(host).start do |http|
163
+ host = http.address
164
+
165
+ request['Date'] ||= Time.now.httpdate
166
+
167
+ if request.body
168
+ request["Content-Type"] ||= "application/octet-stream"
169
+ request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp
170
+ end
171
+
172
+ request["Authorization"] = Signature.generate(:host => host,
173
+ :request => request,
174
+ :access_key_id => access_key_id,
175
+ :secret_access_key => secret_access_key)
176
+ http.request(request)
177
+ end
178
+
179
+ handle_response(response)
180
+ end
181
+
182
+ def handle_response(response)
183
+ case response.code.to_i
184
+ when 200...300
185
+ response
186
+ when 300...600
187
+ if response.body.nil? || response.body.empty?
188
+ raise Error::ResponseError.new(nil, response)
189
+ else
190
+ code, message = parse_error(response.body)
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,210 @@
1
+ module Stree
2
+
3
+ # Class responsible for handling objects stored in S3 buckets
4
+ class Object
5
+ include Parser
6
+ extend Forwardable
7
+
8
+ attr_accessor :content_type, :content_disposition, :content_encoding
9
+ attr_reader :last_modified, :etag, :size, :bucket, :key, :acl
10
+ attr_writer :content
11
+
12
+ def_instance_delegators :bucket, :name, :service, :bucket_request, :vhost?, :host, :path_prefix
13
+ def_instance_delegators :service, :protocol, :port
14
+ private_class_method :new
15
+
16
+ # Compares the object with other object. Returns true if the key
17
+ # of the objects are the same, and both have the same buckets (see
18
+ # bucket equality)
19
+ def ==(other)
20
+ self.key == other.key and self.bucket == other.bucket
21
+ end
22
+
23
+ # Returns full key of the object: e.g. +bucket-name/object/key.ext+
24
+ def full_key
25
+ [name, key].join("/")
26
+ end
27
+
28
+ # Assigns a new +key+ to the object, raises ArgumentError if given
29
+ # key is not valid key name
30
+ def key=(key)
31
+ raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
32
+ @key ||= key
33
+ end
34
+
35
+ # Assigns a new ACL to the object. Please note that ACL is not
36
+ # retrieved from the server and set to "public-read" by default.
37
+ # ==== Example
38
+ # object.acl = :public_read
39
+ def acl=(acl)
40
+ @acl = acl.to_s.gsub("_", "-") if acl
41
+ end
42
+
43
+ # Retrieves the object from the server. Method is used to download
44
+ # object information only (content-type, size and so on). It does
45
+ # NOT download the content of the object (use the content method
46
+ # to do it).
47
+ def retrieve
48
+ get_object(:headers => { :range => 0..0 })
49
+ self
50
+ end
51
+
52
+ # Retrieves the object from the server, returns true if the
53
+ # object exists or false otherwise. Uses retrieve method, but
54
+ # catches NoSuchKey exception and returns false when it happens
55
+ def exists?
56
+ retrieve
57
+ true
58
+ rescue Error::NoSuchKey
59
+ false
60
+ end
61
+
62
+ # Download the content of the object, and caches it. Pass true
63
+ # to clear the cache and download the object again.
64
+ def content(reload = false)
65
+ if reload or @content.nil?
66
+ get_object
67
+ end
68
+ @content
69
+ end
70
+
71
+ # Saves the object, returns true if successfull.
72
+ def save
73
+ put_object
74
+ true
75
+ end
76
+
77
+ # Copies the file to another key and/or bucket.
78
+ # ==== Options:
79
+ # +key+:: new key to store object in
80
+ # +bucket+:: new bucket to store object in (instance of Stree::Bucket)
81
+ # +acl+:: acl of the copied object (default: "public-read")
82
+ # +content_type+:: content type of the copied object (default: "application/octet-stream")
83
+ def copy(options = {})
84
+ copy_object(options)
85
+ end
86
+
87
+ # Destroys the file on the server
88
+ def destroy
89
+ delete_object
90
+ true
91
+ end
92
+
93
+ # Returns Object's URL using protocol specified in Service,
94
+ # e.g. http://domain.com.s3.amazonaws.com/key/with/path.extension
95
+ def url
96
+ URI.escape("#{protocol}#{host}/#{path_prefix}#{key}")
97
+ end
98
+
99
+ # Returns Object's CNAME URL (without s3.amazonaws.com suffix)
100
+ # using protocol specified in Service,
101
+ # e.g. http://domain.com/key/with/path.extension. (you have to set
102
+ # the CNAME in your DNS before you use the CNAME URL schema).
103
+ def cname_url
104
+ URI.escape("#{protocol}#{name}/#{key}") if bucket.vhost?
105
+ end
106
+
107
+ def inspect #:nodoc:
108
+ "#<#{self.class}:/#{name}/#{key}>"
109
+ end
110
+
111
+ private
112
+
113
+ attr_writer :last_modified, :etag, :size, :original_key, :bucket
114
+
115
+ def copy_object(options = {})
116
+ key = options[:key] or raise ArgumentError, "No key given"
117
+ raise ArgumentError.new("Invalid key name: #{key}") unless key_valid?(key)
118
+ bucket = options[:bucket] || self.bucket
119
+
120
+ headers = {}
121
+
122
+ headers[:x_amz_acl] = options[:acl] || acl || "public-read"
123
+ headers[:content_type] = options[:content_type] || content_type || "application/octet-stream"
124
+ headers[:content_encoding] = options[:content_encoding] if options[:content_encoding]
125
+ headers[:content_disposition] = options[:content_disposition] if options[:content_disposition]
126
+ headers[:x_amz_copy_source] = full_key
127
+ headers[:x_amz_metadata_directive] = "REPLACE"
128
+ headers[:x_amz_copy_source_if_match] = options[:if_match] if options[:if_match]
129
+ headers[:x_amz_copy_source_if_none_match] = options[:if_none_match] if options[:if_none_match]
130
+ headers[:x_amz_copy_source_if_unmodified_since] = options[:if_modified_since] if options[:if_modified_since]
131
+ headers[:x_amz_copy_source_if_modified_since] = options[:if_unmodified_since] if options[:if_unmodified_since]
132
+
133
+ response = bucket.send(:bucket_request, :put, :path => key, :headers => headers)
134
+ object_attributes = parse_copy_object_result(response.body)
135
+
136
+ object = Object.send(:new, bucket, object_attributes.merge(:key => key, :size => size))
137
+ object.acl = response[:x_amz_acl]
138
+ object.content_type = response[:content_type]
139
+ object.content_encoding = response[:content_encoding]
140
+ object.content_disposition = response[:content_disposition]
141
+ object
142
+ end
143
+
144
+ def get_object(options = {})
145
+ response = object_request(:get, options)
146
+ parse_headers(response)
147
+ end
148
+
149
+ def put_object
150
+ body = content.is_a?(IO) ? content.read : content
151
+ response = object_request(:put, :body => body, :headers => dump_headers)
152
+ parse_headers(response)
153
+ end
154
+
155
+ def delete_object(options = {})
156
+ object_request(:delete)
157
+ end
158
+
159
+ def initialize(bucket, options = {})
160
+ self.bucket = bucket
161
+ self.key = options[:key]
162
+ self.last_modified = options[:last_modified]
163
+ self.etag = options[:etag]
164
+ self.size = options[:size]
165
+ end
166
+
167
+ def object_request(method, options = {})
168
+ bucket_request(method, options.merge(:path => key))
169
+ end
170
+
171
+ def last_modified=(last_modified)
172
+ @last_modified = Time.parse(last_modified) if last_modified
173
+ end
174
+
175
+ def etag=(etag)
176
+ @etag = etag[1..-2] if etag
177
+ end
178
+
179
+ def key_valid?(key)
180
+ if (key.nil? or key.empty? or key =~ %r#//#)
181
+ false
182
+ else
183
+ true
184
+ end
185
+ end
186
+
187
+ def dump_headers
188
+ headers = {}
189
+ headers[:x_amz_acl] = @acl || "public-read"
190
+ headers[:content_type] = @content_type || "application/octet-stream"
191
+ headers[:content_encoding] = @content_encoding if @content_encoding
192
+ headers[:content_disposition] = @content_disposition if @content_disposition
193
+ headers
194
+ end
195
+
196
+ def parse_headers(response)
197
+ self.etag = response["etag"]
198
+ self.content_type = response["content-type"]
199
+ self.content_disposition = response["content-disposition"]
200
+ self.content_encoding = response["content-encoding"]
201
+ self.last_modified = response["last-modified"]
202
+ if response["content-range"]
203
+ self.size = response["content-range"].sub(/[^\/]+\//, "").to_i
204
+ else
205
+ self.size = response["content-length"]
206
+ self.content = response.body
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,48 @@
1
+ module Stree
2
+ module Parser
3
+ include REXML
4
+
5
+ def rexml_document(xml)
6
+ xml.force_encoding(Encoding::UTF_8) if xml.respond_to? :force_encoding
7
+ Document.new(xml)
8
+ end
9
+
10
+ def parse_list_all_my_buckets_result(xml)
11
+ names = []
12
+ rexml_document(xml).elements.each("ListAllMyBucketsResult/Buckets/Bucket/Name") { |e| names << e.text }
13
+ names
14
+ end
15
+
16
+ def parse_location_constraint(xml)
17
+ rexml_document(xml).elements["LocationConstraint"].text
18
+ end
19
+
20
+ def parse_list_bucket_result(xml)
21
+ objects_attributes = []
22
+ rexml_document(xml).elements.each("ListBucketResult/Contents") do |e|
23
+ object_attributes = {}
24
+ object_attributes[:key] = e.elements["Key"].text
25
+ object_attributes[:etag] = e.elements["ETag"].text
26
+ object_attributes[:last_modified] = e.elements["LastModified"].text
27
+ object_attributes[:size] = e.elements["Size"].text
28
+ objects_attributes << object_attributes
29
+ end
30
+ objects_attributes
31
+ end
32
+
33
+ def parse_copy_object_result(xml)
34
+ object_attributes = {}
35
+ document = rexml_document(xml)
36
+ object_attributes[:etag] = document.elements["CopyObjectResult/ETag"].text
37
+ object_attributes[:last_modified] = document.elements["CopyObjectResult/LastModified"].text
38
+ object_attributes
39
+ end
40
+
41
+ def parse_error(xml)
42
+ document = rexml_document(xml)
43
+ code = document.elements["Error/Code"].text
44
+ message = document.elements["Error/Message"].text
45
+ [code, message]
46
+ end
47
+ end
48
+ 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