webmachine 0.4.2 → 1.0.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 (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