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
@@ -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
+ };