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,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
@@ -1,6 +1,6 @@
1
1
  module Webmachine
2
2
  # Library version
3
- VERSION = "0.4.2"
3
+ VERSION = "1.0.0"
4
4
 
5
5
  # String for use in "Server" HTTP response header, which includes
6
6
  # the {VERSION}.
@@ -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
- "<html><body>testing</body></html>"
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
- let(:env) do
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
- code, headers, body = subject.call(env)
75
- code.should == 200
76
- headers["Content-Type"].should == "text/html"
77
- body.should include "<html><body>testing</body></html>"
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
- subject.call(env)
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
- subject.call(env)
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
- env["REQUEST_METHOD"] = "PUT"
98
- env["CONTENT_TYPE"] = "application/json"
99
- code, headers, body = subject.call(env)
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
- code, headers, body = subject.call(env)
105
- headers.should have_key "Server"
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
- env["PATH_INFO"] = "/missing"
110
- code, headers, body = subject.call(env)
111
- code.should == 404
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
- env["HTTP_ACCEPT"] = "application/json"
117
- code, headers, body = subject.call(env)
118
- code.should == 406
119
- headers.should_not have_key "Content-Type"
120
- headers.should_not have_key "Content-Length"
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
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Webmachine::Configuration do
4
4
  before { Webmachine.configuration = nil }
5
-
5
+
6
6
  %w{ip port adapter adapter_options}.each do |field|
7
7
  it { should respond_to(field) }
8
8
  it { should respond_to("#{field}=") }
@@ -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