webmachine 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/Gemfile +11 -3
  2. data/README.md +55 -27
  3. data/lib/webmachine/adapters/mongrel.rb +84 -0
  4. data/lib/webmachine/adapters/webrick.rb +12 -3
  5. data/lib/webmachine/adapters.rb +1 -7
  6. data/lib/webmachine/configuration.rb +30 -0
  7. data/lib/webmachine/decision/conneg.rb +7 -72
  8. data/lib/webmachine/decision/flow.rb +13 -11
  9. data/lib/webmachine/decision/fsm.rb +1 -9
  10. data/lib/webmachine/decision/helpers.rb +27 -7
  11. data/lib/webmachine/errors.rb +1 -0
  12. data/lib/webmachine/headers.rb +12 -3
  13. data/lib/webmachine/locale/en.yml +2 -2
  14. data/lib/webmachine/media_type.rb +117 -0
  15. data/lib/webmachine/resource/callbacks.rb +9 -0
  16. data/lib/webmachine/streaming.rb +3 -3
  17. data/lib/webmachine/version.rb +1 -1
  18. data/lib/webmachine.rb +3 -1
  19. data/pkg/webmachine-0.1.0/Gemfile +16 -0
  20. data/pkg/webmachine-0.1.0/Guardfile +11 -0
  21. data/pkg/webmachine-0.1.0/README.md +90 -0
  22. data/pkg/webmachine-0.1.0/Rakefile +31 -0
  23. data/pkg/webmachine-0.1.0/examples/webrick.rb +19 -0
  24. data/pkg/webmachine-0.1.0/lib/webmachine/adapters/webrick.rb +74 -0
  25. data/pkg/webmachine-0.1.0/lib/webmachine/adapters.rb +15 -0
  26. data/pkg/webmachine-0.1.0/lib/webmachine/decision/conneg.rb +304 -0
  27. data/pkg/webmachine-0.1.0/lib/webmachine/decision/flow.rb +502 -0
  28. data/pkg/webmachine-0.1.0/lib/webmachine/decision/fsm.rb +79 -0
  29. data/pkg/webmachine-0.1.0/lib/webmachine/decision/helpers.rb +80 -0
  30. data/pkg/webmachine-0.1.0/lib/webmachine/decision.rb +12 -0
  31. data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher/route.rb +85 -0
  32. data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher.rb +40 -0
  33. data/pkg/webmachine-0.1.0/lib/webmachine/errors.rb +37 -0
  34. data/pkg/webmachine-0.1.0/lib/webmachine/headers.rb +16 -0
  35. data/pkg/webmachine-0.1.0/lib/webmachine/locale/en.yml +28 -0
  36. data/pkg/webmachine-0.1.0/lib/webmachine/request.rb +56 -0
  37. data/pkg/webmachine-0.1.0/lib/webmachine/resource/callbacks.rb +362 -0
  38. data/pkg/webmachine-0.1.0/lib/webmachine/resource/encodings.rb +36 -0
  39. data/pkg/webmachine-0.1.0/lib/webmachine/resource.rb +48 -0
  40. data/pkg/webmachine-0.1.0/lib/webmachine/response.rb +49 -0
  41. data/pkg/webmachine-0.1.0/lib/webmachine/streaming.rb +27 -0
  42. data/pkg/webmachine-0.1.0/lib/webmachine/translation.rb +11 -0
  43. data/pkg/webmachine-0.1.0/lib/webmachine/version.rb +4 -0
  44. data/pkg/webmachine-0.1.0/lib/webmachine.rb +19 -0
  45. data/pkg/webmachine-0.1.0/spec/spec_helper.rb +13 -0
  46. data/pkg/webmachine-0.1.0/spec/tests.org +57 -0
  47. data/pkg/webmachine-0.1.0/spec/webmachine/decision/conneg_spec.rb +152 -0
  48. data/pkg/webmachine-0.1.0/spec/webmachine/decision/flow_spec.rb +1030 -0
  49. data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher/route_spec.rb +109 -0
  50. data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher_spec.rb +34 -0
  51. data/pkg/webmachine-0.1.0/spec/webmachine/headers_spec.rb +19 -0
  52. data/pkg/webmachine-0.1.0/spec/webmachine/request_spec.rb +24 -0
  53. data/pkg/webmachine-0.1.0/webmachine.gemspec +44 -0
  54. data/pkg/webmachine-0.1.0.gem +0 -0
  55. data/spec/webmachine/configuration_spec.rb +27 -0
  56. data/spec/webmachine/decision/conneg_spec.rb +18 -11
  57. data/spec/webmachine/decision/flow_spec.rb +2 -0
  58. data/spec/webmachine/decision/helpers_spec.rb +105 -0
  59. data/spec/webmachine/errors_spec.rb +13 -0
  60. data/spec/webmachine/headers_spec.rb +2 -1
  61. data/spec/webmachine/media_type_spec.rb +78 -0
  62. data/webmachine.gemspec +4 -1
  63. metadata +69 -11
@@ -0,0 +1,362 @@
1
+ module Webmachine
2
+ class Resource
3
+ # These
4
+ module Callbacks
5
+ # Does the resource exist? Returning a falsey value (false or nil)
6
+ # will result in a '404 Not Found' response. Defaults to true.
7
+ # @return [true,false] Whether the resource exists
8
+ # @api callback
9
+ def resource_exists?
10
+ true
11
+ end
12
+
13
+ # Is the resource available? Returning a falsey value (false or
14
+ # nil) will result in a '503 Service Not Available'
15
+ # response. Defaults to true. If the resource is only temporarily
16
+ # not available, add a 'Retry-After' response header in the body
17
+ # of the method.
18
+ # @return [true,false]
19
+ # @api callback
20
+ def service_available?
21
+ true
22
+ end
23
+
24
+ # Is the client or request authorized? Returning anything other than true
25
+ # will result in a '401 Unauthorized' response. Defaults to
26
+ # true. If a String is returned, it will be used as the value in
27
+ # the WWW-Authenticate header, which can also be set manually.
28
+ # @param [String] authorization_header The contents of the
29
+ # 'Authorization' header sent by the client, if present.
30
+ # @return [true,false,String] Whether the client is authorized,
31
+ # and if not, the WWW-Authenticate header when a String.
32
+ # @api callback
33
+ def is_authorized?(authorization_header = nil)
34
+ true
35
+ end
36
+
37
+ # Is the request or client forbidden? Returning a truthy value
38
+ # (true or non-nil) will result in a '403 Forbidden' response.
39
+ # Defaults to false.
40
+ # @return [true,false] Whether the client or request is forbidden.
41
+ # @api callback
42
+ def forbidden?
43
+ false
44
+ end
45
+
46
+ # If the resource accepts POST requests to nonexistent resources,
47
+ # then this should return true. Defaults to false.
48
+ # @return [true,false] Whether to accept POST requests to missing
49
+ # resources.
50
+ # @api callback
51
+ def allow_missing_post?
52
+ false
53
+ end
54
+
55
+ # If the request is malformed, this should return true, which will
56
+ # result in a '400 Malformed Request' response. Defaults to false.
57
+ # @return [true,false] Whether the request is malformed.
58
+ # @api callback
59
+ def malformed_request?
60
+ false
61
+ end
62
+
63
+ # If the URI is too long to be processed, this should return true,
64
+ # which will result in a '414 Request URI Too Long'
65
+ # response. Defaults to false.
66
+ # @param [URI] uri the request URI
67
+ # @return [true,false] Whether the request URI is too long.
68
+ # @api callback
69
+ def uri_too_long?(uri = nil)
70
+ false
71
+ end
72
+
73
+ # If the Content-Type on PUT or POST is unknown, this should
74
+ # return false, which will result in a '415 Unsupported Media
75
+ # Type' response. Defaults to true.
76
+ # @param [String] content_type the 'Content-Type' header sent by
77
+ # the client
78
+ # @return [true,false] Whether the passed media type is known or
79
+ # accepted
80
+ # @api callback
81
+ def known_content_type?(content_type = nil)
82
+ true
83
+ end
84
+
85
+ # If the request includes any invalid Content-* headers, this
86
+ # should return false, which will result in a '501 Not
87
+ # Implemented' response. Defaults to false.
88
+ # @param [Hash] content_headers Request headers that begin with
89
+ # 'Content-'
90
+ # @return [true,false] Whether the Content-* headers are invalid
91
+ # or unsupported
92
+ # @api callback
93
+ def valid_content_headers?(content_headers = nil)
94
+ true
95
+ end
96
+
97
+ # If the entity length on PUT or POST is invalid, this should
98
+ # return false, which will result in a '413 Request Entity Too
99
+ # Large' response. Defaults to true.
100
+ # @param [Fixnum] length the size of the request body (entity)
101
+ # @return [true,false] Whether the body is a valid length (not too
102
+ # large)
103
+ # @api callback
104
+ def valid_entity_length?(length = nil)
105
+ true
106
+ end
107
+
108
+ # If the OPTIONS method is supported and is used, this method
109
+ # should return a Hash of headers that should appear in the
110
+ # response. Defaults to {}.
111
+ # @return [Hash] headers to appear in the response
112
+ # @api callback
113
+ def options
114
+ {}
115
+ end
116
+
117
+ # HTTP methods that are allowed on this resource. This must return
118
+ # an Array of Strings in all capitals. Defaults to ['GET','HEAD'].
119
+ # @return [Array<String>] allowed methods on this resource
120
+ # @api callback
121
+ def allowed_methods
122
+ ['GET', 'HEAD']
123
+ end
124
+
125
+ # HTTP methods that are known to the resource. Like
126
+ # {#allowed_methods}, this must return an Array of Strings in
127
+ # all capitals. Default includes all standard HTTP methods. One
128
+ # could override this callback to allow additional methods,
129
+ # e.g. WebDAV.
130
+ # @return [Array<String>] known methods
131
+ # @api callback
132
+ def known_methods
133
+ ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS']
134
+ end
135
+
136
+ # This method is called when a DELETE request should be enacted,
137
+ # and should return true if the deletion succeeded. Defaults to false.
138
+ # @return [true,false] Whether the deletion succeeded.
139
+ # @api callback
140
+ def delete_resource
141
+ false
142
+ end
143
+
144
+ # This method is called after a successful call to
145
+ # {#delete_resource} and should return false if the deletion was
146
+ # accepted but cannot yet be guaranteed to have finished. Defaults
147
+ # to true.
148
+ # @return [true,false] Whether the deletion completed
149
+ # @api callback
150
+ def delete_completed?
151
+ true
152
+ end
153
+
154
+ # If POST requests should be treated as a request to put content
155
+ # into a (potentially new) resource as opposed to a generic
156
+ # submission for processing, then this method should return
157
+ # true. If it does return true, then {#create_path} will be called
158
+ # and the rest of the request will be treated much like a PUT to
159
+ # the path returned by that call. Default is false.
160
+ # @return [true,false] Whether POST creates a new resource
161
+ # @api callback
162
+ def post_is_create?
163
+ false
164
+ end
165
+
166
+ # This will be called on a POST request if post_is_create? returns
167
+ # true. The path returned should be a valid URI part following the
168
+ # dispatcher prefix. That path will replace the previous one in
169
+ # the return value of {Request#disp_path} for all subsequent
170
+ # resource function calls in the course of this request.
171
+ # @return [String, URI] the path to the new resource
172
+ # @api callback
173
+ def create_path
174
+ nil
175
+ end
176
+
177
+ # This will be called after {#create_path} but before setting the
178
+ # Location response header, and is used to determine the root
179
+ # URI of the new resource. Default is nil, which uses the URI of
180
+ # the request as the base.
181
+ # @return [String, URI, nil]
182
+ # @api callback
183
+ def base_uri
184
+ nil
185
+ end
186
+
187
+ # If post_is_create? returns false, then this will be called to
188
+ # process any POST request. If it succeeds, it should return true.
189
+ # @return [true,false,Fixnum] Whether the POST was successfully
190
+ # processed, or an alternate response code
191
+ # @api callback
192
+ def process_post
193
+ false
194
+ end
195
+
196
+ # This should return an array of pairs where each pair is of the
197
+ # form [mediatype, :handler] where mediatype is a String of
198
+ # Content-Type format and :handler is a Symbol naming the method
199
+ # which can provide a resource representation in that media
200
+ # type. For example, if a client request includes an 'Accept'
201
+ # header with a value that does not appear as a first element in
202
+ # any of the return pairs, then a '406 Not Acceptable' will be
203
+ # sent. Default is [['text/html', :to_html]].
204
+ # @return an array of mediatype/handler pairs
205
+ # @api callback
206
+ def content_types_provided
207
+ [['text/html', :to_html]]
208
+ end
209
+
210
+ # Similarly to content_types_provided, this should return an array
211
+ # of mediatype/handler pairs, except that it is for incoming
212
+ # resource representations -- for example, PUT requests. Handler
213
+ # functions usually want to use {Request#body} to access the
214
+ # incoming entity.
215
+ # @return [Array] an array of mediatype/handler pairs
216
+ # @api callback
217
+ def content_types_accepted
218
+ []
219
+ end
220
+
221
+ # If this is anything other than nil, it must be an array of pairs
222
+ # where each pair is of the form Charset, Converter where Charset
223
+ # is a string naming a charset and Converter is an arity-1 method
224
+ # in the resource which will be called on the produced body in a
225
+ # GET and ensure that it is in Charset.
226
+ # @return [nil, Array] The provided character sets and encoder
227
+ # methods, or nothing.
228
+ # @api callback
229
+ def charsets_provided
230
+ nil
231
+ end
232
+
233
+ # This should return a list of language tags provided by the
234
+ # resource. Default is the empty Array, in which the content is
235
+ # in no specific language.
236
+ # @return [Array<String>] a list of provided languages
237
+ # @api callback
238
+ def languages_provided
239
+ []
240
+ end
241
+
242
+ # This should return a hash of encodings mapped to encoding
243
+ # methods for Content-Encodings your resource wants to
244
+ # provide. The encoding will be applied to the response body
245
+ # automatically by Webmachine. A number of built-in encodings
246
+ # are provided in the {Encodings} module. Default includes only
247
+ # the 'identity' encoding.
248
+ # @return [Hash] a hash of encodings and encoder methods/procs
249
+ # @api callback
250
+ # @see Encodings
251
+ def encodings_provided
252
+ {"identity" => :encode_identity }
253
+ end
254
+
255
+ # If this method is implemented, it should return a list of
256
+ # strings with header names that should be included in a given
257
+ # response's Vary header. The standard conneg headers (Accept,
258
+ # Accept-Encoding, Accept-Charset, Accept-Language) do not need to
259
+ # be specified here as Webmachine will add the correct elements of
260
+ # those automatically depending on resource behavior. Default is
261
+ # [].
262
+ # @api callback
263
+ # @return [Array<String>] a list of variance headers
264
+ def variances
265
+ []
266
+ end
267
+
268
+ # If this returns true, the client will receive a '409 Conflict'
269
+ # response. This is only called for PUT requests. Default is false.
270
+ # @api callback
271
+ # @return [true,false] whether the submitted entity is in conflict
272
+ # with the current state of the resource
273
+ def is_conflict?
274
+ false
275
+ end
276
+
277
+ # If this returns true, then it is assumed that multiple
278
+ # representations of the response are possible and a single one
279
+ # cannot be automatically chosen, so a 300 Multiple Choices will
280
+ # be sent instead of a 200. Default is false.
281
+ # @api callback
282
+ # @return [true,false] whether the multiple representations are
283
+ # possible
284
+ def multiple_choices?
285
+ false
286
+ end
287
+
288
+ # If this resource is known to have existed previously, this
289
+ # method should return true. Default is false.
290
+ # @api callback
291
+ # @return [true,false] whether the resource existed previously
292
+ def previously_existed?
293
+ false
294
+ end
295
+
296
+ # If this resource has moved to a new location permanently, this
297
+ # method should return the new location as a String or
298
+ # URI. Default is to return false.
299
+ # @api callback
300
+ # @return [String,URI,false] the new location of the resource, or
301
+ # false
302
+ def moved_permanently?
303
+ false
304
+ end
305
+
306
+ # If this resource has moved to a new location temporarily, this
307
+ # method should return the new location as a String or
308
+ # URI. Default is to return false.
309
+ # @api callback
310
+ # @return [String,URI,false] the new location of the resource, or
311
+ # false
312
+ def moved_temporarily?
313
+ false
314
+ end
315
+
316
+ # This method should return the last modified date/time of the
317
+ # resource which will be added as the Last-Modified header in the
318
+ # response and used in negotiating conditional requests. Default
319
+ # is nil.
320
+ # @api callback
321
+ # @return [Time,DateTime,Date,nil] the last modified time
322
+ def last_modified
323
+ nil
324
+ end
325
+
326
+ # If the resource expires, this method should return the date/time
327
+ # it expires. Default is nil.
328
+ # @api callback
329
+ # @return [Time,DateTime,Date,nil] the expiration time
330
+ def expires
331
+ nil
332
+ end
333
+
334
+ # If this returns a value, it will be used as the value of the
335
+ # ETag header and for comparison in conditional requests. Default
336
+ # is nil.
337
+ # @api callback
338
+ # @return [String,nil] the entity tag for this resource
339
+ def generate_etag
340
+ nil
341
+ end
342
+
343
+ # This method is called just before the final response is
344
+ # constructed and sent. The return value is ignored, so any effect
345
+ # of this method must be by modifying the response.
346
+ # @api callback
347
+ def finish_request; end
348
+
349
+ # This method is called when verifying the Content-MD5 header
350
+ # against the request body. To do your own validation, implement
351
+ # it in this callback, returning true or false. To bypass header
352
+ # validation, simply return true. Default is nil, which will
353
+ # invoke Webmachine's default validation.
354
+ # @api callback
355
+ # @return [true,false,nil] Whether the Content-MD5 header
356
+ # validates against the request body
357
+ def validate_content_checksum
358
+ nil
359
+ end
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,36 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+
4
+ module Webmachine
5
+ class Resource
6
+ # This module implements standard Content-Encodings that you might
7
+ # want to use in your {Resource}. To use one, simply return it in
8
+ # the hash from {Callbacks#encodings_provided}.
9
+ module Encodings
10
+ # The 'identity' encoding, which does no compression.
11
+ def encode_identity(data)
12
+ data
13
+ end
14
+
15
+ # The 'deflate' encoding, which uses libz's DEFLATE compression.
16
+ def encode_deflate(data)
17
+ # The deflate options were borrowed from Rack and Mongrel1.
18
+ Zlib::Deflate.deflate(data, *[Zlib::DEFAULT_COMPRESSION,
19
+ # drop the zlib header which causes both Safari and IE to choke
20
+ -Zlib::MAX_WBITS,
21
+ Zlib::DEF_MEM_LEVEL,
22
+ Zlib::DEFAULT_STRATEGY
23
+ ])
24
+ end
25
+
26
+ # The 'gzip' encoding, which uses GNU Zip (via libz).
27
+ # @note Because of the header/checksum requirements, gzip cannot
28
+ # be used on streamed responses.
29
+ def encode_gzip(data)
30
+ "".tap do |out|
31
+ Zlib::GzipWriter.wrap(StringIO.new(out)){|gz| gz << data }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ require 'webmachine/resource/callbacks'
2
+ require 'webmachine/resource/encodings'
3
+
4
+ module Webmachine
5
+ # Resource is the primary building block of Webmachine applications,
6
+ # and describes families of HTTP resources. It includes all of the
7
+ # methods you might want to override to customize the behavior of
8
+ # the resource. The simplest resource family you can implement
9
+ # looks like this:
10
+ #
11
+ # class HelloWorldResource < Webmachine::Resource
12
+ # def to_html
13
+ # "<html><body>Hello, world!</body></html>"
14
+ # end
15
+ # end
16
+ #
17
+ # For more information about how response decisions are made in
18
+ # Webmachine based on your resource class, refer to the diagram at
19
+ # {http://webmachine.basho.com/images/http-headers-status-v3.png}.
20
+ class Resource
21
+ include Callbacks
22
+ include Encodings
23
+
24
+ attr_reader :request, :response
25
+
26
+ # Creates a new {Resource}, initializing it with the request and
27
+ # response. Note that you may still override {#initialize} to
28
+ # initialize your resource. It will be called after the request
29
+ # and response ivars are set.
30
+ # @param [Request] request the request object
31
+ # @param [Response] response the response object
32
+ # @return [Resource] the new resource
33
+ def self.new(request, response)
34
+ instance = allocate
35
+ instance.instance_variable_set(:@request, request)
36
+ instance.instance_variable_set(:@response, response)
37
+ instance.send :initialize
38
+ instance
39
+ end
40
+
41
+ private
42
+ # When no specific charsets are provided, this acts as an identity
43
+ # on the response body. Probably deserves some refactoring.
44
+ def charset_nop(x)
45
+ x
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,49 @@
1
+ module Webmachine
2
+ # Represents an HTTP response from Webmachine.
3
+ class Response
4
+ # @return [Hash] Response headers that will be sent to the client
5
+ attr_reader :headers
6
+
7
+ # @return [Fixnum] The HTTP status code of the response
8
+ attr_accessor :code
9
+
10
+ # @return [String, #each] The response body
11
+ attr_accessor :body
12
+
13
+ # @return [true,false] Whether the response is a redirect
14
+ attr_accessor :redirect
15
+
16
+ # @return [Array] the list of states that were traversed
17
+ attr_reader :trace
18
+
19
+ # @return [Symbol] When an error has occurred, the last state the
20
+ # FSM was in
21
+ attr_accessor :end_state
22
+
23
+ # @return [String] The error message when responding with an error
24
+ # code
25
+ attr_accessor :error
26
+
27
+ # Creates a new Response object with the appropriate defaults.
28
+ def initialize
29
+ @headers = {}
30
+ @trace = []
31
+ self.code = 200
32
+ self.redirect = false
33
+ end
34
+
35
+ # Indicate that the response should be a redirect. This is only
36
+ # used when processing a POST request in {Callbacks#process_post}
37
+ # to indicate that the client should request another resource
38
+ # using GET. Either pass the URI of the target resource, or
39
+ # manually set the Location header using {#headers}.
40
+ # @param [String, URI] location the target of the redirection
41
+ def do_redirect(location=nil)
42
+ headers['Location'] = location.to_s if location
43
+ self.redirect = true
44
+ end
45
+
46
+ alias :is_redirect? :redirect
47
+ alias :redirect_to :do_redirect
48
+ end
49
+ end