webmachine 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/Gemfile +12 -10
  2. data/Guardfile +1 -1
  3. data/README.md +1 -1
  4. data/examples/application.rb +35 -0
  5. data/lib/webmachine.rb +4 -3
  6. data/lib/webmachine/adapter.rb +36 -0
  7. data/lib/webmachine/adapters/mongrel.rb +18 -12
  8. data/lib/webmachine/adapters/rack.rb +26 -7
  9. data/lib/webmachine/adapters/webrick.rb +20 -16
  10. data/lib/webmachine/application.rb +108 -0
  11. data/lib/webmachine/chunked_body.rb +2 -2
  12. data/lib/webmachine/configuration.rb +24 -14
  13. data/lib/webmachine/decision/conneg.rb +9 -10
  14. data/lib/webmachine/decision/flow.rb +25 -28
  15. data/lib/webmachine/decision/fsm.rb +21 -22
  16. data/lib/webmachine/decision/helpers.rb +3 -3
  17. data/lib/webmachine/dispatcher.rb +18 -10
  18. data/lib/webmachine/dispatcher/route.rb +54 -17
  19. data/lib/webmachine/errors.rb +1 -1
  20. data/lib/webmachine/headers.rb +2 -2
  21. data/lib/webmachine/media_type.rb +2 -2
  22. data/lib/webmachine/request.rb +78 -3
  23. data/lib/webmachine/resource.rb +3 -2
  24. data/lib/webmachine/resource/authentication.rb +4 -3
  25. data/lib/webmachine/resource/callbacks.rb +4 -3
  26. data/lib/webmachine/resource/encodings.rb +4 -3
  27. data/lib/webmachine/response.rb +3 -2
  28. data/lib/webmachine/streaming.rb +4 -4
  29. data/lib/webmachine/version.rb +1 -1
  30. data/spec/webmachine/adapter_spec.rb +40 -0
  31. data/spec/webmachine/adapters/mongrel_spec.rb +22 -0
  32. data/spec/webmachine/adapters/rack_spec.rb +34 -8
  33. data/spec/webmachine/adapters/webrick_spec.rb +18 -0
  34. data/spec/webmachine/application_spec.rb +73 -0
  35. data/spec/webmachine/dispatcher/route_spec.rb +59 -2
  36. data/spec/webmachine/dispatcher_spec.rb +17 -5
  37. data/spec/webmachine/request_spec.rb +158 -1
  38. data/webmachine.gemspec +6 -27
  39. metadata +304 -112
  40. data/spec/tests.org +0 -80
@@ -116,5 +116,5 @@ module Webmachine
116
116
  other.major == major && other.minor == "*"
117
117
  end
118
118
  end
119
- end
120
- end
119
+ end # class MediaType
120
+ end # module Webmachine
@@ -9,6 +9,8 @@ module Webmachine
9
9
  attr_reader :method, :uri, :headers, :body
10
10
  attr_accessor :disp_path, :path_info, :path_tokens
11
11
 
12
+ STANDARD_HTTP_METHODS = %w[GET HEAD POST PUT DELETE TRACE CONNECT OPTIONS]
13
+
12
14
  # @param [String] method the HTTP request method
13
15
  # @param [URI] uri the requested URI, including host, scheme and
14
16
  # port
@@ -55,12 +57,85 @@ module Webmachine
55
57
  def query
56
58
  unless @query
57
59
  @query = {}
58
- uri.query.split(/&/).each do |kv|
60
+ (uri.query || '').split(/&/).each do |kv|
59
61
  k, v = CGI.unescape(kv).split(/=/)
60
62
  @query[k] = v if k && v
61
63
  end
62
64
  end
63
65
  @query
64
66
  end
65
- end
66
- end
67
+
68
+ # Is this an HTTPS request?
69
+ #
70
+ # @return [Boolean]
71
+ # true if this request was made via HTTPS
72
+ def https?
73
+ uri.scheme == "https"
74
+ end
75
+
76
+ # Is this a GET request?
77
+ #
78
+ # @return [Boolean]
79
+ # true if this request was made with the GET method
80
+ def get?
81
+ method == "GET"
82
+ end
83
+
84
+ # Is this a HEAD request?
85
+ #
86
+ # @return [Boolean]
87
+ # true if this request was made with the HEAD method
88
+ def head?
89
+ method == "HEAD"
90
+ end
91
+
92
+ # Is this a POST request?
93
+ #
94
+ # @return [Boolean]
95
+ # true if this request was made with the GET method
96
+ def post?
97
+ method == "POST"
98
+ end
99
+
100
+ # Is this a PUT request?
101
+ #
102
+ # @return [Boolean]
103
+ # true if this request was made with the PUT method
104
+ def put?
105
+ method == "PUT"
106
+ end
107
+
108
+ # Is this a DELETE request?
109
+ #
110
+ # @return [Boolean]
111
+ # true if this request was made with the DELETE method
112
+ def delete?
113
+ method == "DELETE"
114
+ end
115
+
116
+ # Is this a TRACE request?
117
+ #
118
+ # @return [Boolean]
119
+ # true if this request was made with the TRACE method
120
+ def trace?
121
+ method == "TRACE"
122
+ end
123
+
124
+ # Is this a CONNECT request?
125
+ #
126
+ # @return [Boolean]
127
+ # true if this request was made with the CONNECT method
128
+ def connect?
129
+ method == "CONNECT"
130
+ end
131
+
132
+ # Is this an OPTIONS request?
133
+ #
134
+ # @return [Boolean]
135
+ # true if this request was made with the OPTIONS method
136
+ def options?
137
+ method == "OPTIONS"
138
+ end
139
+
140
+ end # class Request
141
+ end # module Webmachine
@@ -45,5 +45,6 @@ module Webmachine
45
45
  def charset_nop(x)
46
46
  x
47
47
  end
48
- end
49
- end
48
+
49
+ end # class Resource
50
+ end # module Webmachine
@@ -30,6 +30,7 @@ module Webmachine
30
30
  %Q[Basic realm="#{realm}"]
31
31
  end
32
32
  end
33
- end
34
- end
35
- end
33
+
34
+ end # module Authentication
35
+ end # class Resource
36
+ end # module Webmachine
@@ -371,6 +371,7 @@ module Webmachine
371
371
  def validate_content_checksum
372
372
  nil
373
373
  end
374
- end
375
- end
376
- end
374
+
375
+ end # module Callbacks
376
+ end # class Resource
377
+ end # module Webmachine
@@ -31,6 +31,7 @@ module Webmachine
31
31
  Zlib::GzipWriter.wrap(StringIO.new(out)){|gz| gz << data }
32
32
  end
33
33
  end
34
- end
35
- end
36
- end
34
+
35
+ end # module Encodings
36
+ end # class Resource
37
+ end # module Webmachine
@@ -46,5 +46,6 @@ module Webmachine
46
46
 
47
47
  alias :is_redirect? :redirect
48
48
  alias :redirect_to :do_redirect
49
- end
50
- end
49
+
50
+ end # class Response
51
+ end # module Webmachine
@@ -26,7 +26,7 @@ module Webmachine
26
26
  yield resource.send(encoder, resource.send(charsetter, block.to_s))
27
27
  end
28
28
  end
29
- end
29
+ end # class EnumerableEncoder
30
30
 
31
31
  # Implements a streaming encoder for callable bodies, such as
32
32
  # Proc. (essentially futures)
@@ -44,7 +44,7 @@ module Webmachine
44
44
  def to_proc
45
45
  method(:call).to_proc
46
46
  end
47
- end
47
+ end # class CallableEncoder
48
48
 
49
49
  # Implements a streaming encoder for Fibers with the same API as the
50
50
  # EnumerableEncoder. This will resume the Fiber until it terminates
@@ -59,5 +59,5 @@ module Webmachine
59
59
  yield resource.send(encoder, resource.send(charsetter, chunk.to_s))
60
60
  end
61
61
  end
62
- end
63
- end
62
+ end # class FiberEncoder
63
+ end # module Webmachine
@@ -1,6 +1,6 @@
1
1
  module Webmachine
2
2
  # Library version
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.0"
4
4
 
5
5
  # String for use in "Server" HTTP response header, which includes
6
6
  # the {VERSION}.
@@ -0,0 +1,40 @@
1
+ require "spec_helper"
2
+
3
+ describe Webmachine::Adapter do
4
+ let(:configuration) { Webmachine::Configuration.default }
5
+ let(:dispatcher) { Webmachine::Dispatcher.new }
6
+ let(:adapter) do
7
+ described_class.new(configuration, dispatcher)
8
+ end
9
+
10
+ describe "#initialize" do
11
+ it "stores the provided configuration" do
12
+ adapter.configuration.should eql configuration
13
+ end
14
+
15
+ it "stores the provided dispatcher" do
16
+ adapter.dispatcher.should eql dispatcher
17
+ end
18
+ end
19
+
20
+ describe ".run" do
21
+ it "creates a new adapter and runs it" do
22
+ adapter = mock(described_class)
23
+
24
+ described_class.should_receive(:new).
25
+ with(configuration, dispatcher).
26
+ and_return(adapter)
27
+
28
+ adapter.should_receive(:run)
29
+
30
+ described_class.run(configuration, dispatcher)
31
+ end
32
+ end
33
+
34
+ describe "#run" do
35
+ it "raises a NotImplementedError" do
36
+ lambda { adapter.run }.should raise_exception(NotImplementedError)
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ begin
4
+ describe Webmachine::Adapters::Mongrel do
5
+ let(:configuration) { Webmachine::Configuration.default }
6
+ let(:dispatcher) { Webmachine::Dispatcher.new }
7
+
8
+ let(:adapter) do
9
+ described_class.new(configuration, dispatcher)
10
+ end
11
+
12
+ it "inherits from Webmachine::Adapter" do
13
+ adapter.should be_a_kind_of(Webmachine::Adapter)
14
+ end
15
+
16
+ it "implements #run" do
17
+ described_class.instance_methods(false).map {|m| m.to_sym }.should include :run
18
+ end
19
+ end
20
+ rescue LoadError
21
+ warn "Platform is #{RUBY_PLATFORM}: skipping mongrel adapter spec."
22
+ end
@@ -11,8 +11,6 @@ module Test
11
11
  end
12
12
 
13
13
  describe Webmachine::Adapters::Rack do
14
- let(:adapter) { described_class }
15
-
16
14
  let(:env) do
17
15
  { "REQUEST_METHOD" => "GET",
18
16
  "SCRIPT_NAME" => "",
@@ -29,33 +27,61 @@ describe Webmachine::Adapters::Rack do
29
27
  "rack.run_once" => false }
30
28
  end
31
29
 
30
+ let(:configuration) { Webmachine::Configuration.new('0.0.0.0', 8080, :Rack, {}) }
31
+ let(:dispatcher) { Webmachine::Dispatcher.new }
32
+ let(:adapter) do
33
+ described_class.new(configuration, dispatcher)
34
+ end
35
+
36
+ subject { adapter }
37
+
32
38
  before do
33
- Webmachine::Dispatcher.reset
34
- Webmachine::Dispatcher.add_route ['test'], Test::Resource
39
+ dispatcher.add_route ['test'], Test::Resource
40
+ end
41
+
42
+ it "inherits from Webmachine::Adapter" do
43
+ adapter.should be_a_kind_of(Webmachine::Adapter)
44
+ end
45
+
46
+ describe "#run" do
47
+ before do
48
+ configuration.adapter_options[:debug] = true
49
+ end
50
+
51
+ it "starts a rack server with the correct options" do
52
+ Rack::Server.should_receive(:start).with(
53
+ :app => adapter,
54
+ :Port => configuration.port,
55
+ :Host => configuration.ip,
56
+ :debug => true
57
+ )
58
+
59
+ adapter.run
60
+ end
35
61
  end
36
62
 
37
63
  it "should proxy request to webmachine" do
38
- code, headers, body = adapter.new.call(env)
64
+ code, headers, body = subject.call(env)
39
65
  code.should == 200
40
66
  headers["Content-Type"].should == "text/html"
41
67
  body.should include "<html><body>testing</body></html>"
42
68
  end
43
69
 
44
70
  it "should set Server header" do
45
- code, headers, body = adapter.new.call(env)
71
+ code, headers, body = subject.call(env)
46
72
  headers.should have_key "Server"
47
73
  end
48
74
 
49
75
  it "should handle non-success correctly" do
50
76
  env["PATH_INFO"] = "/missing"
51
- code, headers, body = adapter.new.call(env)
77
+ code, headers, body = subject.call(env)
52
78
  code.should == 404
53
79
  headers["Content-Type"].should == "text/html"
54
80
  end
55
81
 
56
82
  it "should handle empty bodies correctly" do
57
83
  env["HTTP_ACCEPT"] = "application/json"
58
- code, headers, body = adapter.new.call(env)
84
+ code, headers, body = subject.call(env)
59
85
  code.should == 406
60
86
  headers.should_not have_key "Content-Type"
61
87
  headers.should_not have_key "Content-Length"
@@ -0,0 +1,18 @@
1
+ require "spec_helper"
2
+
3
+ describe Webmachine::Adapters::WEBrick do
4
+ let(:configuration) { Webmachine::Configuration.default }
5
+ let(:dispatcher) { Webmachine::Dispatcher.new }
6
+
7
+ let(:adapter) do
8
+ described_class.new(configuration, dispatcher)
9
+ end
10
+
11
+ it "inherits from Webmachine::Adapter" do
12
+ adapter.should be_a_kind_of(Webmachine::Adapter)
13
+ end
14
+
15
+ it "implements #run" do
16
+ described_class.instance_methods(false).map {|m| m.to_sym }.should include :run
17
+ end
18
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webmachine::Application do
4
+ let(:application) { described_class.new }
5
+ let(:test_resource) { Class.new(Webmachine::Resource) }
6
+
7
+ it "accepts a Configuration when initialized" do
8
+ config = Webmachine::Configuration.new('1.1.1.1', 9999, :Mongrel, {})
9
+ described_class.new(config).configuration.should be(config)
10
+ end
11
+
12
+ it "is yielded into a block provided during initialization" do
13
+ yielded_app = nil
14
+ described_class.new do |app|
15
+ app.should be_kind_of(Webmachine::Application)
16
+ yielded_app = app
17
+ end.should be(yielded_app)
18
+ end
19
+
20
+ it "is initialized with the default Configration if none is given" do
21
+ application.configuration.should eq(Webmachine::Configuration.default)
22
+ end
23
+
24
+ it "returns the receiver from the configure call so you can chain it" do
25
+ application.configure { |c| }.should equal(application)
26
+ end
27
+
28
+ it "is configurable" do
29
+ application.configure do |config|
30
+ config.should be_kind_of(Webmachine::Configuration)
31
+ end
32
+ end
33
+
34
+ it "is initialized with an empty Dispatcher" do
35
+ application.dispatcher.routes.should be_empty
36
+ end
37
+
38
+ it "can have routes added" do
39
+ route = nil
40
+ resource = test_resource # overcome instance_eval :/
41
+
42
+ application.routes.should be_empty
43
+
44
+ application.routes do
45
+ route = add ['*'], resource
46
+ end
47
+
48
+ route.should be_kind_of(Webmachine::Dispatcher::Route)
49
+ application.routes.should eq([route])
50
+ end
51
+
52
+ describe "#adapter" do
53
+ let(:adapter_class) { application.adapter_class }
54
+
55
+ it "returns an instance of it's adapter class" do
56
+ application.adapter.should be_an_instance_of(adapter_class)
57
+ end
58
+
59
+ it "is memoized" do
60
+ application.adapter.should eql application.adapter
61
+ end
62
+ end
63
+
64
+ it "can be run" do
65
+ application.adapter.should_receive(:run)
66
+ application.run
67
+ end
68
+
69
+ it "can be queried about its configured adapter" do
70
+ expected = Webmachine::Adapters.const_get(application.configuration.adapter)
71
+ application.adapter_class.should equal(expected)
72
+ end
73
+ end
@@ -1,7 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Webmachine::Dispatcher::Route do
4
- let(:request){ Webmachine::Request.new("GET", URI.parse("http://localhost:8098/"), Webmachine::Headers.new, "") }
4
+ let(:method) { "GET" }
5
+ let(:uri) { URI.parse("http://localhost:8080/") }
6
+ let(:request){ Webmachine::Request.new(method, uri, Webmachine::Headers.new, "") }
5
7
  let(:resource){ Class.new(Webmachine::Resource) }
6
8
 
7
9
  matcher :match_route do |*expected|
@@ -37,6 +39,61 @@ describe Webmachine::Dispatcher::Route do
37
39
  it { should_not match_route [] }
38
40
  it { should_not match_route %w{bar *} }
39
41
  end
42
+
43
+ context "with a guard on the request method" do
44
+ let(:uri){ URI.parse("http://localhost:8080/notes") }
45
+ let(:route) do
46
+ described_class.new(
47
+ ["notes"],
48
+ lambda { |request| request.method == "POST" },
49
+ resource
50
+ )
51
+ end
52
+ subject { route }
53
+
54
+ context "when guard passes" do
55
+ let(:method){ "POST" }
56
+ it { should be_match(request) }
57
+
58
+ context "but the path match fails" do
59
+ let(:uri){ URI.parse("http://localhost:8080/other") }
60
+ it { should_not be_match(request) }
61
+ end
62
+ end
63
+
64
+ context "when guard fails" do
65
+ let(:method) { "GET" }
66
+ it { should_not be_match(request) }
67
+ end
68
+
69
+ context "when the guard responds to #call" do
70
+ let(:guard_class) do
71
+ Class.new do
72
+ def initialize(method)
73
+ @method = method
74
+ end
75
+
76
+ def call(request)
77
+ request.method == @method
78
+ end
79
+ end
80
+ end
81
+
82
+ let(:route) do
83
+ described_class.new(["notes"], guard_class.new("POST"), resource)
84
+ end
85
+
86
+ context "when the guard passes" do
87
+ let(:method){ "POST" }
88
+ it { should be_match(request) }
89
+ end
90
+
91
+ context "when the guard fails" do
92
+ # let(:method){ "GET" }
93
+ it { should_not be_match(request) }
94
+ end
95
+ end
96
+ end
40
97
  end
41
98
 
42
99
  context "applying bindings" do
@@ -66,7 +123,7 @@ describe Webmachine::Dispatcher::Route do
66
123
 
67
124
  context "with a splat" do
68
125
  subject { described_class.new(['*'], resource) }
69
-
126
+
70
127
  it "should assign empty path tokens" do
71
128
  request.path_tokens.should == []
72
129
  end