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,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
+