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
@@ -0,0 +1,60 @@
1
+ module Webmachine
2
+ module Trace
3
+ # This module is injected into {Webmachine::Decision::FSM} when
4
+ # tracing is enabled for a resource, enabling the capturing of
5
+ # traces.
6
+ module FSM
7
+ # Adds the request to the trace.
8
+ # @param [Webmachine::Request] request the request to be traced
9
+ def trace_request(request)
10
+ response.trace << {
11
+ :type => :request,
12
+ :method => request.method,
13
+ :path => request.uri.request_uri.to_s,
14
+ :headers => request.headers,
15
+ :body => request.body.to_s
16
+ }
17
+ end
18
+
19
+ # Adds the response to the trace.
20
+ # @param [Webmachine::Response] response the response to be traced
21
+ def trace_response(response)
22
+ response.trace << {
23
+ :type => :response,
24
+ :code => response.code.to_s,
25
+ :headers => response.headers,
26
+ :body => trace_response_body(response.body)
27
+ }
28
+ end
29
+
30
+ # Adds a decision to the trace.
31
+ # @param [Symbol] decision the decision being processed
32
+ def trace_decision(decision)
33
+ response.trace << {:type => :decision, :decision => decision}
34
+ end
35
+
36
+ # Overrides the default resource accessor so that incoming
37
+ # callbacks are traced.
38
+ def resource
39
+ @resource_proxy ||= ResourceProxy.new(@resource)
40
+ end
41
+
42
+ private
43
+ # Works around streaming encoders where possible
44
+ def trace_response_body(body)
45
+ case body
46
+ when FiberEncoder
47
+ # TODO: figure out how to properly rewind or replay the
48
+ # fiber
49
+ body.inspect
50
+ when EnumerableEncoder
51
+ body.body.join
52
+ when CallableEncoder
53
+ body.body.call.to_s
54
+ else
55
+ body.to_s
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,39 @@
1
+ require 'pstore'
2
+
3
+ module Webmachine
4
+ module Trace
5
+ # Implements a trace storage using PStore from Ruby's standard
6
+ # library. To use this trace store, specify the :pstore engine
7
+ # and a path where it can store traces:
8
+ # @example
9
+ # Webmachine::Trace.trace_store = :pstore, "/tmp/webmachine.trace"
10
+ class PStoreTraceStore
11
+ # @api private
12
+ # @param [String] path where to store traces in a PStore
13
+ def initialize(path)
14
+ @pstore = PStore.new(path)
15
+ end
16
+
17
+ # Lists the recorded traces
18
+ # @api private
19
+ # @return [Array] a list of recorded traces
20
+ def keys
21
+ @pstore.transaction(true) { @pstore.roots }
22
+ end
23
+
24
+ # Fetches a trace from the store
25
+ # @api private
26
+ # @param [String] key the trace to fetch
27
+ # @return [Array] a raw trace
28
+ def fetch(key)
29
+ @pstore.transaction(true) { @pstore[key] }
30
+ end
31
+
32
+ # Records a trace in the store
33
+ # @api private
34
+ def []=(key, trace)
35
+ @pstore.transaction { @pstore[key] = trace }
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,107 @@
1
+ module Webmachine
2
+ module Trace
3
+ # This class is injected into the decision FSM as a stand-in for
4
+ # the resource when tracing is enabled. It proxies all callbacks
5
+ # to the resource so that they get logged in the trace.
6
+ class ResourceProxy
7
+ # @return [Webmachine::Resource] the wrapped resource
8
+ attr_reader :resource
9
+
10
+ # Callback methods that can return data that refers to
11
+ # user-defined callbacks that are not in the canonical set,
12
+ # including body-producing or accepting methods, encoders and
13
+ # charsetters.
14
+ CALLBACK_REFERRERS = [:content_types_accepted, :content_types_provided,
15
+ :encodings_provided, :charsets_provided]
16
+
17
+ # Creates a {ResourceProxy} that decorates the passed
18
+ # {Webmachine::Resource} such that callbacks invoked by the
19
+ # {Webmachine::Decision::FSM} will be logged in the response's
20
+ # trace.
21
+ def initialize(resource)
22
+ @resource = resource
23
+ @dynamic_callbacks = Module.new
24
+ extend @dynamic_callbacks
25
+ end
26
+
27
+ # Create wrapper methods for every exposed callback
28
+ Webmachine::Resource::Callbacks.instance_methods(false).each do |c|
29
+ define_method c do |*args|
30
+ proxy_callback c, *args
31
+ end
32
+ end
33
+
34
+ def charset_nop(*args)
35
+ proxy_callback :charset_nop, *args
36
+ end
37
+
38
+ # Calls the resource's finish_request method and then commits
39
+ # the trace to separate storage which can be discovered by the
40
+ # debugger.
41
+ def finish_request(*args)
42
+ proxy_callback :finish_request, *args
43
+ ensure
44
+ resource.response.headers['X-Webmachine-Trace-Id'] = object_id.to_s
45
+ Trace.record(object_id.to_s, resource.response.trace)
46
+ end
47
+
48
+ private
49
+ # Proxy a given callback to the inner resource, decorating with traces
50
+ def proxy_callback(callback, *args)
51
+ # Log inputs and attempt
52
+ resource.response.trace << attempt(callback, args)
53
+ # Do the call
54
+ _result = resource.send(callback, *args)
55
+ add_dynamic_callback_proxies(_result) if CALLBACK_REFERRERS.include?(callback.to_sym)
56
+ resource.response.trace << result(_result)
57
+ _result
58
+ rescue => exc
59
+ exc.backtrace.reject! {|s| s.include?(__FILE__) }
60
+ resource.response.trace << exception(exc)
61
+ raise
62
+ end
63
+
64
+ # Creates a log entry for the entry to a resource callback.
65
+ def attempt(callback, args)
66
+ log = {:type => :attempt}
67
+ method = resource.method(callback)
68
+ if method.owner == ::Webmachine::Resource::Callbacks
69
+ log[:name] = "(default)##{method.name}"
70
+ else
71
+ log[:name] = "#{method.owner.name}##{method.name}"
72
+ log[:source] = method.source_location.join(":") if method.respond_to?(:source_location)
73
+ end
74
+ unless args.empty?
75
+ log[:args] = args
76
+ end
77
+ log
78
+ end
79
+
80
+ # Creates a log entry for the result of a resource callback
81
+ def result(result)
82
+ {:type => :result, :value => result}
83
+ end
84
+
85
+ # Creates a log entry for an exception that was raised from a callback
86
+ def exception(e)
87
+ {:type => :exception,
88
+ :class => e.class.name,
89
+ :backtrace => e.backtrace,
90
+ :message => e.message }
91
+ end
92
+
93
+ # Adds proxy methods for callbacks that are dynamically referred to.
94
+ def add_dynamic_callback_proxies(pairs)
95
+ pairs.to_a.each do |(_, m)|
96
+ unless respond_to?(m)
97
+ @dynamic_callbacks.module_eval do
98
+ define_method m do |*args|
99
+ proxy_callback m, *args
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
3
+ <head>
4
+ <title>Webmachine Trace <%= name %></title>
5
+ <link rel="stylesheet" type="text/css" href="static/wmtrace.css" />
6
+ <script type="text/javascript" src="static/wmtrace.js"></script>
7
+ <script type="text/javascript">
8
+ <!--
9
+ var request = <%= treq %>;
10
+ var response = <%= tres %>;
11
+ var trace = <%= trace %>
12
+ -->
13
+ </script>
14
+ </head>
15
+ <body>
16
+ <div id="zoompanel">
17
+ <button id="zoomout">zoom out</button>
18
+ <button id="zoomin">zoom in</button>
19
+ </div>
20
+ <canvas id="v3map" width="3138" height="2184"></canvas>
21
+ <div id="sizetest"></div>
22
+ <div id="preview">
23
+ <div id="previewid"></div>
24
+ <ul id="previewcalls"></ul>
25
+ </div>
26
+ <div id="infopanel">
27
+ <div id="infocontrols">
28
+ <div id="requesttab" class="selectedtab">Q</div>
29
+ <div id="responsetab">R</div>
30
+ <div id="decisiontab">D</div>
31
+ </div>
32
+ <div id="requestdetail">
33
+ <div>
34
+ <span id="requestmethod"></span> <span id="requestpath"></span>
35
+ </div>
36
+ <table id="requestheaders" class="headers"></table>
37
+ <pre id="requestbody"></pre>
38
+ </div>
39
+ <div id="responsedetail">
40
+ <div id="responsecode"></div>
41
+ <table id="responseheaders" class="headers"></table>
42
+ <pre id="responsebody"></pre>
43
+ </div>
44
+ <div id="decisiondetail">
45
+ <div>Decision: <select id="decisionid"></select></div>
46
+ <div>Calls: <select id="decisioncalls"></select></div>
47
+ <div>Input:</div>
48
+ <pre id="callinput"></pre>
49
+ <div>Output:</div>
50
+ <pre id="calloutput"></pre>
51
+ </div>
52
+ </div>
53
+ </body>
54
+ </html>
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
3
+ <head>
4
+ <title>Webmachine Trace List</title>
5
+ </head>
6
+ <body>
7
+ <h1>Traces</h1>
8
+ <ul>
9
+ <% traces.each do |trace| %>
10
+ <li><a href="<%= trace %>"><%= trace %></a></li>
11
+ <% end %>
12
+ </ul>
13
+ </body>
14
+ </html>
@@ -0,0 +1,123 @@
1
+ body {
2
+ margin:0px;
3
+ padding:0px;
4
+ }
5
+
6
+ canvas#v3map {
7
+ margin-top:2em;
8
+ z-index: 1;
9
+ }
10
+
11
+ div#sizetest {
12
+ width:100%;
13
+ }
14
+
15
+ div#zoompanel {
16
+ height:2em;
17
+ position:fixed;
18
+ z-index:10;
19
+ }
20
+
21
+ div#preview {
22
+ position:absolute;
23
+ display:none;
24
+ background:#dddddd;
25
+ border:1px solid #999999;
26
+ }
27
+
28
+ div#preview ul {
29
+ padding: 0px 0px 0px 0.5em;
30
+ margin: 0px;
31
+ list-style: none;
32
+ }
33
+
34
+ div#infopanel {
35
+ z-index:20;
36
+ background:#dddddd;
37
+ position:fixed;
38
+ top:0px;
39
+ right:0px;
40
+ bottom:0px;
41
+ left:75%;
42
+ min-width:30em;
43
+ padding:5px;
44
+ }
45
+
46
+ div#infocontrols {
47
+ position:absolute;
48
+ top:0px;
49
+ bottom:0px;
50
+ left:-5px;
51
+ width:5px;
52
+ background:#999999;
53
+ cursor:ew-resize;
54
+ }
55
+
56
+ div#infocontrols div {
57
+ position:absolute;
58
+ left:-15px;
59
+ width:20px;
60
+ height:49px;
61
+ background:#999999;
62
+ cursor:pointer;
63
+ }
64
+
65
+ div#infocontrols div.selectedtab {
66
+ background:#dddddd;
67
+ border-top: 1px solid #999999;
68
+ border-left: 1px solid #999999;
69
+ border-bottom: 1px solid #999999;
70
+ }
71
+
72
+ div#requesttab {
73
+ top:2px;
74
+ }
75
+
76
+ div#responsetab {
77
+ top:54px;
78
+ }
79
+
80
+ div#decisiontab {
81
+ top:106px;
82
+ }
83
+
84
+ div#requestdetail, div#responsedetail, div#decisiondetail {
85
+ height:100%;
86
+ }
87
+
88
+ div#responsedetail, div#decisiondetail {
89
+ display:none;
90
+ }
91
+
92
+ div#infopanel ul {
93
+ list-style:none;
94
+ padding-left:0px;
95
+ height:5em;
96
+ overflow-y:scroll;
97
+ }
98
+
99
+ pre {
100
+ height:40%;
101
+ overflow: auto;
102
+ }
103
+
104
+ table.headers {
105
+ border-collapse: collapse;
106
+ margin: 10px 20px 10px 0;
107
+ }
108
+
109
+ table.headers th {
110
+ text-align: right ! important;
111
+ text-transform: capitalize;
112
+ white-space: nowrap;
113
+ vertical-align: top;
114
+ }
115
+
116
+ table.headers td, table.headers th {
117
+ padding: 5px 3px;
118
+ }
119
+
120
+ div#responsebody, div#requestbody {
121
+ height:70%;
122
+ overflow-y:scroll;
123
+ }
@@ -0,0 +1,725 @@
1
+ var HIGHLIGHT = '#cc00cc';
2
+ var REGULAR = '#666666';
3
+
4
+ var cols = {
5
+ 'a':173,
6
+ 'b':325,
7
+ 'c':589,
8
+ 'd':797,
9
+ 'e':1005,
10
+ 'f':1195,
11
+ 'g':1402,
12
+ 'gg':1515,
13
+ 'h':1572,
14
+ 'i':1799,
15
+ 'j':1893,
16
+ 'k':1988,
17
+ 'l':2157,
18
+ 'll':2346,
19
+ 'm':2403,
20
+ 'mm':2535,
21
+ 'n':2554,
22
+ 'o':2649,
23
+ 'oo':2781,
24
+ 'ooo':2801,
25
+ 'p':2894,
26
+ 'q':3007
27
+ };
28
+
29
+ var rows = {
30
+ '1':221,
31
+ '2':298,
32
+ '3':373,
33
+ '4':448,
34
+ '5':524,
35
+ '6':599,
36
+ '7':675,
37
+ '8':751,
38
+ '9':826,
39
+ '10':902,
40
+ '11':977,
41
+ '12':1053,
42
+ '13':1129,
43
+ '14':1204,
44
+ '15':1280,
45
+ '16':1355,
46
+ '17':1431,
47
+ '18':1506,
48
+ '19':1583,
49
+ '20':1658,
50
+ '21':1734,
51
+ '22':1809,
52
+ '23':1885,
53
+ '24':1961,
54
+ '25':2036,
55
+ '26':2112
56
+ };
57
+
58
+ var edges = {
59
+ 'b14b13':['b14','b13'],
60
+
61
+ 'b13b12':['b13','b12'],
62
+ 'b13503':['b13','503'],
63
+
64
+ 'b12b11':['b12','b11'],
65
+ 'b12501':['b12','501'],
66
+
67
+ 'b11b10':['b11','b10'],
68
+ 'b11414':['b11','414'],
69
+
70
+ 'b10b9':['b10','b9'],
71
+ 'b10405':['b10','405'],
72
+
73
+ 'b9b8':['b9','b8'],
74
+ 'b9400':['b9','400'],
75
+
76
+ 'b8b7':['b8','b7'],
77
+ 'b8401':['b8','401'],
78
+
79
+ 'b7b6':['b7','b6'],
80
+ 'b7403':['b7','403'],
81
+
82
+ 'b6b5':['b6','b5'],
83
+ 'b6501':['b6','501a'],
84
+
85
+ 'b5b4':['b5','b4'],
86
+ 'b5415':['b5','415'],
87
+
88
+ 'b4b3':['b4','b3'],
89
+ 'b4413':['b4','b4'],
90
+
91
+ 'b3c3':['b3','c3'],
92
+ 'b3200':['b3','200'],
93
+
94
+ 'c3c4':['c3','c4'],
95
+ 'c3d4':['c3','d3','d4'],
96
+
97
+ 'c4d4':['c4','d4'],
98
+ 'c4406':['c4','406'],
99
+
100
+ 'd4d5':['d4','d5'],
101
+ 'd4e5':['d4','e4','e5'],
102
+
103
+ 'd5e5':['d5','e5'],
104
+ 'd5406':['d5','d7','406'],
105
+
106
+ 'e5e6':['e5','e6'],
107
+ 'e5f6':['e5','f5','f6'],
108
+
109
+ 'e6f6':['e6','f6'],
110
+ 'e6406':['e6','e7','406'],
111
+
112
+ 'f6f7':['f6','f7'],
113
+ 'f6g7':['f6','g6','g7'],
114
+
115
+ 'f7g7':['f7','g7'],
116
+ 'f7406':['f7','406'],
117
+
118
+ 'g7g8':['g7','g8'],
119
+ 'g7h7':['g7','h7'],
120
+
121
+ 'g8g9':['g8','g9'],
122
+ 'g8h10':['g8','h8','h10'],
123
+
124
+ 'g9g11':['g9','g11'],
125
+ 'g9h10':['g9','gg9','gg10','h10'],
126
+
127
+ 'g11h10':['g11','gg11','gg10','h10'],
128
+ 'g11412':['g11','g18','412a'],
129
+
130
+ 'h7i7':['h7','i7'],
131
+ 'h7412':['h7','412'],
132
+
133
+ 'h10h11':['h10','h11'],
134
+ 'h10i12':['h10','i10','i12'],
135
+
136
+ 'h11h12':['h11','h12'],
137
+ 'h11i12':['h11','i11','i12'],
138
+
139
+ 'h12i12':['h12','i12'],
140
+ 'h12412':['h12','412a'],
141
+
142
+ 'i4p3':['i4','i3','p3'],
143
+ 'i4301':['i4','301'],
144
+
145
+ 'i7i4':['i7','i4'],
146
+ 'i7k7':['i7','k7'],
147
+
148
+ 'i12l13':['i12','l12','l13'],
149
+ 'i12i13':['i12','i13'],
150
+
151
+ 'i13k13':['i13','k13'],
152
+ 'i13j18':['i13','i17','j17','j18'],
153
+
154
+ 'j18412':['j18','412a'],
155
+ 'j18304':['j18','304'],
156
+
157
+ 'k5l5':['k5','l5'],
158
+ 'k5301':['k5','301'],
159
+
160
+ 'k7k5':['k7','k5'],
161
+ 'k7l7':['k7','l7'],
162
+
163
+ 'k13j18':['k13','k17','j17','j18'],
164
+ 'k13l13':['k13','l13'],
165
+
166
+ 'l5m5':['l5','m5'],
167
+ 'l5307':['l5','307'],
168
+
169
+ 'l7m7':['l7','m7'],
170
+ 'l7404':['l7','l8','404'],
171
+
172
+ 'l13l14':['l13','l14'],
173
+ 'l13m16':['l13','m13','m16'],
174
+
175
+ 'l14l15':['l14','l15'],
176
+ 'l14m16':['l14','m14','m16'],
177
+
178
+ 'l15l17':['l15','l17'],
179
+ 'l15m16':['l15','ll15','ll16','m16'],
180
+
181
+ 'l17m16':['l17','ll17','ll16','m16'],
182
+ 'l17304':['l17','304'],
183
+
184
+ 'm5n5':['m5','n5'],
185
+ 'm5410':['m5','m4','410'],
186
+
187
+ 'm7n11':['m7','n7','n11'],
188
+ 'm7404':['m7','404'],
189
+
190
+ 'm16m20':['m16','m20'],
191
+ 'm16n16':['m16','n16'],
192
+
193
+ 'm20o20':['m20','o20'],
194
+ 'm20202':['m20','202'],
195
+
196
+ 'n5n11':['n5','n11'],
197
+ 'n5410':['n5','410'],
198
+
199
+ 'n11p11':['n11','p11'],
200
+ 'n11303':['n11','303'],
201
+
202
+ 'n16n11':['n16','n11'],
203
+ 'n16o16':['n16','o16'],
204
+
205
+ 'o14p11':['o14','o11','p11'],
206
+ 'o14409':['o14','409a'],
207
+
208
+ 'o16o14':['o16','o14'],
209
+ 'o16o18':['o16','o18'],
210
+
211
+ 'o18200':['o18','200a'],
212
+ 'o18300':['o18','oo18','300'],
213
+
214
+ 'o20o18':['o20','o18'],
215
+ 'o20204':['o20','204'],
216
+
217
+ 'p3p11':['p3','p11'],
218
+ 'p3409':['p3','409'],
219
+
220
+ 'p11o20':['p11','p20','o20'],
221
+ 'p11201':['p11','q11','201']
222
+ };
223
+
224
+ var ends = {
225
+ '200': {col:'a', row:'3', width:190},
226
+ '200a': {col:'mm', row:'18', width:116},
227
+ '201': {col:'q', row:'12', width:154},
228
+ '202': {col:'m', row:'21', width:116},
229
+ '204': {col:'o', row:'21', width:152},
230
+
231
+ '300': {col:'oo', row:'19', width:152},
232
+ '301': {col:'k', row:'4', width:154},
233
+ '303': {col:'m', row:'11', width:116},
234
+ '304': {col:'l', row:'18', width:116},
235
+ '307': {col:'l', row:'4', width:154},
236
+
237
+ '400': {col:'a', row:'9', width:190},
238
+ '401': {col:'a', row:'8', width:190},
239
+ '403': {col:'a', row:'7', width:190},
240
+ '404': {col:'m', row:'8', width:116},
241
+ '405': {col:'a', row:'10', width:190},
242
+ '406': {col:'c', row:'7', width:152},
243
+ '409': {col:'p', row:'2', width:116},
244
+ '409a': {col:'oo', row:'14', width:116},
245
+ '410': {col:'n', row:'4', width:116},
246
+ '412': {col:'h', row:'6', width:152},
247
+ '412a': {col:'h', row:'18', width:152},
248
+ '413': {col:'a', row:'4', width:190},
249
+ '414': {col:'a', row:'11', width:190},
250
+ '415': {col:'a', row:'5', width:190},
251
+
252
+ '501a': {col:'a', row:'6', width:190},
253
+ '501': {col:'a', row:'12', width:190},
254
+ '503': {col:'a', row:'13', width:190}
255
+ };
256
+
257
+ var canvas;
258
+
259
+ function decorateTrace() {
260
+ trace[0].x = cols[trace[0].d[0]];
261
+ trace[0].y = rows[trace[0].d.slice(1)];
262
+ trace[0].previewCalls = previewCalls(trace[0]);
263
+
264
+ for (var i = 1; i < trace.length; i++) {
265
+ trace[i].x = cols[trace[i].d[0]];
266
+ trace[i].y = rows[trace[i].d.slice(1)];
267
+ trace[i].previewCalls = previewCalls(trace[i]);
268
+
269
+ var path = edges[trace[i-1].d+trace[i].d];
270
+ if (path) {
271
+ trace[i].path = [path.length-1];
272
+ for (var p = 1; p < path.length; p++) {
273
+ trace[i].path[p-1] = getSeg(path[p-1], path[p], p == path.length-1);
274
+ }
275
+ } else {
276
+ trace[i].path = [];
277
+ }
278
+ }
279
+
280
+ var path = edges[trace[i-1].d+response.code];
281
+ if (path) {
282
+ var end = ends[path[path.length-1]];
283
+ response.x = cols[end.col];
284
+ response.y = rows[end.row];
285
+ response.width = end.width;
286
+ response.type = 'normal';
287
+
288
+ response.path = [path.length-1];
289
+ for (var p = 1; p < path.length; p++) {
290
+ response.path[p-1] = getSeg(path[p-1], path[p], p == path.length-1);
291
+ }
292
+ } else {
293
+ var ld = trace[trace.length-1];
294
+ response.x = ld.x+50;
295
+ response.y = ld.y-50;
296
+ response.width = 38;
297
+ response.type = 'other';
298
+
299
+ response.path = [
300
+ {x1: ld.x+10, y1: ld.y-10,
301
+ x2: ld.x+36, y2: ld.y-36}
302
+ ];
303
+ }
304
+ };
305
+
306
+ function previewCalls(dec) {
307
+ var prev = '';
308
+ for (var i = 0; i < dec.calls.length; i++) {
309
+ if(dec.calls[i].call.indexOf("(default)") !== 0) {
310
+ prev += '<li>'+dec.calls[i].call;
311
+ if(dec.calls[i].source !== null)
312
+ prev += " (" + dec.calls[i].source + ")";
313
+ prev+='</li>';
314
+ }
315
+ }
316
+ return prev;
317
+ };
318
+
319
+ function drawTrace() {
320
+ drawDecision(trace[0]);
321
+ for (var i = 1; i < trace.length; i++) {
322
+ drawPath(trace[i].path);
323
+ drawDecision(trace[i]);
324
+ }
325
+
326
+ drawPath(response.path);
327
+ drawResponse();
328
+ };
329
+
330
+ function drawResponse() {
331
+ if (response.type == 'normal') {
332
+ var context = canvas.getContext('2d');
333
+ context.strokeStyle=HIGHLIGHT;
334
+ context.lineWidth=4;
335
+
336
+ context.beginPath();
337
+ context.rect(response.x-(response.width/2),
338
+ response.y-19,
339
+ response.width,
340
+ 38);
341
+ context.stroke();
342
+ } else {
343
+ var context = canvas.getContext('2d');
344
+ context.strokeStyle='#ff0000';
345
+ context.lineWidth=4;
346
+
347
+ context.beginPath();
348
+ context.arc(response.x, response.y, 19,
349
+ 0, 2*3.14159, false);
350
+ context.stroke();
351
+
352
+ }
353
+ };
354
+
355
+ function drawDecision(dec) {
356
+ var context = canvas.getContext('2d');
357
+
358
+ if (dec.previewCalls == '')
359
+ context.strokeStyle=REGULAR;
360
+ else
361
+ context.strokeStyle=HIGHLIGHT;
362
+ context.lineWidth=4;
363
+
364
+ context.beginPath();
365
+ context.moveTo(dec.x, dec.y-19);
366
+ context.lineTo(dec.x+19, dec.y);
367
+ context.lineTo(dec.x, dec.y+19);
368
+ context.lineTo(dec.x-19, dec.y);
369
+ context.closePath();
370
+ context.stroke();
371
+ };
372
+
373
+ function drawPath(path) {
374
+ var context = canvas.getContext('2d');
375
+ context.strokeStyle=REGULAR;
376
+ context.lineWidth=4;
377
+
378
+ context.beginPath();
379
+ context.moveTo(path[0].x1, path[0].y1);
380
+ for (var p = 0; p < path.length; p++) {
381
+ context.lineTo(path[p].x2, path[p].y2);
382
+ }
383
+ context.stroke();
384
+ };
385
+
386
+ function getSeg(p1, p2, last) {
387
+ var seg = {
388
+ x1:cols[p1[0]],
389
+ y1:rows[p1.slice(1)]
390
+ };
391
+ if (ends[p2]) {
392
+ seg.x2 = cols[ends[p2].col];
393
+ seg.y2 = rows[ends[p2].row];
394
+ } else {
395
+ seg.x2 = cols[p2[0]];
396
+ seg.y2 = rows[p2.slice(1)];
397
+ }
398
+
399
+ if (seg.x1 == seg.x2) {
400
+ if (seg.y1 < seg.y2) {
401
+ seg.y1 = seg.y1+19;
402
+ if (last) seg.y2 = seg.y2-19;
403
+ } else {
404
+ seg.y1 = seg.y1-19;
405
+ if (last) seg.y2 = seg.y2+19;
406
+ }
407
+ } else {
408
+ //assume seg.y1 == seg.y2
409
+ if (seg.x1 < seg.x2) {
410
+ seg.x1 = seg.x1+19;
411
+ if (last) seg.x2 = seg.x2-(ends[p2] ? (ends[p2].width/2) : 19);
412
+ } else {
413
+ seg.x1 = seg.x1-19;
414
+ if (last) seg.x2 = seg.x2+(ends[p2] ? (ends[p2].width/2) : 19);
415
+ }
416
+ }
417
+ return seg;
418
+ };
419
+
420
+ function traceDecision(name) {
421
+ for (var i = trace.length-1; i >= 0; i--)
422
+ if (trace[i].d == name) return trace[i];
423
+ };
424
+
425
+ var detailPanels = {};
426
+ function initDetailPanels() {
427
+ var windowWidth = document.getElementById('sizetest').clientWidth;
428
+ var infoPanel = document.getElementById('infopanel');
429
+ var panelWidth = windowWidth-infoPanel.offsetLeft;
430
+
431
+ var panels = {
432
+ 'request': document.getElementById('requestdetail'),
433
+ 'response': document.getElementById('responsedetail'),
434
+ 'decision': document.getElementById('decisiondetail')
435
+ };
436
+
437
+ var tabs = {
438
+ 'request': document.getElementById('requesttab'),
439
+ 'response': document.getElementById('responsetab'),
440
+ 'decision': document.getElementById('decisiontab')
441
+ };
442
+
443
+ var decisionId = document.getElementById('decisionid');
444
+ var decisionCalls = document.getElementById('decisioncalls');
445
+ var callInput = document.getElementById('callinput');
446
+ var callOutput = document.getElementById('calloutput');
447
+
448
+ var lastUsedPanelWidth = windowWidth-infoPanel.offsetLeft;
449
+
450
+ var setPanelWidth = function(width) {
451
+ infoPanel.style.left = (windowWidth-width)+'px';
452
+ canvas.style.marginRight = (width+20)+'px';
453
+ panelWidth = width;
454
+ };
455
+ setPanelWidth(panelWidth);
456
+
457
+ var ensureVisible = function() {
458
+ if (windowWidth-infoPanel.offsetLeft < 10)
459
+ setPanelWidth(lastUsedPanelWidth);
460
+ };
461
+
462
+ var decChoices = '';
463
+ for (var i = 0; i < trace.length; i++) {
464
+ decChoices += '<option value="'+trace[i].d+'">'+trace[i].d+'</option>';
465
+ }
466
+ decisionId.innerHTML = decChoices;
467
+ decisionId.selectedIndex = -1;
468
+
469
+ decisionId.onchange = function() {
470
+ detailPanels.setDecision(traceDecision(decisionId.value));
471
+ }
472
+
473
+ detailPanels.setDecision = function(dec) {
474
+ decisionId.value = dec.d;
475
+
476
+ var calls = [];
477
+ for (var i = 0; i < dec.calls.length; i++) {
478
+ calls.push('<option value="'+dec.d+'-'+i+'">');
479
+ calls.push(dec.calls[i].call);
480
+ if(dec.calls[i].source !== null)
481
+ calls.push(' (' + dec.calls[i].source + ')');
482
+ calls.push('</option>');
483
+ }
484
+ decisionCalls.innerHTML = calls.join('');
485
+ decisionCalls.selectedIndex = 0;
486
+
487
+ decisionCalls.onchange();
488
+ };
489
+
490
+ detailPanels.show = function(name) {
491
+ for (p in panels) {
492
+ if (p == name) {
493
+ panels[p].style.display = 'block';
494
+ tabs[p].className = 'selectedtab';
495
+ }
496
+ else {
497
+ panels[p].style.display = 'none';
498
+ tabs[p].className = '';
499
+ }
500
+ }
501
+ ensureVisible();
502
+ };
503
+
504
+ detailPanels.hide = function() {
505
+ setPanelWidth(0);
506
+ }
507
+
508
+ decisionCalls.onchange = function() {
509
+ var val = decisionCalls.value;
510
+ if (val) {
511
+ var dec = traceDecision(val.substring(0, val.indexOf('-')));
512
+ var call = dec.calls[parseInt(val.substring(val.indexOf('-')+1, val.length))];
513
+
514
+ if (call.call.indexOf("(default)") !== 0) {
515
+ callInput.style.color='#000000';
516
+ callInput.textContent = call.input;
517
+ if (call.output != null) {
518
+ callOutput.style.color = '#000000';
519
+ callOutput.textContent = call.output;
520
+ } else {
521
+ if(call.exception !== null){
522
+ callOutput.style.color = '#ff0000';
523
+ callOutput.textContent = 'Exception raised!\n\n' + call.exception['class'] + ': ' +
524
+ call.exception.message + '\n ' + call.exception.backtrace.split('\n').join('\n ');
525
+ }
526
+ }
527
+ } else {
528
+ callInput.style.color='#999999';
529
+ callInput.textContent = call.call.replace('(default)', '') + " was not overridden by the resource";
530
+ callOutput.textContent = '';
531
+ }
532
+ } else {
533
+ callInput.textContent = '';
534
+ callOutput.textContent = '';
535
+ }
536
+ };
537
+
538
+ var headersList = function(headers) {
539
+ var h = '';
540
+ for (n in headers) h += '<tr><th><code>'+n+':</code></th><td><code>'+headers[n] + '</code></td></tr>';
541
+ return h;
542
+ };
543
+
544
+ document.getElementById('requestmethod').innerHTML = request.method;
545
+ document.getElementById('requestpath').innerHTML = request.path;
546
+ document.getElementById('requestheaders').innerHTML = headersList(request.headers);
547
+ document.getElementById('requestbody').textContent = request.body;
548
+
549
+ document.getElementById('responsecode').innerHTML = response.code;
550
+ document.getElementById('responseheaders').innerHTML = headersList(response.headers);
551
+ document.getElementById('responsebody').textContent = response.body;
552
+
553
+
554
+ var infoControls = document.getElementById('infocontrols');
555
+ var md = false;
556
+ var dragged = false;
557
+ var msoff = 0;
558
+ infoControls.onmousedown = function(ev) {
559
+ md = true;
560
+ dragged = false;
561
+ msoff = ev.clientX-infoPanel.offsetLeft;
562
+ };
563
+
564
+ infoControls.onclick = function(ev) {
565
+ if (dragged) {
566
+ lastUsedPanelWidth = panelWidth;
567
+ }
568
+ else if (panelWidth < 10) {
569
+ switch(ev.target.id) {
570
+ case 'requesttab': detailPanels.show('request'); break;
571
+ case 'responsetab': detailPanels.show('response'); break;
572
+ case 'decisiontab': detailPanels.show('decision'); break;
573
+ default: ensureVisible();
574
+ }
575
+ } else {
576
+ var name = 'none';
577
+ switch(ev.target.id) {
578
+ case 'requesttab': name = 'request'; break;
579
+ case 'responsetab': name = 'response'; break;
580
+ case 'decisiontab': name = 'decision'; break;
581
+ }
582
+
583
+ if (panels[name] && panels[name].style.display != 'block')
584
+ detailPanels.show(name);
585
+ else
586
+ detailPanels.hide();
587
+ }
588
+
589
+ return false;
590
+ };
591
+
592
+ document.onmousemove = function(ev) {
593
+ if (md) {
594
+ dragged = true;
595
+ panelWidth = windowWidth-(ev.clientX-msoff);
596
+ if (panelWidth < 0) {
597
+ panelWidth = 0;
598
+ infoPanel.style.left = windowWidth+"px";
599
+ }
600
+ else if (panelWidth > windowWidth-21) {
601
+ panelWidth = windowWidth-21;
602
+ infoPanel.style.left = '21px';
603
+ }
604
+ else
605
+ infoPanel.style.left = (ev.clientX-msoff)+"px";
606
+
607
+ canvas.style.marginRight = panelWidth+20+"px";
608
+ return false;
609
+ }
610
+ };
611
+
612
+ document.onmouseup = function() { md = false; };
613
+
614
+ window.onresize = function() {
615
+ windowWidth = document.getElementById('sizetest').clientWidth;
616
+ infoPanel.style.left = windowWidth-panelWidth+'px';
617
+ };
618
+
619
+ detailPanels.setDecision(trace[0]);
620
+ };
621
+
622
+ window.onload = function() {
623
+ canvas = document.getElementById('v3map');
624
+
625
+ initDetailPanels();
626
+
627
+ var scale = 0.25;
628
+ var coy = canvas.offsetTop;
629
+ function findDecision(ev) {
630
+ var x = (ev.clientX+window.pageXOffset)/scale;
631
+ var y = (ev.clientY+window.pageYOffset-coy)/scale;
632
+
633
+ for (var i = trace.length-1; i >= 0; i--) {
634
+ if (x >= trace[i].x-19 && x <= trace[i].x+19 &&
635
+ y >= trace[i].y-19 && y <= trace[i].y+19)
636
+ return trace[i];
637
+ }
638
+ };
639
+
640
+ var preview = document.getElementById('preview');
641
+ var previewId = document.getElementById('previewid');
642
+ var previewCalls = document.getElementById('previewcalls');
643
+ function previewDecision(dec) {
644
+ preview.style.left = (dec.x*scale)+'px';
645
+ preview.style.top = (dec.y*scale+coy+15)+'px';
646
+ preview.style.display = 'block';
647
+ previewId.textContent = dec.d;
648
+
649
+ previewCalls.innerHTML = dec.previewCalls;
650
+ };
651
+
652
+ function overResponse(ev) {
653
+ var x = (ev.clientX+window.pageXOffset)/scale;
654
+ var y = (ev.clientY+window.pageYOffset-coy)/scale;
655
+
656
+ return (x >= response.x-(response.width/2)
657
+ && x <= response.x+(response.width/2)
658
+ && y >= response.y-19 && y <= response.y+19);
659
+ };
660
+
661
+ decorateTrace();
662
+
663
+ var bg = new Image(3138, 2184);
664
+
665
+ function drawMap() {
666
+ var ctx = canvas.getContext("2d");
667
+
668
+ ctx.save();
669
+ ctx.scale(1/scale, 1/scale);
670
+ ctx.fillStyle = '#ffffff';
671
+ ctx.fillRect(0, 0, 3138, 2184);
672
+ ctx.restore();
673
+
674
+ ctx.drawImage(bg, 0, 0);
675
+ drawTrace();
676
+ };
677
+
678
+ bg.onload = function() {
679
+ canvas.getContext("2d").scale(scale, scale);
680
+
681
+ document.getElementById('zoomin').onclick = function() {
682
+ scale = scale*2;
683
+ canvas.getContext("2d").scale(2, 2);
684
+ drawMap();
685
+ };
686
+
687
+ document.getElementById('zoomout').onclick = function() {
688
+ scale = scale/2;
689
+ canvas.getContext("2d").scale(0.5, 0.5);
690
+ drawMap();
691
+ };
692
+
693
+ drawMap(scale);
694
+
695
+ canvas.onmousemove = function(ev) {
696
+ if (findDecision(ev)) {
697
+ canvas.style.cursor = 'pointer';
698
+ previewDecision(findDecision(ev));
699
+ }
700
+ else {
701
+ preview.style.display = 'none';
702
+ if (overResponse(ev))
703
+ canvas.style.cursor = 'pointer';
704
+ else
705
+ canvas.style.cursor = 'default';
706
+ }
707
+ };
708
+
709
+ canvas.onclick = function(ev) {
710
+ var dec = findDecision(ev);
711
+ if (dec) {
712
+ detailPanels.setDecision(dec);
713
+ detailPanels.show('decision');
714
+ } else if (overResponse(ev)) {
715
+ detailPanels.show('response');
716
+ }
717
+ };
718
+ };
719
+
720
+ bg.onerror = function() {
721
+ alert('Failed to load background image.');
722
+ };
723
+
724
+ bg.src = 'static/map.png';
725
+ };