webmachine 0.3.0 → 0.4.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 (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