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,242 @@
1
+ require 'resourceful/header'
2
+ require 'digest/md5'
3
+
4
+ module Resourceful
5
+
6
+ class AbstractCacheManager
7
+ def initialize
8
+ raise NotImplementedError,
9
+ "Use one of CacheManager's child classes instead. Try NullCacheManager if you don't want any caching at all."
10
+ end
11
+
12
+ # Finds a previously cached response to the provided request. The
13
+ # response returned may be stale.
14
+ #
15
+ # @param [Resourceful::Request] request
16
+ # The request for which we are looking for a response.
17
+ #
18
+ # @return [Resourceful::Response]
19
+ # A (possibly stale) response for the request provided.
20
+ def lookup(request); end
21
+
22
+ # Store a response in the cache.
23
+ #
24
+ # This method is smart enough to not store responses that cannot be
25
+ # cached (Vary: * or Cache-Control: no-cache, private, ...)
26
+ #
27
+ # @param request<Resourceful::Request>
28
+ # The request used to obtain the response. This is needed so the
29
+ # values from the response's Vary header can be stored.
30
+ # @param response<Resourceful::Response>
31
+ # The response to be stored.
32
+ def store(request, response); end
33
+
34
+ # Invalidates a all cached entries for a uri.
35
+ #
36
+ # This is used, for example, to invalidate the cache for a resource
37
+ # that gets POSTed to.
38
+ #
39
+ # @param uri<String>
40
+ # The uri of the resource to be invalidated
41
+ def invalidate(uri); end
42
+
43
+ protected
44
+
45
+ # Returns an alphanumeric hash of a URI
46
+ def uri_hash(uri)
47
+ Digest::MD5.hexdigest(uri)
48
+ end
49
+ end
50
+
51
+ # This is the default cache, and does not do any caching. All lookups
52
+ # result in nil, and all attempts to store a response are a no-op.
53
+ class NullCacheManager < AbstractCacheManager
54
+ def initialize; end
55
+
56
+ def lookup(request)
57
+ nil
58
+ end
59
+
60
+ def store(request, response); end
61
+ end
62
+
63
+ # This is a nieve implementation of caching. Unused entries are never
64
+ # removed, and this may eventually eat up all your memory and cause your
65
+ # machine to explode.
66
+ class InMemoryCacheManager < AbstractCacheManager
67
+
68
+ def initialize
69
+ @collection = Hash.new{ |h,k| h[k] = CacheEntryCollection.new}
70
+ end
71
+
72
+ def lookup(request)
73
+ response = @collection[request.uri.to_s][request]
74
+ response.authoritative = false if response
75
+ response
76
+ end
77
+
78
+ def store(request, response)
79
+ return unless response.cacheable?
80
+
81
+ @collection[request.uri.to_s][request] = response
82
+ end
83
+
84
+ def invalidate(uri)
85
+ @collection.delete(uri)
86
+ end
87
+ end # class InMemoryCacheManager
88
+
89
+ # Stores cache entries in a directory on the filesystem. Similarly to the
90
+ # InMemoryCacheManager there are no limits on storage, so this will eventually
91
+ # eat up all your disk!
92
+ class FileCacheManager < AbstractCacheManager
93
+ # Create a new FileCacheManager
94
+ #
95
+ # @param [String] location
96
+ # A directory on the filesystem to store cache entries. This directory
97
+ # will be created if it doesn't exist
98
+ def initialize(location="/tmp/resourceful")
99
+ require 'fileutils'
100
+ require 'yaml'
101
+ @dir = FileUtils.mkdir_p(location)
102
+ end
103
+
104
+ def lookup(request)
105
+ response = cache_entries_for(request)[request]
106
+ response.authoritative = false if response
107
+ response
108
+ end
109
+
110
+ def store(request, response)
111
+ return unless response.cacheable?
112
+
113
+ entries = cache_entries_for(request)
114
+ entries[request] = response
115
+ File.open(cache_file(request.uri), "w") {|fh| fh.write( YAML.dump(entries) ) }
116
+ end
117
+
118
+ def invalidate(uri);
119
+ File.unlink(cache_file(uri));
120
+ end
121
+
122
+ private
123
+
124
+ def cache_entries_for(request)
125
+ if File.readable?( cache_file(request.uri) )
126
+ YAML.load_file( cache_file(request.uri) )
127
+ else
128
+ Resourceful::CacheEntryCollection.new
129
+ end
130
+ end
131
+
132
+ def cache_file(uri)
133
+ "#{@dir}/#{uri_hash(uri)}"
134
+ end
135
+ end # class FileCacheManager
136
+
137
+
138
+ # The collection of cached entries. Nominally all the entry in a
139
+ # collection of this sort will be for the same resource but that is
140
+ # not required to be true.
141
+ class CacheEntryCollection
142
+ include Enumerable
143
+
144
+ def initialize
145
+ @entries = []
146
+ end
147
+
148
+ # Iterates over the entries. Needed for Enumerable
149
+ def each(&block)
150
+ @entries.each(&block)
151
+ end
152
+
153
+ # Looks for an Entry that could fullfil the request. Returns nil if none
154
+ # was found.
155
+ #
156
+ # @param [Resourceful::Request] request
157
+ # The request to use for the lookup.
158
+ #
159
+ # @return [Resourceful::Response]
160
+ # The cached response for the specified request if one is available.
161
+ def [](request)
162
+ entry = find { |entry| entry.valid_for?(request) }
163
+ entry.response if entry
164
+ end
165
+
166
+ # Saves an entry into the collection. Replaces any existing ones that could
167
+ # be used with the updated response.
168
+ #
169
+ # @param [Resourceful::Request] request
170
+ # The request that was used to obtain the response
171
+ # @param [Resourceful::Response] response
172
+ # The cache_entry generated from response that was obtained.
173
+ def []=(request, response)
174
+ @entries.delete_if { |e| e.valid_for?(request) }
175
+ @entries << CacheEntry.new(request, response)
176
+
177
+ response
178
+ end
179
+ end # class CacheEntryCollection
180
+
181
+ # Represents a previous request and cached response with enough
182
+ # detail to determine construct a cached response to a matching
183
+ # request in the future. It also understands what a matching
184
+ # request means.
185
+ class CacheEntry
186
+ # request_vary_headers is a HttpHeader with keys from the Vary
187
+ # header of the response, plus the values from the matching fields
188
+ # in the request
189
+ attr_reader :request_vary_headers
190
+
191
+ # The time at which the client believes the request was made.
192
+ attr_reader :request_time
193
+
194
+ # The URI of the request
195
+ attr_reader :request_uri
196
+
197
+ # The response to that we are caching
198
+ attr_reader :response
199
+
200
+ # @param [Resourceful::Request] request
201
+ # The request whose response we are storing in the cache.
202
+ # @param response<Resourceful::Response>
203
+ # The Response obhect to be stored.
204
+ def initialize(request, response)
205
+ @request_uri = request.uri
206
+ @request_time = request.request_time
207
+ @request_vary_headers = select_request_headers(request, response)
208
+ @response = response
209
+ end
210
+
211
+ # Returns true if this entry may be used to fullfil the given request,
212
+ # according to the vary headers.
213
+ #
214
+ # @param request<Resourceful::Request>
215
+ # The request to do the lookup on.
216
+ def valid_for?(request)
217
+ request.uri == @request_uri and
218
+ @request_vary_headers.all? {|key, value|
219
+ request.header[key] == value
220
+ }
221
+ end
222
+
223
+ # Selects the headers from the request named by the response's Vary header
224
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.6
225
+ #
226
+ # @param [Resourceful::Request] request
227
+ # The request used to obtain the response.
228
+ # @param [Resourceful::Response] response
229
+ # The response obtained from the request.
230
+ def select_request_headers(request, response)
231
+ header = Resourceful::Header.new
232
+
233
+ response.header['Vary'].each do |name|
234
+ header[name] = request.header[name] if request.header[name]
235
+ end if response.header['Vary']
236
+
237
+ header
238
+ end
239
+
240
+ end # class CacheEntry
241
+
242
+ end
@@ -0,0 +1,34 @@
1
+
2
+ module Resourceful
3
+
4
+ # This exception used to indicate that the request did not succeed.
5
+ # The HTTP response is included so that the appropriate actions can
6
+ # be taken based on the details of that response
7
+ class UnsuccessfulHttpRequestError < Exception
8
+ attr_reader :http_response, :http_request
9
+
10
+ # Initialize new error from the HTTP request and response attributes.
11
+ def initialize(http_request, http_response)
12
+ super("#{http_request.method} request to <#{http_request.uri}> failed with code #{http_response.code}")
13
+ @http_request = http_request
14
+ @http_response = http_response
15
+ end
16
+ end
17
+
18
+ class MalformedServerResponse < UnsuccessfulHttpRequestError
19
+ end
20
+
21
+
22
+ # Exception indicating that the server used a content coding scheme
23
+ # that Resourceful is unable to handle.
24
+ class UnsupportedContentCoding < Exception
25
+ end
26
+
27
+ # Raised when a body is supplied, but not a content-type header
28
+ class MissingContentType < ArgumentError
29
+ def initialize
30
+ super("A Content-Type must be specified when an entity-body is supplied.")
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,355 @@
1
+ require 'options'
2
+ require 'set'
3
+
4
+ # Represents the header fields of an HTTP message. To access a field
5
+ # you can use `#[]` and `#[]=`. For example, to get the content type
6
+ # of a response you can do
7
+ #
8
+ # response.header['Content-Type'] # => "application/xml"
9
+ #
10
+ # Lookups and modifications done in this way are case insensitive, so
11
+ # 'Content-Type', 'content-type' and :content_type are all equivalent.
12
+ #
13
+ # Multi-valued fields
14
+ # -------------------
15
+ #
16
+ # Multi-value fields (e.g. Accept) are always returned as an Array
17
+ # regardless of the number of values, if the field is present.
18
+ # Single-value fields (e.g. Content-Type) are always returned as
19
+ # strings. The multi/single valueness of a header field is determined
20
+ # by the way it is defined in the HTTP spec. Unknown fields are
21
+ # treated as multi-valued.
22
+ #
23
+ # (This behavior is new in 0.6 and may be slightly incompatible with
24
+ # the way previous versions worked in some situations.)
25
+ #
26
+ # For example
27
+ #
28
+ # h = Resourceful::Header.new
29
+ # h['Accept'] = "application/xml"
30
+ # h['Accept'] # => ["application/xml"]
31
+ #
32
+ module Resourceful
33
+ class Header
34
+ include Enumerable
35
+
36
+ def initialize(hash={})
37
+ @raw_fields = {}
38
+ hash.each { |k, v| self[k] = v }
39
+ end
40
+
41
+ def to_hash
42
+ @raw_fields.dup
43
+ end
44
+
45
+ def [](k)
46
+ field_def(k).get_from(@raw_fields)
47
+ end
48
+
49
+ def []=(k, v)
50
+ field_def(k).set_to(v, @raw_fields)
51
+ end
52
+
53
+ def delete(k)
54
+ field_def(k).delete(@raw_fields)
55
+ end
56
+
57
+ def has_key?(k)
58
+ field_def(k).exists_in?(@raw_fields)
59
+ end
60
+ alias has_field? has_key?
61
+
62
+ def each(&blk)
63
+ @raw_fields.each(&blk)
64
+ end
65
+
66
+ # Iterates through the fields with values provided as message
67
+ # ready strings.
68
+ def each_field(&blk)
69
+ each do |k,v|
70
+ str_v = if field_def(k).multivalued?
71
+ v.join(', ')
72
+ else
73
+ v.to_s
74
+ end
75
+
76
+ yield k, str_v
77
+ end
78
+ end
79
+
80
+ def merge!(another)
81
+ another.each do |k,v|
82
+ self[k] = v
83
+ end
84
+ self
85
+ end
86
+
87
+ def delete(k)
88
+ @raw_fields.delete(field_def(k).name)
89
+ end
90
+
91
+ def merge(another)
92
+ self.class.new(self).merge!(another)
93
+ end
94
+
95
+ def reverse_merge(another)
96
+ self.class.new(another).merge!(self)
97
+ end
98
+
99
+ def dup
100
+ self.class.new(@raw_fields.dup)
101
+ end
102
+
103
+ # Class to handle the details of each type of field.
104
+ class FieldDesc
105
+ include Comparable
106
+
107
+ ##
108
+ attr_reader :name
109
+
110
+ # Create a new header field descriptor.
111
+ #
112
+ # @param [String] name The canonical name of this field.
113
+ #
114
+ # @param [Hash] options hash containing extra information about
115
+ # this header fields. Valid keys are:
116
+ #
117
+ # `:multivalued`
118
+ # `:multivalue`
119
+ # `:repeatable`
120
+ # : Values of this field are comma separated list of values.
121
+ # (n#VALUE per HTTP spec.) Default: false
122
+ #
123
+ # `:hop_by_hop`
124
+ # : True if the header is a hop-by-hop header. Default: false
125
+ #
126
+ # `:modifiable`
127
+ # : False if the header should not be modified by intermediates or caches. Default: true
128
+ #
129
+ def initialize(name, options = {})
130
+ @name = name
131
+ options = Options.for(options).validate(:repeatable, :hop_by_hop, :modifiable)
132
+
133
+ @repeatable = options.getopt([:repeatable, :multivalue, :multivalued]) || false
134
+ @hop_by_hop = options.getopt(:hop_by_hop) || false
135
+ @modifiable = options.getopt(:modifiable, true)
136
+ end
137
+
138
+ def repeatable?
139
+ @repeatable
140
+ end
141
+ alias multivalued? repeatable?
142
+
143
+ def hop_by_hop?
144
+ @hop_by_hop
145
+ end
146
+
147
+ def modifiable?
148
+ @modifiable
149
+ end
150
+
151
+ def get_from(raw_fields_hash)
152
+ raw_fields_hash[name]
153
+ end
154
+
155
+ def set_to(value, raw_fields_hash)
156
+ raw_fields_hash[name] = if multivalued?
157
+ Array(value).map{|v| v.split(/,\s*/)}.flatten
158
+ elsif value.kind_of?(Array)
159
+ raise ArgumentError, "#{name} field may only have one value" if value.size > 1
160
+ value.first
161
+ else
162
+ value
163
+ end
164
+ end
165
+
166
+ def delete(raw_fields_hash)
167
+ raw_fields_hash.delete(name)
168
+ end
169
+
170
+ def exists_in?(raw_fields_hash)
171
+ raw_fields_hash.has_key?(name)
172
+ end
173
+
174
+ def <=>(another)
175
+ name <=> another.name
176
+ end
177
+
178
+ def ==(another)
179
+ name_pattern === another.to_s
180
+ end
181
+ alias eql? ==
182
+
183
+ def ===(another)
184
+ if another.kind_of?(FieldDesc)
185
+ self == another
186
+ else
187
+ name_pattern === another
188
+ end
189
+ end
190
+
191
+ def name_pattern
192
+ @name_pattern ||= Regexp.new('^' + name.gsub('-', '[_-]') + '$', Regexp::IGNORECASE)
193
+ end
194
+
195
+ def methodized_name
196
+ @methodized_name ||= name.downcase.gsub('-', '_')
197
+ end
198
+
199
+ def constantized_name
200
+ @constantized_name ||= name.upcase.gsub('-', '_')
201
+ end
202
+
203
+ alias to_s name
204
+
205
+ def accessor_module
206
+ @accessor_module ||= begin
207
+ Module.new.tap{|m| m.module_eval(<<-RUBY)}
208
+ #{constantized_name} = '#{name}'
209
+
210
+ def #{methodized_name} # def accept
211
+ self[#{constantized_name}] # self[ACCEPT]
212
+ end # end
213
+
214
+ def #{methodized_name}=(val) # def accept=(val)
215
+ self[#{constantized_name}] = val # self[ACCEPT] = val
216
+ end # end
217
+ RUBY
218
+ end
219
+ end
220
+
221
+ def hash
222
+ @name.hash
223
+ end
224
+
225
+ # Yields each commonly used lookup key for this header field.
226
+ def lookup_keys(&blk)
227
+ yield name
228
+ yield name.upcase
229
+ yield name.downcase
230
+ yield methodized_name
231
+ yield methodized_name.to_sym
232
+ yield constantized_name
233
+ yield constantized_name.to_sym
234
+ end
235
+ end # FieldDesc
236
+
237
+ @@known_fields = Set.new
238
+ @@known_fields_lookup = Hash.new
239
+
240
+ # Declares a common header field. Header fields do not have to be
241
+ # defined this way but accessing them is easier, safer and faster
242
+ # if you do. Declaring a field does the following things:
243
+ #
244
+ # * defines accessor methods (e.g. `#content_type` and
245
+ # `#content_type=`) on `Header`
246
+ #
247
+ # * defines constant that can be used to reference there field
248
+ # name (e.g. `some_header[Header::CONTENT_TYPE]`)
249
+ #
250
+ # * includes the field in the appropriate *_fields groups (e.g. `Header.non_modifiable_fields`)
251
+ #
252
+ # * provides improved multiple value parsing
253
+ #
254
+ # Create a new header field descriptor.
255
+ #
256
+ # @param [String] name The canonical name of this field.
257
+ #
258
+ # @param [Hash] options hash containing extra information about
259
+ # this header fields. Valid keys are:
260
+ #
261
+ # `:multivalued`
262
+ # `:multivalue`
263
+ # `:repeatable`
264
+ # : Values of this field are comma separated list of values.
265
+ # (n#VALUE per HTTP spec.) Default: false
266
+ #
267
+ # `:hop_by_hop`
268
+ # : True if the header is a hop-by-hop header. Default: false
269
+ #
270
+ # `:modifiable`
271
+ # : False if the header should not be modified by intermediates or caches. Default: true
272
+ #
273
+ def self.header_field(name, options = {})
274
+ hfd = FieldDesc.new(name, options)
275
+
276
+ @@known_fields << hfd
277
+ hfd.lookup_keys do |a_key|
278
+ @@known_fields_lookup[a_key] = hfd
279
+ end
280
+
281
+ include(hfd.accessor_module)
282
+ end
283
+
284
+ def self.hop_by_hop_fields
285
+ @@known_fields.select{|hfd| hfd.hop_by_hop?}
286
+ end
287
+
288
+ def self.non_modifiable_fields
289
+ @@known_fields.reject{|hfd| hfd.modifiable?}
290
+ end
291
+
292
+ protected
293
+
294
+ # ---
295
+ #
296
+ # We have to fall back on a slow iteration to find the header
297
+ # field some times because field names are
298
+ def field_def(name)
299
+ @@known_fields_lookup[name] || # the fast way
300
+ @@known_fields.find{|hfd| hfd === name} || # the slow way
301
+ FieldDesc.new(name.to_s.downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }.gsub('_', '-'), :repeatable => true) # make up as we go
302
+ end
303
+
304
+ header_field('Accept', :repeatable => true)
305
+ header_field('Accept-Charset', :repeatable => true)
306
+ header_field('Accept-Encoding', :repeatable => true)
307
+ header_field('Accept-Language', :repeatable => true)
308
+ header_field('Accept-Ranges', :repeatable => true)
309
+ header_field('Age')
310
+ header_field('Allow', :repeatable => true)
311
+ header_field('Authorization', :repeatable => true)
312
+ header_field('Cache-Control', :repeatable => true)
313
+ header_field('Connection', :hop_by_hop => true)
314
+ header_field('Content-Encoding', :repeatable => true)
315
+ header_field('Content-Language', :repeatable => true)
316
+ header_field('Content-Length')
317
+ header_field('Content-Location', :modifiable => false)
318
+ header_field('Content-MD5', :modifiable => false)
319
+ header_field('Content-Range')
320
+ header_field('Content-Type')
321
+ header_field('Date')
322
+ header_field('ETag', :modifiable => false)
323
+ header_field('Expect', :repeatable => true)
324
+ header_field('Expires', :modifiable => false)
325
+ header_field('From')
326
+ header_field('Host')
327
+ header_field('If-Match', :repeatable => true)
328
+ header_field('If-Modified-Since')
329
+ header_field('If-None-Match', :repeatable => true)
330
+ header_field('If-Range')
331
+ header_field('If-Unmodified-Since')
332
+ header_field('Keep-Alive', :hop_by_hop => true)
333
+ header_field('Last-Modified', :modifiable => false)
334
+ header_field('Location')
335
+ header_field('Max-Forwards')
336
+ header_field('Pragma', :repeatable => true)
337
+ header_field('Proxy-Authenticate', :hop_by_hop => true)
338
+ header_field('Proxy-Authorization', :hop_by_hop => true)
339
+ header_field('Range')
340
+ header_field('Referer')
341
+ header_field('Retry-After')
342
+ header_field('Server')
343
+ header_field('TE', :repeatable => true, :hop_by_hop => true)
344
+ header_field('Trailer', :repeatable => true, :hop_by_hop => true)
345
+ header_field('Transfer-Encoding', :repeatable => true, :hop_by_hop => true)
346
+ header_field('Upgrade', :repeatable => true, :hop_by_hop => true)
347
+ header_field('User-Agent')
348
+ header_field('Vary', :repeatable => true)
349
+ header_field('Via', :repeatable => true)
350
+ header_field('Warning', :repeatable => true)
351
+ header_field('WWW-Authenticate', :repeatable => true)
352
+ end
353
+ end
354
+
355
+