openlogic-resourceful 1.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.
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