webmachine 0.4.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/README.md +70 -7
  2. data/Rakefile +19 -0
  3. data/examples/debugger.rb +32 -0
  4. data/examples/webrick.rb +6 -2
  5. data/lib/webmachine.rb +2 -0
  6. data/lib/webmachine/adapters/rack.rb +16 -3
  7. data/lib/webmachine/adapters/webrick.rb +7 -1
  8. data/lib/webmachine/application.rb +10 -10
  9. data/lib/webmachine/cookie.rb +168 -0
  10. data/lib/webmachine/decision/conneg.rb +1 -1
  11. data/lib/webmachine/decision/flow.rb +1 -1
  12. data/lib/webmachine/decision/fsm.rb +19 -12
  13. data/lib/webmachine/decision/helpers.rb +25 -1
  14. data/lib/webmachine/dispatcher.rb +34 -5
  15. data/lib/webmachine/dispatcher/route.rb +2 -0
  16. data/lib/webmachine/media_type.rb +3 -3
  17. data/lib/webmachine/request.rb +11 -0
  18. data/lib/webmachine/resource.rb +3 -1
  19. data/lib/webmachine/resource/authentication.rb +1 -1
  20. data/lib/webmachine/resource/callbacks.rb +16 -0
  21. data/lib/webmachine/resource/tracing.rb +20 -0
  22. data/lib/webmachine/response.rb +38 -8
  23. data/lib/webmachine/trace.rb +74 -0
  24. data/lib/webmachine/trace/fsm.rb +60 -0
  25. data/lib/webmachine/trace/pstore_trace_store.rb +39 -0
  26. data/lib/webmachine/trace/resource_proxy.rb +107 -0
  27. data/lib/webmachine/trace/static/http-headers-status-v3.png +0 -0
  28. data/lib/webmachine/trace/static/trace.erb +54 -0
  29. data/lib/webmachine/trace/static/tracelist.erb +14 -0
  30. data/lib/webmachine/trace/static/wmtrace.css +123 -0
  31. data/lib/webmachine/trace/static/wmtrace.js +725 -0
  32. data/lib/webmachine/trace/trace_resource.rb +129 -0
  33. data/lib/webmachine/version.rb +1 -1
  34. data/spec/spec_helper.rb +19 -0
  35. data/spec/webmachine/adapters/rack_spec.rb +77 -41
  36. data/spec/webmachine/configuration_spec.rb +1 -1
  37. data/spec/webmachine/cookie_spec.rb +99 -0
  38. data/spec/webmachine/decision/conneg_spec.rb +9 -8
  39. data/spec/webmachine/decision/flow_spec.rb +52 -4
  40. data/spec/webmachine/decision/helpers_spec.rb +36 -6
  41. data/spec/webmachine/dispatcher_spec.rb +1 -1
  42. data/spec/webmachine/headers_spec.rb +1 -1
  43. data/spec/webmachine/media_type_spec.rb +1 -1
  44. data/spec/webmachine/request_spec.rb +10 -0
  45. data/spec/webmachine/resource/authentication_spec.rb +3 -3
  46. data/spec/webmachine/response_spec.rb +45 -0
  47. data/spec/webmachine/trace/fsm_spec.rb +32 -0
  48. data/spec/webmachine/trace/resource_proxy_spec.rb +36 -0
  49. data/spec/webmachine/trace/trace_store_spec.rb +29 -0
  50. data/spec/webmachine/trace_spec.rb +17 -0
  51. data/webmachine.gemspec +2 -0
  52. metadata +130 -15
@@ -97,7 +97,7 @@ module Webmachine
97
97
  # @api private
98
98
  def do_choose(choices, header, default)
99
99
  choices = choices.dup.map {|s| s.downcase }
100
- accepted = PriorityList.build(header.split(/\s*,\s/))
100
+ accepted = PriorityList.build(header.split(/\s*,\s*/))
101
101
  default_priority = accepted.priority_of(default)
102
102
  star_priority = accepted.priority_of("*")
103
103
  default_ok = (default_priority.nil? && star_priority != 0.0) || default_priority
@@ -8,7 +8,7 @@ module Webmachine
8
8
  # This module encapsulates all of the decisions in Webmachine's
9
9
  # flow-chart. These invoke {Webmachine::Resource::Callbacks} methods to
10
10
  # determine the appropriate response code, headers, and body for
11
- # the response.
11
+ # the response.
12
12
  #
13
13
  # This module is included into {FSM}, which drives the processing
14
14
  # of the chart.
@@ -16,13 +16,15 @@ module Webmachine
16
16
  def initialize(resource, request, response)
17
17
  @resource, @request, @response = resource, request, response
18
18
  @metadata = {}
19
+ initialize_tracing
19
20
  end
20
21
 
21
22
  # Processes the request, iteratively invoking the decision methods in {Flow}.
22
23
  def run
23
24
  state = Flow::START
25
+ trace_request(request)
24
26
  loop do
25
- response.trace << state
27
+ trace_decision(state)
26
28
  result = send(state)
27
29
  case result
28
30
  when Fixnum # Response code
@@ -37,14 +39,16 @@ module Webmachine
37
39
  rescue MalformedRequest => malformed
38
40
  Webmachine.render_error(400, request, response, :message => malformed.message)
39
41
  respond(400)
40
- rescue => e # Handle all exceptions without crashing the server
41
- error_response(e, state)
42
+ rescue Exception => e # Handle all exceptions without crashing the server
43
+ code = resource.handle_exception(e)
44
+ code = (100...600).include?(code) ? (code) : (500)
45
+ respond(code)
42
46
  end
43
47
 
44
48
  private
49
+
45
50
  def respond(code, headers={})
46
51
  response.headers.merge!(headers)
47
- end_time = Time.now
48
52
  case code
49
53
  when 404
50
54
  Webmachine.render_error(code, request, response)
@@ -54,17 +58,20 @@ module Webmachine
54
58
  end
55
59
  response.code = code
56
60
  resource.finish_request
57
- # TODO: add logging/tracing
61
+ ensure_content_length
62
+ trace_response(response)
58
63
  end
59
64
 
60
- # Renders a 500 error by capturing the exception information.
61
- def error_response(exception, state)
62
- response.error = [exception.message, exception.backtrace].flatten.join("\n ")
63
- response.end_state = state
64
- Webmachine.render_error(500, request, response)
65
- respond(500)
66
- end
65
+ # When tracing is disabled, this does nothing.
66
+ def trace_decision(state); end
67
+ # When tracing is disabled, this does nothing.
68
+ def trace_request(request); end
69
+ # When tracing is disabled, this does nothing.
70
+ def trace_response(response); end
67
71
 
72
+ def initialize_tracing
73
+ extend Trace::FSM if Trace.trace?(resource)
74
+ end
68
75
  end # class FSM
69
76
  end # module Decision
70
77
  end # module Webmachine
@@ -41,7 +41,7 @@ module Webmachine
41
41
  end
42
42
  end
43
43
  if String === response.body
44
- response.headers['Content-Length'] = response.body.respond_to?(:bytesize) ? response.body.bytesize.to_s : response.body.length.to_s
44
+ set_content_length
45
45
  else
46
46
  response.headers.delete 'Content-Length'
47
47
  response.headers['Transfer-Encoding'] = 'chunked'
@@ -99,6 +99,30 @@ module Webmachine
99
99
  response.headers['Last-Modified'] = modified.httpdate
100
100
  end
101
101
  end
102
+
103
+ # Ensures that responses have an appropriate Content-Length
104
+ # header
105
+ def ensure_content_length
106
+ case
107
+ when response.headers['Transfer-Encoding']
108
+ return
109
+ when [204, 304].include?(response.code)
110
+ response.headers.delete 'Content-Length'
111
+ when has_response_body?
112
+ set_content_length
113
+ else
114
+ response.headers['Content-Length'] = '0'
115
+ end
116
+ end
117
+
118
+ # Sets the Content-Length header on the response
119
+ def set_content_length
120
+ if response.body.respond_to?(:bytesize)
121
+ response.headers['Content-Length'] = response.body.bytesize.to_s
122
+ else
123
+ response.headers['Content-Length'] = response.body.length.to_s
124
+ end
125
+ end
102
126
  end # module Helpers
103
127
  end # module Decision
104
128
  end # module Webmachine
@@ -11,9 +11,16 @@ module Webmachine
11
11
  # @see #add_route
12
12
  attr_reader :routes
13
13
 
14
+ # The creator for resources used to process requests.
15
+ # Must respond to call(route, request, response) and return
16
+ # a newly created resource instance.
17
+ attr_accessor :resource_creator
18
+
14
19
  # Initialize a Dispatcher instance
15
- def initialize
20
+ # @param resource_creator Invoked to create resource instances.
21
+ def initialize(resource_creator = method(:create_resource))
16
22
  @routes = []
23
+ @resource_creator = resource_creator
17
24
  end
18
25
 
19
26
  # Adds a route to the dispatch list. Routes will be matched in the
@@ -32,10 +39,7 @@ module Webmachine
32
39
  # @param [Request] request the request object
33
40
  # @param [Response] response the response object
34
41
  def dispatch(request, response)
35
- route = @routes.find {|r| r.match?(request) }
36
- if route
37
- route.apply(request)
38
- resource = route.resource.new(request, response)
42
+ if resource = find_resource(request, response)
39
43
  Webmachine::Decision::FSM.new(resource, request, response).run
40
44
  else
41
45
  Webmachine.render_error(404, request, response)
@@ -47,6 +51,31 @@ module Webmachine
47
51
  def reset
48
52
  @routes.clear
49
53
  end
54
+
55
+ # Find the first resource that matches an incoming request
56
+ # @param [Request] request the request to match
57
+ # @param [Response] response the response for the resource
58
+ def find_resource(request, response)
59
+ if route = find_route(request)
60
+ prepare_resource(route, request, response)
61
+ end
62
+ end
63
+
64
+ # Find the first route that matches an incoming request
65
+ # @param [Request] request the request to match
66
+ def find_route(request)
67
+ @routes.find {|r| r.match?(request) }
68
+ end
69
+
70
+ private
71
+ def prepare_resource(route, request, response)
72
+ route.apply(request)
73
+ @resource_creator.call(route, request, response)
74
+ end
75
+
76
+ def create_resource(route, request, response)
77
+ route.resource.new(request, response)
78
+ end
50
79
  end
51
80
 
52
81
  # Evaluates the passed block in the context of
@@ -6,6 +6,8 @@ module Webmachine
6
6
  # Pairs URIs with {Resource} classes in the {Dispatcher}. To
7
7
  # create routes, use {Dispatcher#add_route}.
8
8
  class Route
9
+ include Webmachine::Translation
10
+
9
11
  # @return [Class] the resource this route will dispatch to, a
10
12
  # subclass of {Resource}
11
13
  attr_reader :resource
@@ -62,7 +62,7 @@ module Webmachine
62
62
  # Detects whether this {MediaType} matches the other {MediaType},
63
63
  # taking into account wildcards. Sub-type parameters are treated
64
64
  # strictly.
65
- # @param [MediaType, String, Array<String,Hash>] other the other type
65
+ # @param [MediaType, String, Array<String,Hash>] other the other type
66
66
  # @return [true,false] whether it is an acceptable match
67
67
  def exact_match?(other)
68
68
  other = self.class.parse(other)
@@ -73,7 +73,7 @@ module Webmachine
73
73
  # other {MediaType}, taking into account wildcards and satisfying
74
74
  # all requested parameters, but allowing this type to have extra
75
75
  # specificity.
76
- # @param [MediaType, String, Array<String,Hash>] other the other type
76
+ # @param [MediaType, String, Array<String,Hash>] other the other type
77
77
  # @return [true,false] whether it is an acceptable match
78
78
  def match?(other)
79
79
  other = self.class.parse(other)
@@ -88,7 +88,7 @@ module Webmachine
88
88
  def params_match?(other)
89
89
  other.all? {|k,v| params[k] == v }
90
90
  end
91
-
91
+
92
92
  # Reconstitutes the type into a String
93
93
  # @return [String] the type as a String
94
94
  def to_s
@@ -68,6 +68,17 @@ module Webmachine
68
68
  @query
69
69
  end
70
70
 
71
+ # The cookies sent in the request.
72
+ #
73
+ # @return [Hash]
74
+ # {} if no Cookies header set
75
+ def cookies
76
+ unless @cookies
77
+ @cookies = Webmachine::Cookie.parse(headers['Cookie'])
78
+ end
79
+ @cookies
80
+ end
81
+
71
82
  # Is this an HTTPS request?
72
83
  #
73
84
  # @return [Boolean]
@@ -1,6 +1,7 @@
1
1
  require 'webmachine/resource/callbacks'
2
2
  require 'webmachine/resource/encodings'
3
3
  require 'webmachine/resource/authentication'
4
+ require 'webmachine/resource/tracing'
4
5
 
5
6
  module Webmachine
6
7
  # Resource is the primary building block of Webmachine applications,
@@ -21,6 +22,7 @@ module Webmachine
21
22
  class Resource
22
23
  include Callbacks
23
24
  include Encodings
25
+ include Tracing
24
26
 
25
27
  attr_reader :request, :response
26
28
 
@@ -38,7 +40,7 @@ module Webmachine
38
40
  instance.send :initialize
39
41
  instance
40
42
  end
41
-
43
+
42
44
  private
43
45
  # When no specific charsets are provided, this acts as an identity
44
46
  # on the response body. Probably deserves some refactoring.
@@ -11,7 +11,7 @@ module Webmachine
11
11
  # A simple implementation of HTTP Basic auth. Call this from the
12
12
  # {Webmachine::Resource::Callbacks#is_authorized?} callback,
13
13
  # giving it a block which will be yielded the username and
14
- # password and return true or false.
14
+ # password and return true or false.
15
15
  # @param [String] header the value of the Authentication request
16
16
  # header, passed to the {Callbacks#is_authorized?} callback.
17
17
  # @param [String] realm the "realm", or description of the
@@ -360,6 +360,22 @@ module Webmachine
360
360
  # @api callback
361
361
  def finish_request; end
362
362
 
363
+ #
364
+ # This method is called when an exception is raised within a subclass of
365
+ # {Webmachine::Resource}.
366
+ #
367
+ # @param [Exception] e
368
+ # The exception.
369
+ #
370
+ # @return [void]
371
+ #
372
+ # @api callback
373
+ #
374
+ def handle_exception(e)
375
+ response.error = [e.message, e.backtrace].flatten.join("\n ")
376
+ Webmachine.render_error(500, request, response)
377
+ end
378
+
363
379
  # This method is called when verifying the Content-MD5 header
364
380
  # against the request body. To do your own validation, implement
365
381
  # it in this callback, returning true or false. To bypass header
@@ -0,0 +1,20 @@
1
+ module Webmachine
2
+ class Resource
3
+ # Contains {Resource} methods related to the visual debugger.
4
+ module Tracing
5
+ # Whether this resource should be traced. By default, tracing is
6
+ # disabled, but you can override it by setting the @trace
7
+ # instance variable in the initialize method, or by overriding
8
+ # this method. When enabled, traces can be visualized using the
9
+ # web debugging interface.
10
+ # @example
11
+ # def initialize
12
+ # @trace = true
13
+ # end
14
+ # @api callback
15
+ def trace?
16
+ !!@trace
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,7 +1,7 @@
1
1
  module Webmachine
2
2
  # Represents an HTTP response from Webmachine.
3
3
  class Response
4
- # @return [Hash] Response headers that will be sent to the client
4
+ # @return [HeaderHash] Response headers that will be sent to the client
5
5
  attr_reader :headers
6
6
 
7
7
  # @return [Fixnum] The HTTP status code of the response
@@ -15,21 +15,17 @@ module Webmachine
15
15
 
16
16
  # @return [Array] the list of states that were traversed
17
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
18
 
23
19
  # @return [String] The error message when responding with an error
24
20
  # code
25
21
  attr_accessor :error
26
-
22
+
27
23
  # Creates a new Response object with the appropriate defaults.
28
24
  def initialize
29
- @headers = {}
25
+ @headers = HeaderHash.new
30
26
  @trace = []
31
27
  self.code = 200
32
- self.redirect = false
28
+ self.redirect = false
33
29
  end
34
30
 
35
31
  # Indicate that the response should be a redirect. This is only
@@ -44,8 +40,42 @@ module Webmachine
44
40
  self.redirect = true
45
41
  end
46
42
 
43
+ # Set a cookie for the response.
44
+ # @param [String, Symbol] name the name of the cookie
45
+ # @param [String] value the value of the cookie
46
+ # @param [Hash] attributes for the cookie. See RFC2109.
47
+ def set_cookie(name, value, attributes = {})
48
+ cookie = Webmachine::Cookie.new(name, value).to_s
49
+ case headers['Set-Cookie']
50
+ when nil
51
+ headers['Set-Cookie'] = cookie
52
+ when String
53
+ headers['Set-Cookie'] = [headers['Set-Cookie'], cookie]
54
+ when Array
55
+ headers['Set-Cookie'] = headers['Set-Cookie'] + cookie
56
+ end
57
+ end
58
+
47
59
  alias :is_redirect? :redirect
48
60
  alias :redirect_to :do_redirect
49
61
 
62
+ # A {Hash} that can flatten array values into single values with a separator
63
+ class HeaderHash < ::Hash
64
+ # Return a new array with any {Array} values combined with the separator
65
+ # @param [String] The separator used to join Array values
66
+ # @return [HeaderHash] A new {HeaderHash} with Array values flattened
67
+ def flattened(separator = ',')
68
+ Hash[self.collect { |k,v|
69
+ case v
70
+ when Array
71
+ [k,v.join(separator)]
72
+ else
73
+ [k,v]
74
+ end
75
+ }]
76
+
77
+ end
78
+ end
79
+
50
80
  end # class Response
51
81
  end # module Webmachine
@@ -0,0 +1,74 @@
1
+ require 'webmachine/trace/resource_proxy'
2
+ require 'webmachine/trace/fsm'
3
+ require 'webmachine/trace/pstore_trace_store'
4
+ require 'webmachine/trace/trace_resource'
5
+
6
+ module Webmachine
7
+ # Contains means to enable the Webmachine visual debugger.
8
+ module Trace
9
+ module_function
10
+ # Classes that implement storage for visual debugger traces.
11
+ TRACE_STORES = {
12
+ :memory => Hash,
13
+ :pstore => PStoreTraceStore
14
+ }
15
+
16
+ # Determines whether this resource instance should be traced.
17
+ # @param [Webmachine::Resource] resource a resource instance
18
+ # @return [true, false] whether to trace the resource
19
+ def trace?(resource)
20
+ # For now this defers to the resource to enable tracing in the
21
+ # initialize method. At a later time, we can add tracing at the
22
+ # Application level, perhaps.
23
+ resource.trace?
24
+ end
25
+
26
+ # Records a trace from a processed request. This is automatically
27
+ # called by {Webmachine::Trace::ResourceProxy} when finishing the
28
+ # request.
29
+ # @api private
30
+ # @param [String] key a unique identifier for the request
31
+ # @param [Array] trace the raw trace generated by processing the
32
+ # request
33
+ def record(key, trace)
34
+ trace_store[key] = trace
35
+ end
36
+
37
+ # Retrieves keys of traces that have been recorded. This is used
38
+ # to present a list of available traces in the visual debugger.
39
+ # @api private
40
+ # @return [Array<String>] a list of recorded traces
41
+ def traces
42
+ trace_store.keys
43
+ end
44
+
45
+ # Fetches a given trace from the trace store. This is used to
46
+ # send specific trace information to the visual debugger.
47
+ # @api private
48
+ # @param [String] key the trace's key
49
+ # @return [Array] the raw trace
50
+ def fetch(key)
51
+ trace_store.fetch(key)
52
+ end
53
+
54
+ # Sets the trace storage method. The first parameter should be a
55
+ # Symbol, followed by any additional options for the
56
+ # store. Defaults to :memory, which is an in-memory Hash.
57
+ # @example
58
+ # Webmachine::Trace.trace_store = :pstore, "/tmp/webmachine.trace"
59
+ def trace_store=(*args)
60
+ @trace_store = nil
61
+ @trace_store_opts = args
62
+ end
63
+ self.trace_store = :memory
64
+
65
+ def trace_store
66
+ @trace_store ||= begin
67
+ opts = Array(@trace_store_opts).dup
68
+ type = opts.shift
69
+ TRACE_STORES[type].new(*opts)
70
+ end
71
+ end
72
+ private :trace_store
73
+ end
74
+ end