webmachine 0.4.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|