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,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