webmachine 0.3.0 → 0.4.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 (40) hide show
  1. data/Gemfile +12 -10
  2. data/Guardfile +1 -1
  3. data/README.md +1 -1
  4. data/examples/application.rb +35 -0
  5. data/lib/webmachine.rb +4 -3
  6. data/lib/webmachine/adapter.rb +36 -0
  7. data/lib/webmachine/adapters/mongrel.rb +18 -12
  8. data/lib/webmachine/adapters/rack.rb +26 -7
  9. data/lib/webmachine/adapters/webrick.rb +20 -16
  10. data/lib/webmachine/application.rb +108 -0
  11. data/lib/webmachine/chunked_body.rb +2 -2
  12. data/lib/webmachine/configuration.rb +24 -14
  13. data/lib/webmachine/decision/conneg.rb +9 -10
  14. data/lib/webmachine/decision/flow.rb +25 -28
  15. data/lib/webmachine/decision/fsm.rb +21 -22
  16. data/lib/webmachine/decision/helpers.rb +3 -3
  17. data/lib/webmachine/dispatcher.rb +18 -10
  18. data/lib/webmachine/dispatcher/route.rb +54 -17
  19. data/lib/webmachine/errors.rb +1 -1
  20. data/lib/webmachine/headers.rb +2 -2
  21. data/lib/webmachine/media_type.rb +2 -2
  22. data/lib/webmachine/request.rb +78 -3
  23. data/lib/webmachine/resource.rb +3 -2
  24. data/lib/webmachine/resource/authentication.rb +4 -3
  25. data/lib/webmachine/resource/callbacks.rb +4 -3
  26. data/lib/webmachine/resource/encodings.rb +4 -3
  27. data/lib/webmachine/response.rb +3 -2
  28. data/lib/webmachine/streaming.rb +4 -4
  29. data/lib/webmachine/version.rb +1 -1
  30. data/spec/webmachine/adapter_spec.rb +40 -0
  31. data/spec/webmachine/adapters/mongrel_spec.rb +22 -0
  32. data/spec/webmachine/adapters/rack_spec.rb +34 -8
  33. data/spec/webmachine/adapters/webrick_spec.rb +18 -0
  34. data/spec/webmachine/application_spec.rb +73 -0
  35. data/spec/webmachine/dispatcher/route_spec.rb +59 -2
  36. data/spec/webmachine/dispatcher_spec.rb +17 -5
  37. data/spec/webmachine/request_spec.rb +158 -1
  38. data/webmachine.gemspec +6 -27
  39. metadata +304 -112
  40. data/spec/tests.org +0 -80
@@ -10,21 +10,31 @@ module Webmachine
10
10
  # @attr [Hash] adapter_options adapter-specific options, defaults to {}
11
11
  Configuration = Struct.new(:ip, :port, :adapter, :adapter_options)
12
12
 
13
- class << self
14
- # @return [Configuration] the current configuration
15
- attr_accessor :configuration
13
+ # @return [Configuration] the default configuration
14
+ def Configuration.default
15
+ new("0.0.0.0", 8080, :WEBrick, {})
16
16
  end
17
17
 
18
- # Sets configuration for the web server via the passed
19
- # block. Returns Webmachine so you can chain it with
20
- # Webmachine.run.
21
- # @yield [config] a block in which to set configuration values
22
- # @yieldparam [Configuration] config the Configuration instance
23
- # @return [Webmachine]
24
- def self.configure
25
- @configuration ||= Configuration.new("0.0.0.0", 8080, :WEBrick, {})
26
- yield @configuration if block_given?
18
+ # Yields the current configuration to the passed block.
19
+ # @yield [config] a block that will modify the configuration
20
+ # @yieldparam [Configuration] config the adapter configuration
21
+ # @return self
22
+ # @see Application#configure
23
+ def self.configure(&block)
24
+ application.configure(&block)
27
25
  self
28
26
  end
29
- end
30
-
27
+
28
+ # @return [Configuration] the current configuration
29
+ # @see Application#configuration
30
+ def self.configuration
31
+ application.configuration
32
+ end
33
+
34
+ # Sets the current configuration
35
+ # @param [Configuration] configuration the new config
36
+ # @see Application#configuration=
37
+ def self.configuration=(configuration)
38
+ application.configuration = configuration
39
+ end
40
+ end # Webmachine
@@ -226,15 +226,14 @@ module Webmachine
226
226
  # {MediaType} items instead of Strings.
227
227
  # @see PriorityList#add_header_val
228
228
  def add_header_val(c)
229
- begin
230
- mt = MediaType.parse(c)
231
- q = mt.params.delete('q') || 1.0
232
- add(q.to_f, mt)
233
- rescue ArgumentError
234
- raise MalformedRequest, t('invalid_media_type', :type => c)
235
- end
229
+ mt = MediaType.parse(c)
230
+ q = mt.params.delete('q') || 1.0
231
+ add(q.to_f, mt)
232
+ rescue ArgumentError
233
+ raise MalformedRequest, t('invalid_media_type', :type => c)
236
234
  end
237
235
  end
238
- end
239
- end
240
- end
236
+
237
+ end # module Conneg
238
+ end # module Decision
239
+ end # module Webmachine
@@ -131,7 +131,7 @@ module Webmachine
131
131
 
132
132
  # OPTIONS?
133
133
  def b3
134
- if request.method == "OPTIONS"
134
+ if request.options?
135
135
  response.headers.merge!(resource.options)
136
136
  200
137
137
  else
@@ -254,14 +254,12 @@ module Webmachine
254
254
 
255
255
  # If-Unmodified-Since is valid date?
256
256
  def h11
257
- begin
258
- date = Time.httpdate(request.if_unmodified_since)
259
- metadata['If-Unmodified-Since'] = date
260
- rescue ArgumentError
261
- :i12
262
- else
263
- :h12
264
- end
257
+ date = Time.httpdate(request.if_unmodified_since)
258
+ metadata['If-Unmodified-Since'] = date
259
+ rescue ArgumentError
260
+ :i12
261
+ else
262
+ :h12
265
263
  end
266
264
 
267
265
  # Last-Modified > I-UM-S?
@@ -284,7 +282,7 @@ module Webmachine
284
282
 
285
283
  # PUT?
286
284
  def i7
287
- request.method == "PUT" ? :i4 : :k7
285
+ request.put? ? :i4 : :k7
288
286
  end
289
287
 
290
288
  # If-none-match exists?
@@ -299,7 +297,7 @@ module Webmachine
299
297
 
300
298
  # GET or HEAD?
301
299
  def j18
302
- %w{GET HEAD}.include?(request.method) ? 304 : 412
300
+ (request.get? || request.head?) ? 304 : 412
303
301
  end
304
302
 
305
303
  # Moved permanently?
@@ -341,7 +339,7 @@ module Webmachine
341
339
 
342
340
  # POST?
343
341
  def l7
344
- request.method == "POST" ? :m7 : 404
342
+ request.post? ? :m7 : 404
345
343
  end
346
344
 
347
345
  # If-Modified-Since exists?
@@ -351,14 +349,12 @@ module Webmachine
351
349
 
352
350
  # IMS is valid date?
353
351
  def l14
354
- begin
355
- date = Time.httpdate(request.if_modified_since)
356
- metadata['If-Modified-Since'] = date
357
- rescue ArgumentError
358
- :m16
359
- else
360
- :l15
361
- end
352
+ date = Time.httpdate(request.if_modified_since)
353
+ metadata['If-Modified-Since'] = date
354
+ rescue ArgumentError
355
+ :m16
356
+ else
357
+ :l15
362
358
  end
363
359
 
364
360
  # IMS > Now?
@@ -373,7 +369,7 @@ module Webmachine
373
369
 
374
370
  # POST?
375
371
  def m5
376
- request.method == "POST" ? :n5 : 410
372
+ request.post? ? :n5 : 410
377
373
  end
378
374
 
379
375
  # Server allows POST to missing resource?
@@ -383,7 +379,7 @@ module Webmachine
383
379
 
384
380
  # DELETE?
385
381
  def m16
386
- request.method == "DELETE" ? :m20 : :n16
382
+ request.delete? ? :m20 : :n16
387
383
  end
388
384
 
389
385
  # DELETE enacted immediately? (Also where DELETE is forced.)
@@ -439,7 +435,7 @@ module Webmachine
439
435
 
440
436
  # POST?
441
437
  def n16
442
- request.method == "POST" ? :n11 : :o16
438
+ request.post? ? :n11 : :o16
443
439
  end
444
440
 
445
441
  # Conflict?
@@ -454,13 +450,13 @@ module Webmachine
454
450
 
455
451
  # PUT?
456
452
  def o16
457
- request.method == "PUT" ? :o14 : :o18
453
+ request.put? ? :o14 : :o18
458
454
  end
459
455
 
460
456
  # Multiple representations?
461
457
  # Also where body generation for GET and HEAD is done.
462
458
  def o18
463
- if request.method =~ /^(GET|HEAD)$/
459
+ if request.get? || request.head?
464
460
  add_caching_headers
465
461
  content_type = metadata['Content-Type']
466
462
  handler = resource.content_types_provided.find {|ct, _| content_type.type_matches?(MediaType.parse(ct)) }.last
@@ -501,6 +497,7 @@ module Webmachine
501
497
  def p11
502
498
  !response.headers["Location"] ? :o20 : 201
503
499
  end
504
- end
505
- end
506
- end
500
+
501
+ end # module Flow
502
+ end # module Decision
503
+ end # module Webmachine
@@ -20,27 +20,25 @@ module Webmachine
20
20
 
21
21
  # Processes the request, iteratively invoking the decision methods in {Flow}.
22
22
  def run
23
- begin
24
- state = Flow::START
25
- loop do
26
- response.trace << state
27
- result = send(state)
28
- case result
29
- when Fixnum # Response code
30
- respond(result)
31
- break
32
- when Symbol # Next state
33
- state = result
34
- else # You bwoke it
35
- raise InvalidResource, t('fsm_broke', :state => state, :result => result.inspect)
36
- end
23
+ state = Flow::START
24
+ loop do
25
+ response.trace << state
26
+ result = send(state)
27
+ case result
28
+ when Fixnum # Response code
29
+ respond(result)
30
+ break
31
+ when Symbol # Next state
32
+ state = result
33
+ else # You bwoke it
34
+ raise InvalidResource, t('fsm_broke', :state => state, :result => result.inspect)
37
35
  end
38
- rescue MalformedRequest => malformed
39
- Webmachine.render_error(400, request, response, :message => malformed.message)
40
- respond(400)
41
- rescue => e # Handle all exceptions without crashing the server
42
- error_response(e, state)
43
36
  end
37
+ rescue MalformedRequest => malformed
38
+ Webmachine.render_error(400, request, response, :message => malformed.message)
39
+ respond(400)
40
+ rescue => e # Handle all exceptions without crashing the server
41
+ error_response(e, state)
44
42
  end
45
43
 
46
44
  private
@@ -66,6 +64,7 @@ module Webmachine
66
64
  Webmachine.render_error(500, request, response)
67
65
  respond(500)
68
66
  end
69
- end
70
- end
71
- end
67
+
68
+ end # class FSM
69
+ end # module Decision
70
+ end # module Webmachine
@@ -99,6 +99,6 @@ module Webmachine
99
99
  response.headers['Last-Modified'] = modified.httpdate
100
100
  end
101
101
  end
102
- end
103
- end
104
- end
102
+ end # module Helpers
103
+ end # module Decision
104
+ end # module Webmachine
@@ -1,23 +1,31 @@
1
+ require 'forwardable'
1
2
  require 'webmachine/decision'
2
3
  require 'webmachine/dispatcher/route'
3
4
 
4
5
  module Webmachine
5
6
  # Handles dispatching incoming requests to the proper registered
6
7
  # resources and initializing the decision logic.
7
- module Dispatcher
8
- extend self
9
- @routes = []
8
+ class Dispatcher
9
+ # @return [Array<Route>] the list of routes that will be
10
+ # dispatched to
11
+ # @see #add_route
12
+ attr_reader :routes
13
+
14
+ # Initialize a Dispatcher instance
15
+ def initialize
16
+ @routes = []
17
+ end
10
18
 
11
19
  # Adds a route to the dispatch list. Routes will be matched in the
12
20
  # order they are added.
13
21
  # @see Route#new
14
- def add_route(*args)
15
- route = Route.new(*args)
22
+ def add_route(*args, &block)
23
+ route = Route.new(*args, &block)
16
24
  @routes << route
17
25
  route
18
26
  end
19
27
  alias :add :add_route
20
-
28
+
21
29
  # Dispatches a request to the appropriate {Resource} in the
22
30
  # dispatch list. If a matching resource is not found, a "404 Not
23
31
  # Found" will be rendered.
@@ -37,7 +45,7 @@ module Webmachine
37
45
  # Resets, removing all routes. Useful for testing or reloading the
38
46
  # application.
39
47
  def reset
40
- @routes = []
48
+ @routes.clear
41
49
  end
42
50
  end
43
51
 
@@ -45,9 +53,9 @@ module Webmachine
45
53
  # {Webmachine::Dispatcher} for use in adding a number of routes at
46
54
  # once.
47
55
  # @return [Webmachine] self
48
- # @see Webmachine::Dispatcher.add_route
56
+ # @see Webmachine::Dispatcher#add_route
49
57
  def self.routes(&block)
50
- Dispatcher.module_eval(&block)
58
+ application.routes(&block)
51
59
  self
52
60
  end
53
- end
61
+ end # module Webmachine
@@ -2,7 +2,7 @@ require 'webmachine/resource'
2
2
  require 'webmachine/translation'
3
3
 
4
4
  module Webmachine
5
- module Dispatcher
5
+ class Dispatcher
6
6
  # Pairs URIs with {Resource} classes in the {Dispatcher}. To
7
7
  # create routes, use {Dispatcher#add_route}.
8
8
  class Route
@@ -14,24 +14,60 @@ module Webmachine
14
14
  # used to define this route (see #initialize).
15
15
  attr_reader :path_spec
16
16
 
17
+ # @return [Array<Proc>] the list of guard blocks used to define this
18
+ # route (see #initialize).
19
+ attr_reader :guards
20
+
17
21
  # When used in a path specification, will match all remaining
18
22
  # segments
19
23
  MATCH_ALL = '*'.freeze
20
24
 
21
25
  # Creates a new Route that will associate a pattern to a
22
26
  # {Resource}.
23
- # @param [Array<String|Symbol>] path_spec a list of path
24
- # segments (String) and identifiers (Symbol) to bind.
25
- # Strings will be simply matched for equality. Symbols in
26
- # the path spec will be extracted into {Request#path_info} for use
27
- # inside your {Resource}. The special segment {MATCH_ALL} will match
28
- # all remaining segments.
29
- # @param [Class] resource the {Resource} to dispatch to
30
- # @param [Hash] bindings additional information to add to
31
- # {Request#path_info} when this route matches
27
+ #
28
+ # @example Standard route
29
+ # Route.new(["*"], MyResource)
30
+ #
31
+ # @example Guarded route
32
+ # Route.new ["/notes"],
33
+ # ->(request) { request.method == "POST" },
34
+ # Resources::Note
35
+ # Route.new ["/notes"], Resources::NoteList
36
+ # Route.new ["/notes", :id], Resources::Note
37
+ # Route.new ["/notes"], Resources::Note do |req|
38
+ # req.query['foo']
39
+ # end
40
+ #
41
+ # @overload initialize(path_spec, *guards, resource, bindings = {}, &block)
42
+ # @param [Array<String|Symbol>] path_spec a list of path
43
+ # segments (String) and identifiers (Symbol) to bind.
44
+ # Strings will be simply matched for equality. Symbols in
45
+ # the path spec will be extracted into {Request#path_info} for use
46
+ # inside your {Resource}. The special segment {MATCH_ALL} will match
47
+ # all remaining segments.
48
+ # @param [Proc] guards optional guard blocks called with the request.
49
+ # @param [Class] resource the {Resource} to dispatch to
50
+ # @param [Hash] bindings additional information to add to
51
+ # {Request#path_info} when this route matches
52
+ # @yield [req] an optional guard block
53
+ # @yieldparam [Request] req the request object
32
54
  # @see Dispatcher#add_route
33
- def initialize(path_spec, resource, bindings={})
34
- @path_spec, @resource, @bindings = path_spec, resource, bindings
55
+ def initialize(path_spec, *args)
56
+ if args.last.is_a? Hash
57
+ bindings = args.pop
58
+ else
59
+ bindings = {}
60
+ end
61
+
62
+ resource = args.pop
63
+ guards = args
64
+ guards << Proc.new if block_given?
65
+
66
+ @path_spec = path_spec
67
+ @guards = guards
68
+ @resource = resource
69
+ @bindings = bindings
70
+
35
71
  raise ArgumentError, t('not_resource_class', :class => resource.name) unless resource < Resource
36
72
  end
37
73
 
@@ -40,7 +76,7 @@ module Webmachine
40
76
  # @param [Reqeust] request the request object
41
77
  def match?(request)
42
78
  tokens = request.uri.path.match(/^\/(.*)/)[1].split('/')
43
- bind(tokens, {})
79
+ bind(tokens, {}) && guards.all? { |guard| guard.call(request) }
44
80
  end
45
81
 
46
82
  # Decorates the request with information about the dispatch
@@ -75,7 +111,7 @@ module Webmachine
75
111
  return false
76
112
  when Symbol === spec.first
77
113
  bindings[spec.first] = tokens.first
78
- when spec.first == tokens.first
114
+ when spec.first == tokens.first
79
115
  else
80
116
  return false
81
117
  end
@@ -84,6 +120,7 @@ module Webmachine
84
120
  depth += 1
85
121
  end
86
122
  end
87
- end
88
- end
89
- end
123
+
124
+ end # class Route
125
+ end # module Dispatcher
126
+ end # module Webmachine
@@ -35,4 +35,4 @@ module Webmachine
35
35
  # the case where a request header is improperly formed. Raising this
36
36
  # exception will result in a 400 response.
37
37
  class MalformedRequest < Error; end
38
- end
38
+ end # module Webmachine
@@ -36,5 +36,5 @@ module Webmachine
36
36
  def transform_key(key)
37
37
  key.to_s.downcase
38
38
  end
39
- end
40
- end
39
+ end # class Headers
40
+ end # module Webmachine