s3 0.2.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
+ 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