paul-resourceful 0.2.3

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 (35) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/Manifest.txt +34 -0
  3. data/README.markdown +86 -0
  4. data/Rakefile +14 -0
  5. data/lib/resourceful.rb +29 -0
  6. data/lib/resourceful/authentication_manager.rb +107 -0
  7. data/lib/resourceful/cache_manager.rb +174 -0
  8. data/lib/resourceful/header.rb +31 -0
  9. data/lib/resourceful/http_accessor.rb +85 -0
  10. data/lib/resourceful/net_http_adapter.rb +60 -0
  11. data/lib/resourceful/options_interpreter.rb +78 -0
  12. data/lib/resourceful/request.rb +63 -0
  13. data/lib/resourceful/resource.rb +266 -0
  14. data/lib/resourceful/response.rb +175 -0
  15. data/lib/resourceful/stubbed_resource_proxy.rb +47 -0
  16. data/lib/resourceful/util.rb +6 -0
  17. data/lib/resourceful/version.rb +1 -0
  18. data/resourceful.gemspec +30 -0
  19. data/spec/acceptance_shared_specs.rb +49 -0
  20. data/spec/acceptance_spec.rb +408 -0
  21. data/spec/resourceful/authentication_manager_spec.rb +249 -0
  22. data/spec/resourceful/cache_manager_spec.rb +211 -0
  23. data/spec/resourceful/header_spec.rb +38 -0
  24. data/spec/resourceful/http_accessor_spec.rb +125 -0
  25. data/spec/resourceful/net_http_adapter_spec.rb +96 -0
  26. data/spec/resourceful/options_interpreter_spec.rb +94 -0
  27. data/spec/resourceful/request_spec.rb +186 -0
  28. data/spec/resourceful/resource_spec.rb +600 -0
  29. data/spec/resourceful/response_spec.rb +238 -0
  30. data/spec/resourceful/stubbed_resource_proxy_spec.rb +58 -0
  31. data/spec/simple_http_server_shared_spec.rb +160 -0
  32. data/spec/simple_http_server_shared_spec_spec.rb +212 -0
  33. data/spec/spec.opts +3 -0
  34. data/spec/spec_helper.rb +14 -0
  35. metadata +98 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2008 Absolute-Performance, Inc
2
+ Copyright (c) 2008 Peter Williams
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,34 @@
1
+ MIT-LICENSE
2
+ Manifest.txt
3
+ README.markdown
4
+ Rakefile
5
+ lib/resourceful.rb
6
+ lib/resourceful/authentication_manager.rb
7
+ lib/resourceful/cache_manager.rb
8
+ lib/resourceful/header.rb
9
+ lib/resourceful/http_accessor.rb
10
+ lib/resourceful/net_http_adapter.rb
11
+ lib/resourceful/options_interpreter.rb
12
+ lib/resourceful/request.rb
13
+ lib/resourceful/resource.rb
14
+ lib/resourceful/response.rb
15
+ lib/resourceful/stubbed_resource_proxy.rb
16
+ lib/resourceful/util.rb
17
+ lib/resourceful/version.rb
18
+ resourceful.gemspec
19
+ spec/acceptance_shared_specs.rb
20
+ spec/acceptance_spec.rb
21
+ spec/resourceful/authentication_manager_spec.rb
22
+ spec/resourceful/cache_manager_spec.rb
23
+ spec/resourceful/header_spec.rb
24
+ spec/resourceful/http_accessor_spec.rb
25
+ spec/resourceful/net_http_adapter_spec.rb
26
+ spec/resourceful/options_interpreter_spec.rb
27
+ spec/resourceful/request_spec.rb
28
+ spec/resourceful/resource_spec.rb
29
+ spec/resourceful/response_spec.rb
30
+ spec/resourceful/stubbed_resource_proxy_spec.rb
31
+ spec/simple_http_server_shared_spec.rb
32
+ spec/simple_http_server_shared_spec_spec.rb
33
+ spec/spec.opts
34
+ spec/spec_helper.rb
data/README.markdown ADDED
@@ -0,0 +1,86 @@
1
+ Resourceful
2
+ ===========
3
+
4
+ Resourceful provides a convenient Ruby API for making HTTP requests.
5
+
6
+ Features:
7
+
8
+ * GET, PUT, POST and DELETE HTTP requests
9
+ * HTTP Basic and Digest authentication
10
+ * HTTP Caching with pluggable backends
11
+ * Follow redirects based on the results of a callback
12
+
13
+ More Info
14
+ =========
15
+
16
+ * Source: [Github](http://github.com/paul/resourceful/tree/master)
17
+ * Bug Tracking: [Lighthouse](http://resourceful.lighthouseapp.com)
18
+ * Project Page: [Rubyforge](http://rubyforge.org/projects/resourceful/)
19
+ * Documentation: [API Docs](http://resourceful.rubyforge.org)
20
+
21
+ Examples
22
+ ========
23
+
24
+ Getting started
25
+ ---------------
26
+
27
+ gem install resourceful
28
+
29
+ Simplest example
30
+ ---------------
31
+
32
+ require 'resourceful'
33
+ http = Resourceful::HttpAccessor.new
34
+ resp = http.resource('http://rubyforge.org').get
35
+ puts resp.body
36
+
37
+ Get a page requiring HTTP Authentication
38
+ ----------------------------------------
39
+
40
+ basic_handler = Resourceful::BasicAuthenticator.new('My Realm', 'admin', 'secret')
41
+ http.auth_manager.add_auth_hander(basic_handler)
42
+ resp = http.resource('http://example.com/').get
43
+ puts resp.body
44
+
45
+ Redirection based on callback results
46
+ -------------------------------------
47
+
48
+ Resourceful will by default follow redirects on read requests (GET and HEAD), but not for
49
+ POST, etc. If you want to follow a redirect after a post, you will need to set the resource#on_redirect
50
+ callback. If the callback evaluates to true, it will follow the redirect.
51
+
52
+ resource = http.resource('http://example.com/redirect_me')
53
+ resource.on_redirect { |req, resp| resp.header['Location'] =~ /example.com/ }
54
+ resource.get # Will only follow the redirect if the new location is example.com
55
+
56
+
57
+
58
+ Post a URL encoded form
59
+ -----------------------
60
+
61
+ require 'resourceful'
62
+ http = Resourceful::HttpAccessor.new
63
+ resp = http.resource('http://mysite.example/service').
64
+ post('hostname=test&level=super', :content_type => 'application/x-www-form-urlencoded')
65
+
66
+ Put an XML document
67
+ -------------------
68
+
69
+ require 'resourceful'
70
+ http = Resourceful::HttpAccessor.new
71
+ resp = http.resource('http://mysite.example/service').
72
+ put('<?xml version="1.0"?><test/>', :content_type => 'application/xml')
73
+
74
+ Delete a resource
75
+ -----------------
76
+
77
+ require 'resourceful'
78
+ http = Resourceful::HttpAccessor.new
79
+ resp = http.resource('http://mysite.example/service').delete
80
+
81
+
82
+ Copyright
83
+ ---------
84
+
85
+ Copyright (c) 2008 Absolute Performance, Inc, Peter Williams; released under the MIT License.
86
+
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/resourceful.rb'
6
+
7
+ Hoe.new('resourceful', Resourceful::VERSION) do |p|
8
+ p.rubyforge_name = 'resourceful' # if different than lowercase project name
9
+ p.developer('Paul Sadauskas', 'psadauskas@gmail.com')
10
+ p.developer('Peter Williams', 'pezra@barelyenough.org')
11
+ p.summary = "An HTTP library for Ruby that takes advantage of everything HTTP has to offer."
12
+ end
13
+
14
+ # vim: syntax=Ruby
@@ -0,0 +1,29 @@
1
+ require 'resourceful/http_accessor'
2
+
3
+ require 'resourceful/util'
4
+
5
+ # Resourceful is a library that provides a high level HTTP interface.
6
+ module Resourceful
7
+ VERSION = "0.2.3"
8
+
9
+ HOP_BY_HOP_HEADERS = %w{
10
+ Connection
11
+ Keep-Alive
12
+ Proxy-Authenticate
13
+ Proxy-Authorization
14
+ TE
15
+ Trailers
16
+ Transfer-Encoding
17
+ Upgrade
18
+ }
19
+
20
+ NON_MODIFIABLE_HEADERS = %w{
21
+ Content-Location
22
+ Content-MD5
23
+ ETag
24
+ Last-Modified
25
+ Expires
26
+ }
27
+
28
+
29
+ end
@@ -0,0 +1,107 @@
1
+ require 'rubygems'
2
+ require 'httpauth'
3
+ require 'addressable/uri'
4
+
5
+ module Resourceful
6
+
7
+ class AuthenticationManager
8
+ def initialize
9
+ @authenticators = []
10
+ end
11
+
12
+ def add_auth_handler(authenticator)
13
+ @authenticators << authenticator
14
+ end
15
+
16
+ def associate_auth_info(challenge)
17
+ @authenticators.each do |authenticator|
18
+ authenticator.update_credentials(challenge) if authenticator.valid_for?(challenge)
19
+ end
20
+ end
21
+
22
+ def add_credentials(request)
23
+ authenticator = @authenticators.find { |authenticator| authenticator.can_handle?(request) }
24
+ authenticator.add_credentials_to(request) if authenticator
25
+ end
26
+
27
+ end
28
+
29
+ class BasicAuthenticator
30
+
31
+ def initialize(realm, username, password)
32
+ @realm, @username, @password = realm, username, password
33
+ @domain = nil
34
+ end
35
+
36
+ def valid_for?(challenge_response)
37
+ return false unless challenge_response.header['WWW-Authenticate']
38
+
39
+ !challenge_response.header['WWW-Authenticate'].grep(/^\s*basic/i).find do |a_challenge|
40
+ @realm.downcase == /realm="([^"]+)"/i.match(a_challenge)[1].downcase
41
+ end.nil?
42
+ end
43
+
44
+ def update_credentials(challenge)
45
+ @domain = Addressable::URI.parse(challenge.uri).host
46
+ end
47
+
48
+ def can_handle?(request)
49
+ Addressable::URI.parse(request.uri).host == @domain
50
+ end
51
+
52
+ def add_credentials_to(request)
53
+ request.header['Authorization'] = credentials
54
+ end
55
+
56
+ def credentials
57
+ HTTPAuth::Basic.pack_authorization(@username, @password)
58
+ end
59
+
60
+ end
61
+
62
+ class DigestAuthenticator
63
+
64
+ attr_reader :username, :password, :realm, :domain, :challenge
65
+
66
+ def initialize(realm, username, password)
67
+ @realm = realm
68
+ @username, @password = username, password
69
+ @domain = nil
70
+ end
71
+
72
+ def update_credentials(challenge_response)
73
+ @domain = Addressable::URI.parse(challenge_response.uri).host
74
+ @challenge = HTTPAuth::Digest::Challenge.from_header(challenge_response.header['WWW-Authenticate'].first)
75
+ end
76
+
77
+ def valid_for?(challenge_response)
78
+ return false unless challenge_header = challenge_response.header['WWW-Authenticate']
79
+ begin
80
+ challenge = HTTPAuth::Digest::Challenge.from_header(challenge_header.first)
81
+ rescue HTTPAuth::UnwellformedHeader
82
+ return false
83
+ end
84
+ challenge.realm == @realm
85
+ end
86
+
87
+ def can_handle?(request)
88
+ Addressable::URI.parse(request.uri).host == @domain
89
+ end
90
+
91
+ def add_credentials_to(request)
92
+ request.header['Authorization'] = credentials_for(request)
93
+ end
94
+
95
+ def credentials_for(request)
96
+ HTTPAuth::Digest::Credentials.from_challenge(@challenge,
97
+ :username => @username,
98
+ :password => @password,
99
+ :method => request.method.to_s.upcase,
100
+ :uri => Addressable::URI.parse(request.uri).path).to_header
101
+ end
102
+
103
+ end
104
+
105
+
106
+ end
107
+
@@ -0,0 +1,174 @@
1
+ require 'resourceful/header'
2
+
3
+ module Resourceful
4
+
5
+ class CacheManager
6
+ def initialize
7
+ raise NotImplementedError,
8
+ "Use one of CacheManager's child classes instead. Try NullCacheManager if you don't want any caching at all."
9
+ end
10
+
11
+ # Search for a cached representation that can be used to fulfill the
12
+ # the given request. If none are found, returns nil.
13
+ #
14
+ # @param request<Resourceful::Request>
15
+ # The request to use for searching.
16
+ def lookup(request); end
17
+
18
+ # Store a response in the cache.
19
+ #
20
+ # This method is smart enough to not store responses that cannot be
21
+ # cached (Vary: * or Cache-Control: no-cache, private, ...)
22
+ #
23
+ # @param request<Resourceful::Request>
24
+ # The request used to obtain the response. This is needed so the
25
+ # values from the response's Vary header can be stored.
26
+ # @param response<Resourceful::Response>
27
+ # The response to be stored.
28
+ def store(request, response); end
29
+
30
+ # Invalidates a all cached entries for a uri.
31
+ #
32
+ # This is used, for example, to invalidate the cache for a resource
33
+ # that gets POSTed to.
34
+ #
35
+ # @param uri<String>
36
+ # The uri of the resource to be invalidated
37
+ def invalidate(uri); end
38
+
39
+ # Selects the headers from the request named by the response's Vary header
40
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.6
41
+ #
42
+ # @param request<Resourceful::Request>
43
+ # The request used to obtain the response.
44
+ # @param response<Resourceful::Response>
45
+ # The response obtained from the request.
46
+ def select_request_headers(request, response)
47
+ header = Resourceful::Header.new
48
+
49
+ response.header['Vary'].first.split(',').each do |name|
50
+ name.strip!
51
+ header[name] = request.header[name]
52
+ end if response.header['Vary']
53
+
54
+ header
55
+ end
56
+ end
57
+
58
+ # This is the default cache, and does not do any caching. All lookups
59
+ # result in nil, and all attempts to store a response are a no-op.
60
+ class NullCacheManager < CacheManager
61
+ def initialize; end
62
+
63
+ def lookup(request)
64
+ nil
65
+ end
66
+
67
+ def store(request, response); end
68
+ end
69
+
70
+ # This is a nieve implementation of caching. Unused entries are never
71
+ # removed, and this may eventually eat up all your memory and cause your
72
+ # machine to explode.
73
+ class InMemoryCacheManager < CacheManager
74
+
75
+ def initialize
76
+ @collection = Hash.new{ |h,k| h[k] = CacheEntryCollection.new}
77
+ end
78
+
79
+ def lookup(request)
80
+ entry = @collection[request.uri.to_s][request]
81
+ response = entry.response if entry
82
+ response.authoritative = false if response
83
+
84
+ response
85
+ end
86
+
87
+ def store(request, response)
88
+ return unless response.cachable?
89
+
90
+ entry = CacheEntry.new(request.request_time,
91
+ select_request_headers(request, response),
92
+ response)
93
+
94
+ @collection[request.uri.to_s][request] = entry
95
+ end
96
+
97
+ def invalidate(uri)
98
+ @collection.delete(uri)
99
+ end
100
+
101
+ # The collection of all cached entries for a single resource (uri).
102
+ class CacheEntryCollection
103
+ include Enumerable
104
+
105
+ def initialize
106
+ @entries = []
107
+ end
108
+
109
+ # Iterates over the entries. Needed for Enumerable
110
+ def each(&block)
111
+ @entries.each(&block)
112
+ end
113
+
114
+ # Looks of a Entry that could fullfil the request. Returns nil if none
115
+ # was found.
116
+ #
117
+ # @param request<Resourceful::Request>
118
+ # The request to use for the lookup.
119
+ def [](request)
120
+ self.each do |entry|
121
+ return entry if entry.valid_for?(request)
122
+ end
123
+ return nil
124
+ end
125
+
126
+ # Saves an entry into the collection. Replaces any existing ones that could
127
+ # be used with the updated response.
128
+ #
129
+ # @param request<Resourceful::Request>
130
+ # The request that was used to obtain the response
131
+ # @param cache_entry<CacheEntry>
132
+ # The cache_entry generated from response that was obtained.
133
+ def []=(request, cache_entry)
134
+ @entries.delete_if { |e| e.valid_for?(request) }
135
+ @entries.unshift cache_entry
136
+ end
137
+
138
+ end # class CacheEntryCollection
139
+
140
+ # Contains everything we need to know to build a response for a request using the
141
+ # stored request.
142
+ class CacheEntry
143
+ # request_vary_headers is a HttpHeader with keys from the
144
+ # Vary header of the response, plus the values from the matching
145
+ # fields in the request
146
+ attr_accessor :request_time, :request_vary_headers, :response
147
+
148
+ # @param request_time<Time>
149
+ # Client-generated timestamp for when the request was made
150
+ # @param request_vary_headers<Resourceful::HttpHeader>
151
+ # A HttpHeader constructed from the keys listed in the vary headers
152
+ # of the response, and values obtained from those headers in the request
153
+ # @param response<Resourceful::Response>
154
+ # The Response obhect to be stored.
155
+ def initialize(request_time, request_vary_headers, response)
156
+ @request_time, @request_vary_headers, @response = request_time, request_vary_headers, response
157
+ end
158
+
159
+ # Returns true if this entry may be used to fullfil the given request,
160
+ # according to the vary headers.
161
+ #
162
+ # @param request<Resourceful::Request>
163
+ # The request to do the lookup on.
164
+ def valid_for?(request)
165
+ @request_vary_headers.all? do |key, value|
166
+ request.header[key] == value
167
+ end
168
+ end
169
+
170
+ end # class CacheEntry
171
+
172
+ end # class InMemoryCacheManager
173
+
174
+ end