openlogic-resourceful 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/History.txt +45 -0
  2. data/MIT-LICENSE +21 -0
  3. data/Manifest +46 -0
  4. data/README.markdown +92 -0
  5. data/Rakefile +91 -0
  6. data/lib/resourceful.rb +27 -0
  7. data/lib/resourceful/abstract_form_data.rb +30 -0
  8. data/lib/resourceful/authentication_manager.rb +107 -0
  9. data/lib/resourceful/cache_manager.rb +242 -0
  10. data/lib/resourceful/exceptions.rb +34 -0
  11. data/lib/resourceful/header.rb +355 -0
  12. data/lib/resourceful/http_accessor.rb +103 -0
  13. data/lib/resourceful/memcache_cache_manager.rb +75 -0
  14. data/lib/resourceful/multipart_form_data.rb +46 -0
  15. data/lib/resourceful/net_http_adapter.rb +84 -0
  16. data/lib/resourceful/promiscuous_basic_authenticator.rb +18 -0
  17. data/lib/resourceful/request.rb +235 -0
  18. data/lib/resourceful/resource.rb +179 -0
  19. data/lib/resourceful/response.rb +221 -0
  20. data/lib/resourceful/simple.rb +36 -0
  21. data/lib/resourceful/stubbed_resource_proxy.rb +47 -0
  22. data/lib/resourceful/urlencoded_form_data.rb +19 -0
  23. data/lib/resourceful/util.rb +6 -0
  24. data/openlogic-resourceful.gemspec +51 -0
  25. data/resourceful.gemspec +51 -0
  26. data/spec/acceptance/authorization_spec.rb +16 -0
  27. data/spec/acceptance/caching_spec.rb +190 -0
  28. data/spec/acceptance/header_spec.rb +24 -0
  29. data/spec/acceptance/redirecting_spec.rb +12 -0
  30. data/spec/acceptance/resource_spec.rb +84 -0
  31. data/spec/acceptance/resourceful_spec.rb +56 -0
  32. data/spec/acceptance_shared_specs.rb +44 -0
  33. data/spec/caching_spec.rb +89 -0
  34. data/spec/old_acceptance_specs.rb +378 -0
  35. data/spec/resourceful/header_spec.rb +153 -0
  36. data/spec/resourceful/http_accessor_spec.rb +56 -0
  37. data/spec/resourceful/multipart_form_data_spec.rb +84 -0
  38. data/spec/resourceful/promiscuous_basic_authenticator_spec.rb +30 -0
  39. data/spec/resourceful/resource_spec.rb +20 -0
  40. data/spec/resourceful/response_spec.rb +51 -0
  41. data/spec/resourceful/urlencoded_form_data_spec.rb +64 -0
  42. data/spec/resourceful_spec.rb +79 -0
  43. data/spec/simple_sinatra_server.rb +74 -0
  44. data/spec/simple_sinatra_server_spec.rb +98 -0
  45. data/spec/spec.opts +3 -0
  46. data/spec/spec_helper.rb +31 -0
  47. metadata +192 -0
@@ -0,0 +1,179 @@
1
+ require 'pathname'
2
+ require 'resourceful/request'
3
+
4
+ module Resourceful
5
+
6
+ class Resource
7
+ attr_reader :accessor
8
+
9
+ # Build a new resource for a uri
10
+ #
11
+ # @param accessor<HttpAccessor>
12
+ # The parent http accessor
13
+ # @param uri<String, Addressable::URI>
14
+ # The uri for the location of the resource
15
+ def initialize(accessor, uri, default_header = {})
16
+ @accessor, @uris = accessor, [uri]
17
+ @default_header = Resourceful::Header.new({'User-Agent' => Resourceful::RESOURCEFUL_USER_AGENT_TOKEN}.merge(default_header))
18
+ @on_redirect = nil
19
+ end
20
+
21
+ # The uri used to identify this resource. This is almost always the uri
22
+ # used to create the resource, but in the case of a permanent redirect, this
23
+ # will always reflect the lastest uri.
24
+ #
25
+ # @return Addressable::URI
26
+ # The current uri of the resource
27
+ def effective_uri
28
+ @uris.first
29
+ end
30
+ alias uri effective_uri
31
+
32
+ def default_header(temp_defaults = {})
33
+ @default_header.merge(temp_defaults)
34
+ end
35
+
36
+ # Returns the host for this Resource's current uri
37
+ def host
38
+ Addressable::URI.parse(uri).host
39
+ end
40
+
41
+ # Updates the effective uri after following a permanent redirect
42
+ def update_uri(uri)
43
+ @uris.unshift(uri)
44
+ end
45
+
46
+ # When performing a redirect, this callback will be executed first. If the callback
47
+ # returns true, then the redirect is followed, otherwise it is not. The request that
48
+ # triggered the redirect and the response will be passed into the block. This can be
49
+ # used to update any links on the client side.
50
+ #
51
+ # Example:
52
+ #
53
+ # author_resource.on_redirect do |req, resp|
54
+ # post.author_uri = resp.header['Location']
55
+ # end
56
+ #
57
+ # @yieldparam callback<request, response>
58
+ # The action to be executed when a request results in a redirect. Yields the
59
+ # current request and result objects to the callback.
60
+ #
61
+ # @raise ArgumentError if called without a block
62
+ def on_redirect(&callback)
63
+ if block_given?
64
+ @on_redirect = callback
65
+ else
66
+ @on_redirect
67
+ end
68
+ end
69
+
70
+ # Performs a GET on the resource, following redirects as neccessary, and retriving
71
+ # it from the local cache if its available and valid.
72
+ #
73
+ # @return [Response] The Response to the final request made.
74
+ #
75
+ # @raise [UnsuccessfulHttpRequestError] unless the request is a
76
+ # success, ie the final request returned a 2xx response code
77
+ def get(header = {})
78
+ request(:get, nil, header)
79
+ end
80
+
81
+ def head(header = {})
82
+ request(:head, nil, header)
83
+ end
84
+ # :call-seq:
85
+ # post(data = "", :content_type => mime_type)
86
+ #
87
+ # Performs a POST with the given data to the resource, following redirects as
88
+ # neccessary.
89
+ #
90
+ # @param [String] data
91
+ # The body of the data to be posted
92
+ # @param [Hash] options
93
+ # Options to pass into the request header. At the least, :content_type is required.
94
+ #
95
+ # @return [Response] The Response to the final request that was made.
96
+ #
97
+ # @raise [ArgumentError] unless :content-type is specified in options
98
+ # @raise [UnsuccessfulHttpRequestError] unless the request is a
99
+ # success, ie the final request returned a 2xx response code
100
+ def post(data = nil, header = {})
101
+ request(:post, data, header)
102
+ end
103
+
104
+ # :call-seq:
105
+ # put(data = "", :content_type => mime_type)
106
+ #
107
+ # Performs a PUT with the given data to the resource, following redirects as
108
+ # neccessary.
109
+ #
110
+ # @param [String] data
111
+ # The body of the data to be posted
112
+ # @param [Hash] options
113
+ # Options to pass into the request header. At the least, :content_type is required.
114
+ #
115
+ # @return [Response] The response to the final request made.
116
+ #
117
+ # @raise [ArgumentError] unless :content-type is specified in options
118
+ # @raise [UnsuccessfulHttpRequestError] unless the request is a
119
+ # success, ie the final request returned a 2xx response code
120
+ def put(data, header = {})
121
+ request(:put, data, header)
122
+ end
123
+
124
+ # Performs a DELETE on the resource, following redirects as neccessary.
125
+ #
126
+ # @return <Response>
127
+ #
128
+ # @raise [UnsuccessfulHttpRequestError] unless the request is a
129
+ # success, ie the final request returned a 2xx response code
130
+ def delete(header = {})
131
+ request(:delete, nil, header)
132
+ end
133
+
134
+ def logger
135
+ accessor.logger
136
+ end
137
+
138
+ # Actually make the request
139
+ def request(method, data, header)
140
+ header = default_header.merge(header)
141
+ ensure_content_type(data, header) if data
142
+
143
+ data = StringIO.new(data) if data.kind_of?(String)
144
+
145
+ log_request_with_time "#{method.to_s.upcase} [#{uri}]" do
146
+ request = Request.new(method, self, data, header)
147
+ request.fetch_response
148
+ end
149
+ end
150
+
151
+ private
152
+
153
+ # Ensures that the request has a content type header
154
+ def ensure_content_type(body, header)
155
+ return if header.has_key?('Content-Type')
156
+
157
+ if body.respond_to?(:content_type)
158
+ header['Content-Type'] = body.content_type
159
+ return
160
+ end
161
+
162
+ return if default_header.has_key?('Content-Type')
163
+
164
+ # could not figure it out
165
+ raise MissingContentType
166
+ end
167
+
168
+ # Log it took the time to make the request
169
+ def log_request_with_time(msg, indent = 2)
170
+ logger.info(" " * indent + msg)
171
+ result = nil
172
+ time = Benchmark.measure { result = yield }
173
+ logger.info(" " * indent + "-> Returned #{result.code} in %.4fs" % time.real)
174
+ result
175
+ end
176
+
177
+ end
178
+
179
+ end
@@ -0,0 +1,221 @@
1
+ require 'net/http'
2
+ require 'time'
3
+ require 'zlib'
4
+
5
+ module Resourceful
6
+
7
+ class Response
8
+ REDIRECT_RESPONSE_CODES = [301,302,303,307]
9
+ NORMALLY_CACHEABLE_RESPONSE_CODES = [200, 203, 300, 301, 410]
10
+
11
+ attr_reader :uri, :code, :header, :body, :response_time
12
+ alias headers header
13
+
14
+ attr_accessor :authoritative, :request_time
15
+ alias authoritative? authoritative
16
+
17
+ def initialize(uri, code, header, body)
18
+ @uri, @code, @header, @body = uri, code, header, body
19
+ @response_time = Time.now
20
+ end
21
+
22
+ # Is this a cached response that has expired?
23
+ #
24
+ # @return true|false
25
+ def expired?
26
+ if header.cache_control and m_age_str = header.cache_control.find{|cc| /^max-age=/ === cc}
27
+ return current_age > m_age_str[/\d+/].to_i
28
+ elsif header.expires
29
+ return Time.httpdate(header.expires) < Time.now
30
+ end
31
+
32
+ false
33
+ end
34
+
35
+ # Is this a cached response that is stale?
36
+ #
37
+ # @return true|false
38
+ def stale?
39
+ return true if expired?
40
+ return false unless header.has_field?(Header::CACHE_CONTROL)
41
+ return true if header.cache_control.any?{|cc| /must-revalidate|no-cache/ === cc}
42
+
43
+ false
44
+ end
45
+
46
+ # Is this response cachable?
47
+ #
48
+ # @return true|false
49
+ def cacheable?
50
+ @cacheable ||= begin
51
+ @cacheable = true if NORMALLY_CACHEABLE_RESPONSE_CODES.include?(code.to_i)
52
+ @cacheable = false if header.vary && header.vary.include?('*')
53
+ @cacheable = false if header.cache_control && header.cache_control.include?('no-cache')
54
+ @cacheable = true if header.cache_control && header.cache_control.include?('public')
55
+ @cacheable = true if header.cache_control && header.cache_control.include?('private')
56
+ @cacheable || false
57
+ end
58
+ end
59
+
60
+ # Does this response force revalidation?
61
+ def must_be_revalidated?
62
+ header.cache_control && header.cache_control.include?('must-revalidate')
63
+ end
64
+
65
+ # Update our headers from a later 304 response
66
+ def revalidate!(not_modified_response)
67
+ header.merge!(not_modified_response.header)
68
+ @request_time = not_modified_response.request_time
69
+ @response_time = not_modified_response.response_time
70
+ @authoritative = true
71
+ end
72
+
73
+ # Algorithm taken from RCF2616#13.2.3
74
+ def current_age
75
+ age_value = header.age.to_i
76
+ date_value = Time.httpdate(header.date)
77
+ now = Time.now
78
+
79
+ apparent_age = [0, response_time - date_value].max
80
+ corrected_received_age = [apparent_age, age_value].max
81
+ current_age = corrected_received_age + (response_time - request_time) + (now - response_time)
82
+ end
83
+
84
+ def body
85
+ raise(NotImplementedError, 'Chained encodings are not supported') if header['Content-Encoding'] && header['Content-Encoding'].length > 1
86
+ encoding = header['Content-Encoding'] && header['Content-Encoding'].first
87
+
88
+ case encoding
89
+ when nil
90
+ # body is identity encoded; just return it
91
+ @body
92
+ when /^\s*gzip\s*$/i
93
+ gz_in = ::Zlib::GzipReader.new(StringIO.new(@body, 'r'))
94
+ @body = gz_in.read
95
+ gz_in.close
96
+ header.delete('Content-Encoding')
97
+ @body
98
+ else
99
+ raise UnsupportedContentCoding, "Resourceful does not support #{encoding} content coding"
100
+ end
101
+ end
102
+
103
+ CODE_NAMES = {
104
+ 100 => "Continue".freeze,
105
+ 101 => "Switching Protocols".freeze,
106
+
107
+ 200 => "OK".freeze,
108
+ 201 => "Created".freeze,
109
+ 202 => "Accepted".freeze,
110
+ 203 => "Non-Authoritative Information".freeze,
111
+ 204 => "No Content".freeze,
112
+ 205 => "Reset Content".freeze,
113
+ 206 => "Partial Content".freeze,
114
+
115
+ 300 => "Multiple Choices".freeze,
116
+ 301 => "Moved Permanently".freeze,
117
+ 302 => "Found".freeze,
118
+ 303 => "See Other".freeze,
119
+ 304 => "Not Modified".freeze,
120
+ 305 => "Use Proxy".freeze,
121
+ 307 => "Temporary Redirect".freeze,
122
+
123
+ 400 => "Bad Request".freeze,
124
+ 401 => "Unauthorized".freeze,
125
+ 402 => "Payment Required".freeze,
126
+ 403 => "Forbidden".freeze,
127
+ 404 => "Not Found".freeze,
128
+ 405 => "Method Not Allowed".freeze,
129
+ 406 => "Not Acceptable".freeze,
130
+ 407 => "Proxy Authentication Required".freeze,
131
+ 408 => "Request Timeout".freeze,
132
+ 409 => "Conflict".freeze,
133
+ 410 => "Gone".freeze,
134
+ 411 => "Length Required".freeze,
135
+ 412 => "Precondition Failed".freeze,
136
+ 413 => "Request Entity Too Large".freeze,
137
+ 414 => "Request-URI Too Long".freeze,
138
+ 415 => "Unsupported Media Type".freeze,
139
+ 416 => "Requested Range Not Satisfiable".freeze,
140
+ 417 => "Expectation Failed".freeze,
141
+
142
+ 500 => "Internal Server Error".freeze,
143
+ 501 => "Not Implemented".freeze,
144
+ 502 => "Bad Gateway".freeze,
145
+ 503 => "Service Unavailable".freeze,
146
+ 504 => "Gateway Timeout".freeze,
147
+ 505 => "HTTP Version Not Supported".freeze,
148
+ }.freeze
149
+
150
+ CODES = CODE_NAMES.keys
151
+
152
+ CODE_NAMES.each do |code, msg|
153
+ method_name = msg.downcase.gsub(/[- ]/, "_")
154
+
155
+ class_eval <<-RUBY
156
+ def #{method_name}? # def ok?
157
+ @code == #{code} # @code == 200
158
+ end # end
159
+ RUBY
160
+ end
161
+
162
+ # Is the response informational? True for
163
+ # 1xx series response codes
164
+ #
165
+ # @return true|false
166
+ def informational?
167
+ @code.in? 100..199
168
+ end
169
+
170
+ # Is the response code sucessful? True for only 2xx series
171
+ # response codes.
172
+ #
173
+ # @return true|false
174
+ def successful?
175
+ @code.in? 200..299
176
+ end
177
+ alias success? successful?
178
+
179
+ # Is the response a redirect? True for
180
+ # 3xx series response codes
181
+ #
182
+ # @return true|false
183
+ def redirection?
184
+ @code.in? 300..399
185
+ end
186
+
187
+ # Is the response a actual redirect? True for
188
+ # 301, 302, 303, 307 response codes
189
+ #
190
+ # @return true|false
191
+ def redirect?
192
+ @code.in? REDIRECT_RESPONSE_CODES
193
+ end
194
+
195
+ # Is the response the result of a client error? True for
196
+ # 4xx series response codes
197
+ #
198
+ # @return true|false
199
+ def client_error?
200
+ @code.in? 400..499
201
+ end
202
+
203
+ # Is the response the result of a server error? True for
204
+ # 5xx series response codes
205
+ #
206
+ # @return true|false
207
+ def server_error?
208
+ @code.in? 500..599
209
+ end
210
+
211
+ # Is the response the result of any kind of error? True for
212
+ # 4xx and 5xx series response codes
213
+ #
214
+ # @return true|false
215
+ def error?
216
+ server_error? || client_error?
217
+ end
218
+
219
+ end
220
+
221
+ end
@@ -0,0 +1,36 @@
1
+ module Resourceful
2
+ module Simple
3
+ def request(method, uri, header = {}, data = nil)
4
+ default_accessor.resource(uri).request(method, data, header)
5
+ end
6
+
7
+ def default_accessor
8
+ @default_accessor ||= Resourceful::HttpAccessor.new
9
+ end
10
+
11
+ def add_authenticator(an_authenticator)
12
+ default_accessor.add_authenticator(an_authenticator)
13
+ end
14
+
15
+ def get(uri, header = {})
16
+ request(:get, uri, header)
17
+ end
18
+
19
+ def head(uri, header = {})
20
+ request(:head, uri, header)
21
+ end
22
+
23
+ def delete(uri, header = {})
24
+ request(:delete, uri, header)
25
+ end
26
+
27
+ def post(uri, data = nil, header = {})
28
+ request(:post, uri, header, data)
29
+ end
30
+
31
+ def put(uri, data = nil, header = {})
32
+ request(:put, uri, header, data)
33
+ end
34
+ end
35
+
36
+ end
@@ -0,0 +1,47 @@
1
+ require 'resourceful/resource'
2
+
3
+ module Resourceful
4
+ class StubbedResourceProxy
5
+ def initialize(resource, canned_responses)
6
+ @resource = resource
7
+
8
+ @canned_responses = {}
9
+
10
+ canned_responses.each do |cr|
11
+ mime_type = cr[:mime_type]
12
+ @canned_responses[mime_type] = resp = Net::HTTPOK.new('1.1', '200', 'OK')
13
+ resp['content-type'] = mime_type.to_str
14
+ resp.instance_variable_set(:@read, true)
15
+ resp.instance_variable_set(:@body, cr[:body])
16
+
17
+ end
18
+ end
19
+
20
+ def get_body(*args)
21
+ get(*args).body
22
+ end
23
+
24
+ def get(*args)
25
+ options = args.last.is_a?(Hash) ? args.last : {}
26
+
27
+ if accept = [(options[:accept] || '*/*')].flatten.compact
28
+ accept.each do |mt|
29
+ return canned_response(mt) || next
30
+ end
31
+ @resource.get(*args)
32
+ end
33
+ end
34
+
35
+ def method_missing(method, *args)
36
+ @resource.send(method, *args)
37
+ end
38
+
39
+ protected
40
+
41
+ def canned_response(mime_type)
42
+ mime_type = @canned_responses.keys.first if mime_type == '*/*'
43
+ @canned_responses[mime_type]
44
+ end
45
+
46
+ end
47
+ end