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.
- data/README.md +70 -7
- data/Rakefile +19 -0
- data/examples/debugger.rb +32 -0
- data/examples/webrick.rb +6 -2
- data/lib/webmachine.rb +2 -0
- data/lib/webmachine/adapters/rack.rb +16 -3
- data/lib/webmachine/adapters/webrick.rb +7 -1
- data/lib/webmachine/application.rb +10 -10
- data/lib/webmachine/cookie.rb +168 -0
- data/lib/webmachine/decision/conneg.rb +1 -1
- data/lib/webmachine/decision/flow.rb +1 -1
- data/lib/webmachine/decision/fsm.rb +19 -12
- data/lib/webmachine/decision/helpers.rb +25 -1
- data/lib/webmachine/dispatcher.rb +34 -5
- data/lib/webmachine/dispatcher/route.rb +2 -0
- data/lib/webmachine/media_type.rb +3 -3
- data/lib/webmachine/request.rb +11 -0
- data/lib/webmachine/resource.rb +3 -1
- data/lib/webmachine/resource/authentication.rb +1 -1
- data/lib/webmachine/resource/callbacks.rb +16 -0
- data/lib/webmachine/resource/tracing.rb +20 -0
- data/lib/webmachine/response.rb +38 -8
- data/lib/webmachine/trace.rb +74 -0
- data/lib/webmachine/trace/fsm.rb +60 -0
- data/lib/webmachine/trace/pstore_trace_store.rb +39 -0
- data/lib/webmachine/trace/resource_proxy.rb +107 -0
- data/lib/webmachine/trace/static/http-headers-status-v3.png +0 -0
- data/lib/webmachine/trace/static/trace.erb +54 -0
- data/lib/webmachine/trace/static/tracelist.erb +14 -0
- data/lib/webmachine/trace/static/wmtrace.css +123 -0
- data/lib/webmachine/trace/static/wmtrace.js +725 -0
- data/lib/webmachine/trace/trace_resource.rb +129 -0
- data/lib/webmachine/version.rb +1 -1
- data/spec/spec_helper.rb +19 -0
- data/spec/webmachine/adapters/rack_spec.rb +77 -41
- data/spec/webmachine/configuration_spec.rb +1 -1
- data/spec/webmachine/cookie_spec.rb +99 -0
- data/spec/webmachine/decision/conneg_spec.rb +9 -8
- data/spec/webmachine/decision/flow_spec.rb +52 -4
- data/spec/webmachine/decision/helpers_spec.rb +36 -6
- data/spec/webmachine/dispatcher_spec.rb +1 -1
- data/spec/webmachine/headers_spec.rb +1 -1
- data/spec/webmachine/media_type_spec.rb +1 -1
- data/spec/webmachine/request_spec.rb +10 -0
- data/spec/webmachine/resource/authentication_spec.rb +3 -3
- data/spec/webmachine/response_spec.rb +45 -0
- data/spec/webmachine/trace/fsm_spec.rb +32 -0
- data/spec/webmachine/trace/resource_proxy_spec.rb +36 -0
- data/spec/webmachine/trace/trace_store_spec.rb +29 -0
- data/spec/webmachine/trace_spec.rb +17 -0
- data/webmachine.gemspec +2 -0
- 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
|
-
|
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
|
-
|
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
|
-
|
61
|
+
ensure_content_length
|
62
|
+
trace_response(response)
|
58
63
|
end
|
59
64
|
|
60
|
-
#
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/webmachine/request.rb
CHANGED
@@ -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]
|
data/lib/webmachine/resource.rb
CHANGED
@@ -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
|
data/lib/webmachine/response.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Webmachine
|
2
2
|
# Represents an HTTP response from Webmachine.
|
3
3
|
class Response
|
4
|
-
# @return [
|
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
|