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
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
module Webmachine
|
5
|
+
module Trace
|
6
|
+
# Implements the user-interface of the visual debugger. This
|
7
|
+
# includes serving the static files (the PNG flow diagram, CSS and
|
8
|
+
# JS for the UI) and the HTML for the individual traces.
|
9
|
+
class TraceResource < Resource
|
10
|
+
|
11
|
+
MAP_EXTERNAL = %w{static map.png}
|
12
|
+
MAP_FILE = File.expand_path("../static/http-headers-status-v3.png", __FILE__)
|
13
|
+
SCRIPT_EXTERNAL = %w{static wmtrace.js}
|
14
|
+
SCRIPT_FILE = File.expand_path("../#{SCRIPT_EXTERNAL.join '/'}", __FILE__)
|
15
|
+
STYLE_EXTERNAL = %w{static wmtrace.css}
|
16
|
+
STYLE_FILE = File.expand_path("../#{STYLE_EXTERNAL.join '/'}", __FILE__)
|
17
|
+
TRACELIST_ERB = File.expand_path("../static/tracelist.erb", __FILE__)
|
18
|
+
TRACE_ERB = File.expand_path("../static/trace.erb", __FILE__)
|
19
|
+
|
20
|
+
# The ERB template for the trace list
|
21
|
+
def self.tracelist
|
22
|
+
@@tracelist ||= ERB.new(File.read(TRACELIST_ERB))
|
23
|
+
end
|
24
|
+
|
25
|
+
# The ERB template for a single trace
|
26
|
+
def self.trace
|
27
|
+
@@trace ||= ERB.new(File.read(TRACE_ERB))
|
28
|
+
end
|
29
|
+
|
30
|
+
def content_types_provided
|
31
|
+
case request.path_tokens
|
32
|
+
when []
|
33
|
+
[["text/html", :produce_list]]
|
34
|
+
when MAP_EXTERNAL
|
35
|
+
[["image/png", :produce_file]]
|
36
|
+
when SCRIPT_EXTERNAL
|
37
|
+
[["text/javascript", :produce_file]]
|
38
|
+
when STYLE_EXTERNAL
|
39
|
+
[["text/css", :produce_file]]
|
40
|
+
else
|
41
|
+
[["text/html", :produce_trace]]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def resource_exists?
|
46
|
+
case request.path_tokens
|
47
|
+
when []
|
48
|
+
true
|
49
|
+
when MAP_EXTERNAL
|
50
|
+
@file = MAP_FILE
|
51
|
+
File.exist?(MAP_FILE)
|
52
|
+
when SCRIPT_EXTERNAL
|
53
|
+
@file = SCRIPT_FILE
|
54
|
+
File.exist?(SCRIPT_FILE)
|
55
|
+
when STYLE_EXTERNAL
|
56
|
+
@file = STYLE_FILE
|
57
|
+
File.exist?(STYLE_FILE)
|
58
|
+
else
|
59
|
+
@trace = request.path_tokens.first
|
60
|
+
Trace.traces.include? @trace
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def last_modified
|
65
|
+
File.mtime(@file) if @file
|
66
|
+
end
|
67
|
+
|
68
|
+
def expires
|
69
|
+
(Time.now + 30 * 86400).utc if @file
|
70
|
+
end
|
71
|
+
|
72
|
+
def produce_file
|
73
|
+
# TODO: Add support for IO objects as response bodies,
|
74
|
+
# allowing server optimizations like sendfile or chunked
|
75
|
+
# downloads
|
76
|
+
File.read(@file)
|
77
|
+
end
|
78
|
+
|
79
|
+
def produce_list
|
80
|
+
traces = Trace.traces
|
81
|
+
self.class.tracelist.result(binding)
|
82
|
+
end
|
83
|
+
|
84
|
+
def produce_trace
|
85
|
+
data = Trace.fetch(@trace)
|
86
|
+
treq, tres, trace = encode_trace(data)
|
87
|
+
name = @trace
|
88
|
+
self.class.trace.result(binding)
|
89
|
+
end
|
90
|
+
|
91
|
+
def encode_trace(data)
|
92
|
+
data = data.dup
|
93
|
+
# Request is first, response is last
|
94
|
+
treq = data.shift.dup
|
95
|
+
tres = data.pop.dup
|
96
|
+
treq.delete :type
|
97
|
+
tres.delete :type
|
98
|
+
[ MultiJson.dump(treq), MultiJson.dump(tres), MultiJson.dump(encode_decisions(data)) ]
|
99
|
+
end
|
100
|
+
|
101
|
+
def encode_decisions(decisions)
|
102
|
+
decisions.inject([]) do |list, event|
|
103
|
+
case event[:type]
|
104
|
+
when :decision
|
105
|
+
# Don't produce new decisions for sub-steps in the graph
|
106
|
+
unless event[:decision].to_s =~ /[a-z]$/
|
107
|
+
list << {'d' => event[:decision], 'calls' => []}
|
108
|
+
end
|
109
|
+
when :attempt
|
110
|
+
list.last['calls'] << {
|
111
|
+
"call" => event[:name],
|
112
|
+
"source" => event[:source],
|
113
|
+
"input" => event[:args] && event[:args].inspect
|
114
|
+
}
|
115
|
+
when :result
|
116
|
+
list.last['calls'].last['output'] = event[:value].inspect
|
117
|
+
when :exception
|
118
|
+
list.last['calls'].last['exception'] = {
|
119
|
+
'class' => event[:class],
|
120
|
+
'backtrace' => event[:backtrace].join("\n"),
|
121
|
+
'message' => event[:message]
|
122
|
+
}
|
123
|
+
end
|
124
|
+
list
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/webmachine/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -16,3 +16,22 @@ RSpec.configure do |config|
|
|
16
16
|
config.order = :random
|
17
17
|
end
|
18
18
|
end
|
19
|
+
|
20
|
+
# For use in specs that need a fully initialized resource
|
21
|
+
shared_context "default resource" do
|
22
|
+
let(:method) { 'GET' }
|
23
|
+
let(:uri) { URI.parse("http://localhost/") }
|
24
|
+
let(:headers) { Webmachine::Headers.new }
|
25
|
+
let(:body) { "" }
|
26
|
+
let(:request) { Webmachine::Request.new(method, uri, headers, body) }
|
27
|
+
let(:response) { Webmachine::Response.new }
|
28
|
+
|
29
|
+
let(:resource_class) do
|
30
|
+
Class.new(Webmachine::Resource) do
|
31
|
+
def to_html
|
32
|
+
"<html><body>Hello, world!</body></html>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
let(:resource) { resource_class.new(request, response) }
|
37
|
+
end
|
@@ -1,19 +1,42 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'webmachine/adapters/rack'
|
3
3
|
require 'rack'
|
4
|
+
require 'rack/test'
|
4
5
|
|
5
6
|
module Test
|
6
7
|
class Resource < Webmachine::Resource
|
7
8
|
def allowed_methods
|
8
|
-
["GET", "PUT"]
|
9
|
+
["GET", "PUT", "POST"]
|
9
10
|
end
|
10
11
|
|
11
12
|
def content_types_accepted
|
12
13
|
[["application/json", :from_json]]
|
13
14
|
end
|
14
15
|
|
16
|
+
def content_types_provided
|
17
|
+
[
|
18
|
+
["text/html", :to_html],
|
19
|
+
["application/vnd.webmachine.streaming+enum", :to_enum_stream],
|
20
|
+
["application/vnd.webmachine.streaming+proc", :to_proc_stream]
|
21
|
+
]
|
22
|
+
end
|
23
|
+
|
24
|
+
def process_post
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
15
28
|
def to_html
|
16
|
-
|
29
|
+
response.set_cookie('cookie', 'monster')
|
30
|
+
response.set_cookie('rodeo', 'clown')
|
31
|
+
"<html><body>#{request.cookies['string'] || 'testing'}</body></html>"
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_enum_stream
|
35
|
+
%w{Hello, World!}
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_proc_stream
|
39
|
+
Proc.new { "Stream" }
|
17
40
|
end
|
18
41
|
|
19
42
|
def from_json; end
|
@@ -21,29 +44,14 @@ module Test
|
|
21
44
|
end
|
22
45
|
|
23
46
|
describe Webmachine::Adapters::Rack do
|
24
|
-
|
25
|
-
{ "REQUEST_METHOD" => "GET",
|
26
|
-
"SCRIPT_NAME" => "",
|
27
|
-
"PATH_INFO" => "/test",
|
28
|
-
"QUERY_STRING" => "",
|
29
|
-
"SERVER_NAME" => "test.server",
|
30
|
-
"SERVER_PORT" => 8080,
|
31
|
-
"rack.version" => Rack::VERSION,
|
32
|
-
"rack.url_scheme" => "http",
|
33
|
-
"rack.input" => StringIO.new("Hello, World!"),
|
34
|
-
"rack.errors" => StringIO.new,
|
35
|
-
"rack.multithread" => false,
|
36
|
-
"rack.multiprocess" => true,
|
37
|
-
"rack.run_once" => false }
|
38
|
-
end
|
47
|
+
include Rack::Test::Methods
|
39
48
|
|
40
49
|
let(:configuration) { Webmachine::Configuration.new('0.0.0.0', 8080, :Rack, {}) }
|
41
50
|
let(:dispatcher) { Webmachine::Dispatcher.new }
|
42
51
|
let(:adapter) do
|
43
52
|
described_class.new(configuration, dispatcher)
|
44
53
|
end
|
45
|
-
|
46
|
-
subject { adapter }
|
54
|
+
let(:app) { adapter }
|
47
55
|
|
48
56
|
before do
|
49
57
|
dispatcher.add_route ['test'], Test::Resource
|
@@ -71,17 +79,17 @@ describe Webmachine::Adapters::Rack do
|
|
71
79
|
end
|
72
80
|
|
73
81
|
it "should proxy request to webmachine" do
|
74
|
-
|
75
|
-
|
76
|
-
headers["Content-Type"].should == "text/html"
|
77
|
-
body.should
|
82
|
+
get "/test"
|
83
|
+
last_response.status.should == 200
|
84
|
+
last_response.headers["Content-Type"].should == "text/html"
|
85
|
+
last_response.body.should == "<html><body>testing</body></html>"
|
78
86
|
end
|
79
87
|
|
80
88
|
it "should build a string-like request body" do
|
81
89
|
dispatcher.should_receive(:dispatch) do |request, response|
|
82
90
|
request.body.to_s.should eq("Hello, World!")
|
83
91
|
end
|
84
|
-
|
92
|
+
request "/test", :method => "GET", :input => "Hello, World!"
|
85
93
|
end
|
86
94
|
|
87
95
|
it "should build an enumerable request body" do
|
@@ -89,35 +97,63 @@ describe Webmachine::Adapters::Rack do
|
|
89
97
|
dispatcher.should_receive(:dispatch) do |request, response|
|
90
98
|
request.body.each { |chunk| chunks << chunk }
|
91
99
|
end
|
92
|
-
|
100
|
+
request "/test", :method => "GET", :input => "Hello, World!"
|
93
101
|
chunks.join.should eq("Hello, World!")
|
94
102
|
end
|
95
103
|
|
96
104
|
it "should understand the Content-Type header correctly" do
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
code.should == 204
|
105
|
+
header "CONTENT_TYPE", "application/json"
|
106
|
+
put "/test"
|
107
|
+
last_response.status.should == 204
|
101
108
|
end
|
102
109
|
|
103
110
|
it "should set Server header" do
|
104
|
-
|
105
|
-
headers.should have_key
|
111
|
+
get "/test"
|
112
|
+
last_response.headers.should have_key("Server")
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should set Set-Cookie header" do
|
116
|
+
get "/test"
|
117
|
+
# Yes, Rack expects multiple values for a given cookie to be
|
118
|
+
# \n separated.
|
119
|
+
last_response.headers["Set-Cookie"].should == "cookie=monster\nrodeo=clown"
|
106
120
|
end
|
107
121
|
|
108
122
|
it "should handle non-success correctly" do
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
headers["Content-Type"].should == "text/html"
|
123
|
+
get "/missing"
|
124
|
+
last_response.status.should == 404
|
125
|
+
last_response.content_type.should == "text/html"
|
113
126
|
end
|
114
127
|
|
115
128
|
it "should handle empty bodies correctly" do
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
headers.should_not have_key
|
120
|
-
headers.should_not have_key
|
121
|
-
body.should ==
|
129
|
+
header "CONTENT_TYPE", "application/json"
|
130
|
+
post "/test"
|
131
|
+
last_response.status.should == 204
|
132
|
+
last_response.headers.should_not have_key("Content-Type")
|
133
|
+
last_response.headers.should_not have_key("Content-Length")
|
134
|
+
last_response.body.should == ""
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should handle cookies correctly" do
|
138
|
+
header "COOKIE", "string=123"
|
139
|
+
get "/test"
|
140
|
+
last_response.status.should == 200
|
141
|
+
last_response.body.should == "<html><body>123</body></html>"
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should handle streaming enumerable response bodies" do
|
145
|
+
header "ACCEPT", "application/vnd.webmachine.streaming+enum"
|
146
|
+
get "/test"
|
147
|
+
last_response.status.should == 200
|
148
|
+
last_response.headers["Transfer-Encoding"].should == "chunked"
|
149
|
+
last_response.body.split("\r\n").should == %W{6 Hello, 6 World! 0}
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should handle streaming callable response bodies" do
|
153
|
+
header "ACCEPT", "application/vnd.webmachine.streaming+proc"
|
154
|
+
get "/test"
|
155
|
+
last_response.status.should == 200
|
156
|
+
last_response.headers["Transfer-Encoding"].should == "chunked"
|
157
|
+
last_response.body.split("\r\n").should == %W{6 Stream 0}
|
122
158
|
end
|
123
159
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Webmachine::Cookie do
|
4
|
+
describe "creating a cookie" do
|
5
|
+
let(:name) { "monster" }
|
6
|
+
let(:value) { "mash" }
|
7
|
+
let(:attributes) { {} }
|
8
|
+
|
9
|
+
let(:cookie) { Webmachine::Cookie.new(name, value, attributes) }
|
10
|
+
|
11
|
+
subject { cookie }
|
12
|
+
|
13
|
+
its(:name) { should == name }
|
14
|
+
its(:value) { should == value }
|
15
|
+
|
16
|
+
its(:to_s) { should == "monster=mash" }
|
17
|
+
|
18
|
+
describe "a cookie with whitespace in name and value" do
|
19
|
+
let(:name) { "cookie name" }
|
20
|
+
let(:value) { "cookie value" }
|
21
|
+
|
22
|
+
its(:to_s) { should == "cookie+name=cookie+value" }
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "a cookie with attributes set" do
|
26
|
+
let(:domain) { "www.server.com" }
|
27
|
+
let(:path) { "/" }
|
28
|
+
let(:comment) { "comment with spaces" }
|
29
|
+
let(:version) { 1 }
|
30
|
+
let(:maxage) { 60 }
|
31
|
+
let(:expires) { Time.gm(2010,3,14, 3, 14, 0) }
|
32
|
+
let(:attributes) {
|
33
|
+
{
|
34
|
+
:comment => comment,
|
35
|
+
:domain => domain,
|
36
|
+
:path => path,
|
37
|
+
:secure => true,
|
38
|
+
:httponly => true,
|
39
|
+
:version => version,
|
40
|
+
:maxage => maxage,
|
41
|
+
:expires => expires
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
its(:secure?) { should be true }
|
46
|
+
its(:http_only?) { should be true }
|
47
|
+
its(:comment) { should == comment }
|
48
|
+
its(:domain) { should == domain }
|
49
|
+
its(:path) { should == path }
|
50
|
+
its(:version) { should == version }
|
51
|
+
its(:maxage) { should == maxage }
|
52
|
+
its(:expires) { should == expires }
|
53
|
+
|
54
|
+
it "should include the attributes in its string version" do
|
55
|
+
str = subject.to_s
|
56
|
+
str.should include "Secure"
|
57
|
+
str.should include "HttpOnly"
|
58
|
+
str.should include "Comment=comment+with+spaces"
|
59
|
+
str.should include "Domain=www.server.com"
|
60
|
+
str.should include "Path=/"
|
61
|
+
str.should include "Version=1"
|
62
|
+
str.should include "MaxAge=60"
|
63
|
+
str.should include "Expires=Sun, 14-Mar-2010 03:14:00 GMT"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "parsing a cookie parameter" do
|
69
|
+
let(:str) { "cookie = monster" }
|
70
|
+
|
71
|
+
subject { Webmachine::Cookie.parse(str) }
|
72
|
+
|
73
|
+
it("should have the cookie") { subject.should == { "cookie" => "monster" } }
|
74
|
+
|
75
|
+
describe "parsing multiple cookie parameters" do
|
76
|
+
let(:str) { "cookie=monster; monster=mash" }
|
77
|
+
|
78
|
+
it("should have both cookies") { subject.should == { "cookie" => "monster", "monster" => "mash" } }
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "parsing an encoded cookie" do
|
82
|
+
let(:str) { "cookie=yum+yum" }
|
83
|
+
|
84
|
+
it("should decode the cookie") { subject.should == { "cookie" => "yum yum" } }
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "parsing nil" do
|
88
|
+
let(:str) { nil }
|
89
|
+
|
90
|
+
it("should return empty hash") { subject.should == {} }
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "parsing duplicate cookies" do
|
94
|
+
let(:str) { "cookie=monster; cookie=yum+yum" }
|
95
|
+
|
96
|
+
it("should return the first instance of the cookie") { subject.should == { "cookie" => "monster" } }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|