rity 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ - LICENSE
data/Gemfile CHANGED
@@ -4,3 +4,10 @@ gemspec
4
4
 
5
5
  gem "rake"
6
6
  gem "awesome_print"
7
+
8
+ gem "yard"
9
+ gem "rdiscount"
10
+
11
+ gem "em-synchrony", :git => "git://github.com/lgierth/em-synchrony",
12
+ :branch => "immediately-return-from-sync"
13
+ gem "em-http-request", :git => "git://github.com/igrigorik/em-http-request"
data/README.md CHANGED
@@ -7,8 +7,11 @@ TODO
7
7
  ----
8
8
 
9
9
  - Proxying via EM.enable_proxy
10
+ - Investigate side effects of sending 500 for errors
10
11
  - Rack Handler
11
12
  - Support for keep-alive connections
12
13
  - Investigate MVM support in JRuby/Rubinius/MRI
13
14
  - Support for SPDY
14
15
  - Investigate preforking and letting multiple EventMachine loops listen on a shared socket
16
+ - Support for X-Sendfile header
17
+ - Deamonizing & dropping privileges
data/Rakefile CHANGED
@@ -1,9 +1,13 @@
1
1
  require "bundler"
2
- Bundler.setup :default
2
+ Bundler.setup :default, :development
3
3
 
4
4
  task :default => :spec
5
5
 
6
6
  require "rspec/core/rake_task"
7
7
  RSpec::Core::RakeTask.new :spec
8
8
 
9
+ require "bundler/gem_helper"
9
10
  Bundler::GemHelper.install_tasks
11
+
12
+ require "yard"
13
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,102 @@
1
+ #
2
+ # streaming response body
3
+ #
4
+
5
+ run proc {|env|
6
+ EM.add_timer(1) {
7
+ env["stream.start"].call 200, "Content-Type" => "image/png"
8
+ EM.add_periodic_timer(1) { env["stream.send"].call "\n" }
9
+ }
10
+ EM.add_timer(10) { env["stream.close"].call }
11
+ [-1, {}, []]
12
+
13
+ # or instantly:
14
+ EM.add_periodic_timer(1) { env["stream.send"].call "\n" }
15
+ EM.add_timer(10) { env["stream.close"].call }
16
+ [200, {"Content-Type" => "image/png"}, Rack::STREAMING]
17
+ }
18
+
19
+ #
20
+ # async response (though all responses are async by default, anyway)
21
+ #
22
+
23
+ run proc {|env|
24
+ Fiber.new {
25
+ do_something
26
+ env["async.callback"].call [201, {"Content-Type" => "text/plain"}, ["Great!"]]
27
+ }.resume
28
+
29
+ [-1, {}, []]
30
+ }
31
+
32
+ #
33
+ # streaming request body
34
+ #
35
+
36
+ run proc {|env|
37
+ # will return when the input is write-closed
38
+ env["rack.input"].receive {|chunk| }
39
+
40
+ # same here
41
+ env["rack.input"].sync
42
+ File.open("input.dat", "w") {|f| f << env["rack.input"] }
43
+
44
+ [201, {"The" => "headers"}, []]
45
+ }
46
+
47
+ #
48
+ # proxying
49
+ #
50
+
51
+ class MySimpleProxy
52
+ def initialize(env)
53
+ @env = env
54
+ end
55
+
56
+ def call(response)
57
+ @env["async.callback"].call response
58
+
59
+ # or modify the ouput via streaming:
60
+ @env["stream.start"].call *response[0..1]
61
+ response[2].each do |chunk|
62
+ @env["stream.send"].call chunk.upcase
63
+ end
64
+ @env["stream.close"].call
65
+ end
66
+ end
67
+
68
+ class MyProxy
69
+ def initialize(env)
70
+ @env = env
71
+ end
72
+
73
+ def call(response)
74
+ @env["proxy.start_reverse"].call *response[0..1]
75
+ end
76
+ end
77
+
78
+ class MyApp
79
+ def headers(env)
80
+ shard = sharding_func.call env["REQUEST_URI"]
81
+ # env["rity.callback"] = MySimpleProxy.new(env)
82
+ env["proxy.callback"] = MyProxy.new(env)
83
+ env["proxy.start"].call shard.uri, "X-Sharding-Func" => "skip"
84
+ end
85
+ end
86
+
87
+ run MyApp.new
88
+
89
+ # or in short:
90
+
91
+ run proc {|env|
92
+ shard = sharding_func.call(env["REQUEST_URI"])
93
+ # if no block is passed to proxy.start, it will use EM's native proxying to
94
+ # pass the response to the client
95
+ env["proxy.start"].call shard.uri, "Additional" => "headers" do |response|
96
+ @env["stream.start"].call *response[0..1]
97
+ response[2].each do |chunk|
98
+ @env["stream.send"].call chunk.upcase
99
+ end
100
+ @env["stream.close"].call
101
+ end
102
+ }
@@ -1,3 +1,7 @@
1
1
  require "rity/connection"
2
2
  require "rity/request"
3
3
  require "rity/version"
4
+
5
+ module Rack
6
+ STREAMING = "Rack::STREAMING"
7
+ end
@@ -0,0 +1,29 @@
1
+ require "eventmachine"
2
+ require "em-synchrony"
3
+ require "stringio"
4
+
5
+ module Rity
6
+ class Body < StringIO
7
+ include EM::Deferrable
8
+
9
+ def close_write
10
+ super
11
+ succeed
12
+ end
13
+
14
+ def write(str)
15
+ super
16
+ @receiver.call str if @receiver
17
+ end
18
+
19
+ def receive(&block)
20
+ string.each_line &block
21
+ @receiver = block
22
+ sync
23
+ end
24
+
25
+ def sync
26
+ EM::Synchrony.sync self
27
+ end
28
+ end
29
+ end
@@ -49,7 +49,7 @@ module Rity
49
49
 
50
50
  EM.epoll
51
51
 
52
- EM.start_server(address, port, Connection) do |conn|
52
+ EM.start_server address, port, Connection do |conn|
53
53
  conn.app = app
54
54
  conn.log = log if defined? log
55
55
  end
@@ -1,78 +1,72 @@
1
1
  require "eventmachine"
2
+ require "em-synchrony"
2
3
  require "hatetepe"
3
-
4
4
  require "rity/request"
5
5
 
6
6
  module Rity
7
- class Connection < EM::Connection
7
+ module Connection
8
8
  attr_accessor :app, :log
9
- attr_reader :requests, :parser, :builder, :responder
10
9
 
11
10
  def post_init
12
- @requests, request = [], nil
11
+ @parser = Hatetepe::Parser.new
12
+ @builder = Hatetepe::Builder.new
13
13
 
14
- @parser = Hatetepe::Parser.new do |p|
15
- p.on_request do |verb, url|
16
- request = Request.new(@app, verb, url)
17
- request.log = log
18
- @requests.push(request)
19
- @requests.last.callback &method(:write_responses)
20
- end
21
-
22
- p.on_header do |name, value|
23
- @requests.last.add_header(name, value)
24
- end
25
-
26
- p.on_headers_complete do
27
- @requests.last.precall
28
- end
29
-
30
- p.on_body_chunk do |chunk|
31
- @requests.last.add_body_chunk(chunk)
32
- end
33
-
34
- p.on_complete do
35
- @requests.last.call
36
- request = nil
37
- end
38
-
39
- p.on_error do |e|
40
- if request
41
- request.error(e)
42
- else
43
- raise(e)
44
- end
45
- end
14
+ (previous = EM::DefaultDeferrable.new).succeed
15
+ request = nil
16
+
17
+ @parser.on_response { close_connection }
18
+
19
+ @parser.on_request do |verb, uri|
20
+ previous = request if request
21
+ request = Request.new(@app, verb, uri)
46
22
  end
47
23
 
48
- @builder = Hatetepe::Builder.new do |b|
49
- b.on_write &method(:send_data)
24
+ @parser.on_header do |name, value|
25
+ request.add_header name, value
26
+ end
27
+
28
+ @parser.on_headers_complete do
29
+ prev, req = previous, request
50
30
 
51
- b.on_complete do
52
- close_connection_after_writing if @requests.empty?
31
+ req.env["stream.start"] = proc do |response|
32
+ EM::Synchrony.sync prev
33
+
34
+ @builder.response response[0..1]
35
+ req.env["stream.send"] = proc do |chunk|
36
+ @builder.body chunk
37
+ end
38
+
39
+ req.env["stream.close"] = proc do
40
+ @builder.complete
41
+ req.succeed
42
+ close_connection_after_writing if req == request
43
+ end
53
44
  end
54
45
 
55
- b.on_error &method(:error)
46
+ req.process
56
47
  end
57
- end
58
-
59
- def write_responses
60
- while requests[0] && requests[0].response
61
- request = requests.shift
62
- builder.response(request.response)
48
+
49
+ @parser.on_body_chunk do |chunk|
50
+ request.add_body_chunk chunk
51
+ end
52
+
53
+ @parser.on_complete do
54
+ request.close_body
55
+ end
56
+
57
+ @builder.on_write do |data|
58
+ send_data data
63
59
  end
64
60
  end
65
61
 
66
62
  def receive_data(data)
67
63
  @parser << data
68
- rescue Exception => e
69
- error(e)
70
- end
71
-
72
- def error(e)
64
+ rescue Hatetepe::ParserError
65
+ close_connection
66
+ rescue Exception => ex
73
67
  if log
74
- log.error(e.message)
75
- log << e.backtrace.join("\n") + "\n"
68
+ log.error ex.message
69
+ log << ex.backtrace.map {|line| " #{line}" }.join("\n") + "\n"
76
70
  end
77
71
  close_connection
78
72
  end
@@ -1,77 +1,52 @@
1
- require "em/deferrable"
2
- require "fiber"
3
- require "stringio"
4
-
5
- require "rack"
6
- require "async-rack"
1
+ require "eventmachine"
2
+ require "em-synchrony"
3
+ require "rity/body"
7
4
 
8
5
  module Rity
9
6
  class Request
10
7
  include EM::Deferrable
11
8
 
12
- attr_reader :log
13
- attr_reader :app, :env, :response
9
+ attr_reader :app, :env
14
10
 
15
- def initialize(app, verb, url)
16
- @app = app
17
- @env = {
11
+ def initialize(app, verb, uri)
12
+ @app, @env = app, {
13
+ "rack.input" => Body.new,
18
14
  "REQUEST_METHOD" => verb,
19
- "REQUEST_URI" => url,
20
- "rack.input" => StringIO.new,
21
- "async.callback" => method(:postcall),
15
+ "REQUEST_URI" => uri,
16
+ "async.callback" => method(:postprocess),
17
+ # "proxy.start" => &method(:proxy_start),
18
+ # "proxy.callback" => &method(:proxy_start_reverse),
19
+ # "proxy.start_reverse" => &method(:proxy_start_reverse),
22
20
  }
23
21
  end
24
22
 
25
- def log=(log)
26
- @log = log
27
- @env["rack.logger"] = log
28
- end
29
-
30
- def add_body_chunk(chunk)
31
- env["rack.input"] << chunk
32
- end
33
-
34
23
  def add_header(name, value)
35
24
  key = "HTTP_" + name.upcase.gsub("-", "_")
36
- @env[key] = value
25
+ env[key] = value
37
26
  end
38
27
 
39
- def precall
40
- Fiber.new do
41
- rescue_errors { app.precall(env) }
42
- end.resume if app.respond_to?(:precall)
43
- end
44
-
45
- def call
46
- env["rack.input"].close
47
- env["rack.input"].rewind
48
-
49
- Fiber.new do
50
- rescue_errors { postcall(app.call(env)) }
51
- end.resume
28
+ def add_body_chunk(chunk)
29
+ env["rack.input"].write chunk
52
30
  end
53
31
 
54
- def postcall(response)
55
- return if @response || response[0] < 0
56
- @response = response
57
- succeed
32
+ def close_body
33
+ env["rack.input"].close_write
58
34
  end
59
35
 
60
- def error(e)
61
- if log
62
- log.error(e.message)
63
- log << e.backtrace.join("\n") + "\n"
64
- end
65
- postcall [500, {"Content-Type" => "text/html"},
66
- ["<h1>#{Hatetepe::STATUS_CODES[500]}</h1>"]]
36
+ def process
37
+ Fiber.new {
38
+ postprocess app.call(env)
39
+ }.resume
67
40
  end
68
41
 
69
- def rescue_errors
70
- begin
71
- yield
72
- rescue Exception => e
73
- error(e)
74
- end
42
+ def postprocess(response)
43
+ return if response[0] < 0
44
+
45
+ env["stream.start"].call response[0..1]
46
+ return if response[2] == Rack::STREAMING
47
+
48
+ response[2].each {|chunk| env["stream.send"].call chunk }
49
+ env["stream.close"].call
75
50
  end
76
51
  end
77
52
  end
@@ -1,3 +1,3 @@
1
1
  module Rity
2
- VERSION = "0.0.3"
2
+ VERSION = "0.1.0"
3
3
  end
data/myapp.rb CHANGED
@@ -1,20 +1,30 @@
1
1
  require "bundler"
2
2
  Bundler.setup :default
3
3
 
4
- require "rity"
4
+ require "logger"
5
+ require "eventmachine"
6
+ require "em-synchrony"
7
+ require "rity/new_connection"
5
8
  require "awesome_print"
6
9
 
7
- class MyApp
8
- def call(env)
9
- #raise "Error, Error!"
10
-
11
- response = [200, {"Content-Type" => "text/html"}, ["Hello!"]]
12
- return response
13
-
14
- EM.add_timer(2) { env["async.callback"].call(response) }
15
-
16
- [-1, {}, []]
17
- end
10
+ address, port = "127.0.0.1", 3000
11
+
12
+ app = proc {|env|
13
+ [200, {"Content-Type" => "text/html"}, ["You requested: #{env["REQUEST_URI"]}"]]
14
+ }
15
+
16
+ log = Logger.new($stderr)
17
+ log.formatter = proc do |severity, time, progname, message|
18
+ "[#{time}] #{severity}: #{message}\n"
18
19
  end
19
20
 
20
- Rity::Server.start "127.0.0.1", 3000, MyApp.new
21
+ log.info("Binding to #{address}:#{port}")
22
+
23
+ EM.synchrony do
24
+ trap("INT") { EM.stop }
25
+ trap("TERM") { EM.stop }
26
+
27
+ EM.epoll
28
+
29
+ EM.start_server address, port, Rity::Connection, app, log
30
+ end
@@ -0,0 +1,78 @@
1
+ require "spec_helper"
2
+
3
+ describe Rity::Body do
4
+ let(:body) { Rity::Body.new }
5
+
6
+ it "is deferrable" do
7
+ body.should respond_to(:callback)
8
+ end
9
+
10
+ describe "#close_write" do
11
+ it "closes the body for writing" do
12
+ body.close_write
13
+ expect { body.write "" }.to raise_error
14
+ end
15
+
16
+ it "succeeds the deferrable" do
17
+ body.should_receive :succeed
18
+ body.close_write
19
+ end
20
+ end
21
+
22
+ describe "#write" do
23
+ it "writes data to the IO" do
24
+ body.write "asdf"
25
+ body.string.should == "asdf"
26
+ end
27
+
28
+ it "yields the data to the receiver block if any" do
29
+ str = nil
30
+ Fiber.new {
31
+ body.receive {|s| str = s }
32
+ }.resume
33
+
34
+ body.write "asdf"
35
+ str.should == "asdf"
36
+ end
37
+ end
38
+
39
+ describe "#receive" do
40
+ it "yields each line that has already been written" do
41
+ body.write "as\ndf"
42
+ lines = []
43
+ Fiber.new {
44
+ body.receive {|l| lines << l }
45
+ }.resume
46
+
47
+ body.close_write
48
+ lines.should == ["as\n", "df"]
49
+ end
50
+
51
+ it "saves the receiver block for future writes" do
52
+ chunks = []
53
+ Fiber.new {
54
+ body.receive {|c| chunks << c }
55
+ }.resume
56
+
57
+ body.write "as"
58
+ chunks.should == ["as"]
59
+ body.write "df"
60
+ chunks.should == ["as", "df"]
61
+ body.close_write
62
+ end
63
+ end
64
+
65
+ describe "#sync" do
66
+ it "waits until the body is closed for writing" do
67
+ closed = false
68
+ Fiber.new {
69
+ body.sync
70
+ expect { body.write "" }.to raise_error
71
+ closed = true
72
+ }.resume
73
+
74
+ body.close_write
75
+ closed.should be_true
76
+ end
77
+ end
78
+ end
@@ -1,112 +1,181 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Rity::Connection do
4
- before do
5
- @conn = Rity::Connection.new("blah")
6
- @app = stub("app")
7
- @conn.app = @app
8
- end
4
+ let(:conn) { Class.new { include Rity::Connection }.new }
5
+ let(:app) { mock "app" }
6
+ let(:log) { mock "log" }
7
+ let(:parser) { Hatetepe::Parser.new }
8
+ let(:builder) { Hatetepe::Builder.new }
9
+ let(:requests) { [] }
9
10
 
10
- it "has an app, requests and a logger" do
11
- @conn.app.should == @app
12
- @conn.should have(0).requests
11
+ before do
12
+ Hatetepe::Parser.stub :new => parser
13
+ Hatetepe::Builder.stub :new => builder
13
14
 
14
- log = stub("log")
15
- @conn.log = log
16
- @conn.log.should == log
17
- end
18
-
19
- it "passes incoming data to the parser" do
20
- @conn.parser.should_receive(:<<).with("asdfg")
21
- @conn.receive_data("asdfg")
15
+ Rity::Request.singleton_class.send :alias_method, :spec_original_new, :new
16
+ Rity::Request.stub :new do |*args|
17
+ requests << request = Rity::Request.spec_original_new(*args)
18
+ request
19
+ end
20
+
21
+ conn.post_init
22
22
  end
23
23
 
24
- it "closes the connection if parsing fails" do
25
- @conn.should_receive(:close_connection)
26
- @conn.receive_data("this will definitely fail!")
24
+ it "can have an app and a logger" do
25
+ conn.app, conn.log = app, log
26
+ conn.app.should == app
27
+ conn.log.should == log
27
28
  end
28
29
 
29
- it "creates a new request as soon as headers are parsed" do
30
- request, log = stub("request", :callback => nil), stub("log")
31
- @conn.log = log
30
+ describe "#receive_data" do
31
+ it "feeds the parser with data" do
32
+ parser.should_receive(:<<).with "asdf"
33
+ conn.receive_data "asdf"
34
+ end
32
35
 
33
- Rity::Request.should_receive(:new).with(@conn.app, "GET", "/").and_return(request)
34
- request.should_receive(:log=).with(log)
36
+ it "closes the connection if parsing fails" do
37
+ parser.stub(:<<) {|data| raise Hatetepe::ParserError, "parser error" }
38
+ conn.should_receive :close_connection
39
+
40
+ conn.receive_data "asdf"
41
+ end
35
42
 
36
- @conn.parser.on_request[0].call("GET", "/")
37
- @conn.requests[0].should == request
38
- end
39
-
40
- it "adds the responder as request's callback" do
41
- @conn.should_receive(:write_responses)
42
- @conn.parser.on_request[0].call("GET", "/")
43
- @conn.requests[0].succeed
44
- end
45
-
46
- it "adds each parsed header to the request" do
47
- @conn.requests.push(Rity::Request.new(nil, nil, nil))
48
- @conn.requests.last.should_receive(:add_header).with("Asd", "123")
49
- @conn.parser.on_header[0].call("Asd", "123")
50
- end
51
-
52
- it "calls the request's #precall method when headers are finished" do
53
- @conn.requests.push(Rity::Request.new(nil, nil, nil))
54
- @conn.requests.last.should_receive(:precall)
55
- @conn.parser.on_headers_complete[0].call
56
- end
57
-
58
- it "adds each parsed body chunk to the request" do
59
- @conn.requests.push(Rity::Request.new(nil, nil, nil))
60
- @conn.requests.last.should_receive(:add_body_chunk).with("asdf")
61
- @conn.parser.on_body_chunk[0].call("asdf")
43
+ it "logs and closes the connection if other errors happen" do
44
+ parser.stub(:<<) {|data| raise "some other error" }
45
+ conn.should_receive :close_connection
46
+
47
+ conn.log = log
48
+ log.should_receive(:error).with "some other error"
49
+ log.should_receive(:<<)
50
+
51
+ conn.receive_data "asdf"
52
+ end
62
53
  end
63
54
 
64
- it "calls the request's #call method when the whole message is parsed" do
65
- @conn.requests.push(Rity::Request.new(nil, nil, nil))
66
- @conn.requests.last.should_receive(:call)
67
- @conn.parser.on_complete[0].call()
55
+ describe "the parser" do
56
+ it "closes the connection if a response comes in" do
57
+ conn.should_receive :close_connection
58
+ parser.on_response[0].call
59
+ end
60
+
61
+ it "creates a request object for each incoming request" do
62
+ parser.on_request[0].call "GET", "/"
63
+ requests[0].env["REQUEST_METHOD"].should == "GET"
64
+ requests[0].env["REQUEST_URI"].should == "/"
65
+ end
66
+
67
+ it "adds each header to the request object" do
68
+ parser.on_request[0].call "GET", "/"
69
+ requests[0].should_receive(:add_header).with "Key", "value"
70
+ parser.on_header[0].call "Key", "value"
71
+ end
72
+
73
+ it "processes the request when the headers are complete" do
74
+ parser.on_request[0].call "GET", "/"
75
+ requests[0].should_receive :process
76
+ parser.on_headers_complete[0].call
77
+ end
78
+
79
+ it "adds the stream.start proc to the request before processing" do
80
+ parser.on_request[0].call "GET", "/"
81
+ requests[0].stub :process
82
+ parser.on_headers_complete[0].call
83
+ requests[0].env["stream.start"].should respond_to(:call)
84
+ end
68
85
  end
69
86
 
70
- describe "the responder" do
71
- it "pushes the request's response to the builder" do
72
- @conn.requests.push(Rity::Request.new(nil, nil, nil))
73
- response = stub("response")
74
- @conn.requests[0].stub(:response => response)
75
-
76
- @conn.builder.should_receive(:response).with(response)
77
- @conn.write_responses
78
- end
87
+ describe "response stream" do
88
+ let(:env) { requests[0].env }
79
89
 
80
- it "doesn't push the response until it's ready" do
81
- @conn.requests.push(Rity::Request.new(nil, nil, nil))
82
- @conn.builder.should_not_receive(:response)
83
- @conn.write_responses
90
+ before do
91
+ conn.stub :send_data
92
+
93
+ parser.on_request[0].call "GET", "/"
94
+ requests[0].stub :process
95
+ parser.on_headers_complete[0].call
84
96
  end
85
97
 
86
- it "pushes responses in the order the resp. requests came in" do
87
- request1, request2 = mock("request1"), mock("request2")
88
- response1, response2 = stub("response1"), stub("response2")
89
- @conn.requests.push(request1, request2)
98
+ describe "stream.start" do
99
+ it "waits until the previous request is finished" do
100
+ expected_responses = []
101
+ 2.times do |i|
102
+ response = stub "response##{i}"
103
+ response.stub :[] => response
104
+ expected_responses << response
105
+ end
106
+
107
+ actual_responses = []
108
+ builder.stub :response do |response|
109
+ actual_responses << response
110
+ end
111
+
112
+ parser.on_request[0].call "GET", "/another"
113
+ requests[1].stub :process
114
+ parser.on_headers_complete[0].call
115
+
116
+ Fiber.new {
117
+ requests[1].env["stream.start"].call expected_responses[1]
118
+ }.resume
119
+ Fiber.new {
120
+ requests[0].env["stream.start"].call expected_responses[0]
121
+ }.resume
122
+
123
+ requests[0].env["stream.close"].call
124
+ actual_responses.should == expected_responses
125
+ end
90
126
 
91
- request1.stub(:response => nil)
92
- request2.stub(:response => response2)
93
- @conn.builder.should_not_receive(:response)
94
- @conn.write_responses
127
+ it "passes response status and headers to the builder" do
128
+ response = [200, {"Key" => "value"}, []]
129
+ builder.should_receive(:response).with response[0..1]
130
+ requests[0].env["stream.start"].call response
131
+ end
95
132
 
96
- request1.stub(:response => response1)
97
- request2.stub(:response => nil)
98
- @conn.builder.rspec_reset
99
- @conn.builder.should_receive(:response).with(response1)
100
- @conn.write_responses
133
+ it "sets stream.send and stream.close" do
134
+ env["stream.send"].should be_nil
135
+ env["stream.close"].should be_nil
136
+
137
+ env["stream.start"].call [200, {}, []]
138
+ env["stream.send"].should respond_to(:call)
139
+ env["stream.close"].should respond_to(:call)
140
+ end
141
+ end
142
+
143
+ describe "stream.send" do
144
+ it "passes a body chunk to the builder" do
145
+ env["stream.start"].call [200, {}, []]
146
+
147
+ builder.should_receive(:body).with "asdf"
148
+ env["stream.send"].call "asdf"
149
+ end
150
+ end
151
+
152
+ describe "stream.close" do
153
+ before do
154
+ env["stream.start"].call [200, {}, []]
155
+ conn.stub :close_connection_after_writing
156
+ end
101
157
 
102
- @conn.requests.should_not include(request1)
158
+ it "completes building the response" do
159
+ builder.should_receive :complete
160
+ env["stream.close"].call
161
+ end
103
162
 
104
- request2.stub(:response => response2)
105
- @conn.builder.rspec_reset
106
- @conn.builder.should_receive(:response).with(response2)
107
- @conn.write_responses
163
+ it "finishes the request" do
164
+ requests[0].should_receive :succeed
165
+ env["stream.close"].call
166
+ end
108
167
 
109
- @conn.requests.should be_empty
168
+ it "closes the connection" do
169
+ conn.should_receive :close_connection_after_writing
170
+ env["stream.close"].call
171
+ end
172
+ end
173
+ end
174
+
175
+ describe "the builder" do
176
+ it "writes to the connection" do
177
+ conn.should_receive(:send_data).with "asdf"
178
+ builder.on_write[0].call "asdf"
110
179
  end
111
180
  end
112
181
  end
@@ -1,132 +1,103 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Rity::Request do
4
- before do
5
- @app = proc {|env|}
6
- @request = Rity::Request.new(@app, "GET", "/")
7
- end
4
+ let(:app) { mock "app" }
5
+ let(:request) { Rity::Request.new app, "GET", "/" }
8
6
 
9
- it "is deferrable" do
10
- @request.should respond_to(:callback)
11
- @request.should respond_to(:succeed)
7
+ describe "#initialize" do
8
+ it "sets the app" do
9
+ request.app.should == app
10
+ end
11
+
12
+ it "initializes the env hash" do
13
+ request.env["REQUEST_METHOD"].should == "GET"
14
+ request.env["REQUEST_URI"].should == "/"
15
+ request.env["rack.input"].should be_kind_of(Rity::Body)
16
+ request.env["async.callback"].should == request.method(:postprocess)
17
+ end
12
18
  end
13
19
 
14
- it "initializes the env hash" do
15
- @request.env["REQUEST_METHOD"].should == "GET"
16
- @request.env["REQUEST_URI"].should == "/"
17
- StringIO.should === @request.env["rack.input"]
18
- @request.env["async.callback"].should respond_to(:call)
20
+ describe "#add_header" do
21
+ it "adds a header to the env hash" do
22
+ request.add_header "Content-Type", "text/html"
23
+ request.env["HTTP_CONTENT_TYPE"].should == "text/html"
24
+ end
19
25
  end
20
26
 
21
- it "puts the logger into the env hash" do
22
- log = stub("log")
23
- @request.log = log
24
- @request.env["rack.logger"].should == log
27
+ describe "#add_body_chunk" do
28
+ it "adds a chunk to the body input" do
29
+ request.env["rack.input"].should_receive(:write).with "asdf"
30
+ request.add_body_chunk "asdf"
31
+ end
25
32
  end
26
33
 
27
- it "calls the app and stores its response" do
28
- response = [303, {"Content-Type" => "text/html"}, ["foo bar"]]
29
- @app.should_receive(:call).with(@request.env).and_return(response)
30
- @request.should_receive(:succeed)
31
- @request.call
32
- @request.response.should == response
34
+ describe "#close_body" do
35
+ it "closes the body input for writing" do
36
+ request.env["rack.input"].should_receive(:close_write)
37
+ request.close_body
38
+ end
33
39
  end
34
40
 
35
- it "fetches async responses" do
36
- response = [-1, {}, []]
37
- async_response = [200, {}, "okokok"]
38
- @app.should_receive(:call).with(@request.env).and_return(response)
41
+ describe "#process" do
42
+ let(:response) { [-1, {}, []] }
39
43
 
40
- @request.call
44
+ before { @outer_fiber = Fiber.current }
41
45
 
42
- @request.should_receive(:succeed)
43
- @request.env["async.callback"].call(async_response)
44
- @request.response.should == async_response
45
- end
46
-
47
- it "calls the app in a separate fiber" do
48
- outer_fiber = Fiber.current
49
- inner_fiber = nil
50
- @app.singleton_class.send(:define_method, :call) do |env|
51
- inner_fiber = Fiber.current
46
+ it "calls the app in a separate fiber" do
47
+ app.should_receive :call do |env|
48
+ request.env.should == env
49
+ @outer_fiber.should_not == Fiber.current
50
+ response
51
+ end
52
+
53
+ request.process
52
54
  end
53
- @request.call
54
55
 
55
- outer_fiber.should_not == inner_fiber
56
- end
57
-
58
- it "rescues errors in app's #call" do
59
- @app.should_receive(:call) do
60
- raise "error"
56
+ it "hands the response to post-processing in the same fiber" do
57
+ app.stub :call => response
58
+ request.should_receive :postprocess do |resp|
59
+ response.should == resp
60
+ @outer_fiber.should_not == Fiber.current
61
+ end
62
+
63
+ request.process
61
64
  end
62
- @request.should_receive(:succeed)
63
- @request.call
64
-
65
- @request.response[0].should == 500
66
- @request.response[1].should == {"Content-Type" => "text/html"}
67
- @request.response[2].should == ["<h1>Internal Server Error</h1>"]
68
65
  end
69
66
 
70
- it "rescues errors in app's #precall" do
71
- @app.should_receive(:precall) do
72
- raise "error"
67
+ describe "#postprocess" do
68
+ let :response do
69
+ [200, {"Content-Type" => "text/html"}, ["Hello", "World!"]]
73
70
  end
74
- @request.should_receive(:succeed)
75
- @request.precall
76
71
 
77
- @request.response[0].should == 500
78
- @request.response[1].should == {"Content-Type" => "text/html"}
79
- @request.response[2].should == ["<h1>Internal Server Error</h1>"]
80
- end
81
-
82
- it "calls the app's #precall method if it exists" do
83
- called = false
84
- request = @request
85
- @app.singleton_class.send(:define_method, :precall) do |env|
86
- env.should == request.env
87
- called = true
72
+ before do
73
+ request.env.merge!({
74
+ "stream.start" => mock("stream.start"),
75
+ "stream.send" => mock("stream.send"),
76
+ "stream.close" => mock("stream.close")
77
+ })
88
78
  end
89
79
 
90
- @app.stub(:respond_to? => false)
91
- @request.precall
92
- called.should be_false
93
-
94
- @app.stub(:respond_to? => true)
95
- @request.precall
96
- called.should be_true
97
- end
98
-
99
- it "calls the app's #precall method in a separate fiber" do
100
- outer_fiber = Fiber.current
101
- inner_fiber = nil
102
- @app.singleton_class.send(:define_method, :precall) do |env|
103
- inner_fiber = Fiber.current
80
+ it "ignores responses with a status < 0" do
81
+ response[0] = -1
82
+ request.env["stream.start"].should_not_receive :call
83
+
84
+ request.postprocess response
104
85
  end
105
- @request.precall
106
-
107
- outer_fiber.should_not == inner_fiber
108
- end
109
-
110
- it "adds headers to the env hash" do
111
- @request.add_header("Content-Type", "text/html; charset=utf-8")
112
- @request.env["HTTP_CONTENT_TYPE"].should == "text/html; charset=utf-8"
113
- end
114
-
115
- it "adds data to the body" do
116
- @request.add_body_chunk "asdf"
117
- @request.env["rack.input"].string.should == "asdf"
118
86
 
119
- @request.add_body_chunk "foo"
120
- @request.env["rack.input"].string.should == "asdffoo"
121
- end
122
-
123
- it "rewinds the body and closes its input before calling the app" do
124
- @request.add_body_chunk "asdf"
125
- @app.should_receive(:call) do |env|
126
- env["rack.input"].closed_write?.should be_true
127
- env["rack.input"].pos.should == 0
87
+ it "only sends response line and headers for streaming responses" do
88
+ response[2] = Rack::STREAMING
89
+ request.env["stream.start"].should_receive(:call).with response[0..1]
90
+
91
+ request.postprocess response
128
92
  end
129
93
 
130
- @request.call
94
+ it "sends the response and closes the stream" do
95
+ request.env["stream.start"].should_receive(:call).with response[0..1]
96
+ request.env["stream.send"].should_receive(:call).with response[2][0]
97
+ request.env["stream.send"].should_receive(:call).with response[2][1]
98
+ request.env["stream.close"].should_receive :call
99
+
100
+ request.postprocess response
101
+ end
131
102
  end
132
103
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: rity
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.3
5
+ version: 0.1.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Lars Gierth
@@ -10,10 +10,11 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-06-12 00:00:00 Z
13
+ date: 2011-06-19 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: eventmachine
17
+ prerelease: false
17
18
  requirement: &id001 !ruby/object:Gem::Requirement
18
19
  none: false
19
20
  requirements:
@@ -21,10 +22,10 @@ dependencies:
21
22
  - !ruby/object:Gem::Version
22
23
  version: "0"
23
24
  type: :runtime
24
- prerelease: false
25
25
  version_requirements: *id001
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: em-synchrony
28
+ prerelease: false
28
29
  requirement: &id002 !ruby/object:Gem::Requirement
29
30
  none: false
30
31
  requirements:
@@ -32,10 +33,10 @@ dependencies:
32
33
  - !ruby/object:Gem::Version
33
34
  version: "0"
34
35
  type: :runtime
35
- prerelease: false
36
36
  version_requirements: *id002
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: hatetepe
39
+ prerelease: false
39
40
  requirement: &id003 !ruby/object:Gem::Requirement
40
41
  none: false
41
42
  requirements:
@@ -43,10 +44,10 @@ dependencies:
43
44
  - !ruby/object:Gem::Version
44
45
  version: "0"
45
46
  type: :runtime
46
- prerelease: false
47
47
  version_requirements: *id003
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: rack
50
+ prerelease: false
50
51
  requirement: &id004 !ruby/object:Gem::Requirement
51
52
  none: false
52
53
  requirements:
@@ -54,10 +55,10 @@ dependencies:
54
55
  - !ruby/object:Gem::Version
55
56
  version: "0"
56
57
  type: :runtime
57
- prerelease: false
58
58
  version_requirements: *id004
59
59
  - !ruby/object:Gem::Dependency
60
60
  name: async-rack
61
+ prerelease: false
61
62
  requirement: &id005 !ruby/object:Gem::Requirement
62
63
  none: false
63
64
  requirements:
@@ -65,10 +66,10 @@ dependencies:
65
66
  - !ruby/object:Gem::Version
66
67
  version: "0"
67
68
  type: :runtime
68
- prerelease: false
69
69
  version_requirements: *id005
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: thor
72
+ prerelease: false
72
73
  requirement: &id006 !ruby/object:Gem::Requirement
73
74
  none: false
74
75
  requirements:
@@ -76,10 +77,10 @@ dependencies:
76
77
  - !ruby/object:Gem::Version
77
78
  version: "0"
78
79
  type: :runtime
79
- prerelease: false
80
80
  version_requirements: *id006
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: rspec
83
+ prerelease: false
83
84
  requirement: &id007 !ruby/object:Gem::Requirement
84
85
  none: false
85
86
  requirements:
@@ -87,10 +88,10 @@ dependencies:
87
88
  - !ruby/object:Gem::Version
88
89
  version: "0"
89
90
  type: :development
90
- prerelease: false
91
91
  version_requirements: *id007
92
92
  - !ruby/object:Gem::Dependency
93
93
  name: fakefs
94
+ prerelease: false
94
95
  requirement: &id008 !ruby/object:Gem::Requirement
95
96
  none: false
96
97
  requirements:
@@ -98,10 +99,10 @@ dependencies:
98
99
  - !ruby/object:Gem::Version
99
100
  version: "0"
100
101
  type: :development
101
- prerelease: false
102
102
  version_requirements: *id008
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: em-http-request
105
+ prerelease: false
105
106
  requirement: &id009 !ruby/object:Gem::Requirement
106
107
  none: false
107
108
  requirements:
@@ -109,7 +110,6 @@ dependencies:
109
110
  - !ruby/object:Gem::Version
110
111
  version: "0"
111
112
  type: :development
112
- prerelease: false
113
113
  version_requirements: *id009
114
114
  description: Rity is a lightweight Ruby webserver that runs inside an EventMachine loop and puts each request into a fiber.
115
115
  email:
@@ -122,12 +122,15 @@ extra_rdoc_files: []
122
122
 
123
123
  files:
124
124
  - .rspec
125
+ - .yardopts
125
126
  - Gemfile
126
127
  - LICENSE
127
128
  - README.md
128
129
  - Rakefile
129
130
  - bin/rity
131
+ - config.ru
130
132
  - lib/rity.rb
133
+ - lib/rity/body.rb
131
134
  - lib/rity/cli.rb
132
135
  - lib/rity/connection.rb
133
136
  - lib/rity/request.rb
@@ -136,6 +139,7 @@ files:
136
139
  - rity.gemspec
137
140
  - spec/integration/start_spec.rb
138
141
  - spec/spec_helper.rb
142
+ - spec/unit/body_spec.rb
139
143
  - spec/unit/connection_spec.rb
140
144
  - spec/unit/request_spec.rb
141
145
  homepage: https://github.com/lgierth/rity
@@ -151,18 +155,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
151
155
  requirements:
152
156
  - - ">="
153
157
  - !ruby/object:Gem::Version
154
- hash: -888247411
155
- segments:
156
- - 0
157
158
  version: "0"
158
159
  required_rubygems_version: !ruby/object:Gem::Requirement
159
160
  none: false
160
161
  requirements:
161
162
  - - ">="
162
163
  - !ruby/object:Gem::Version
163
- hash: -888247411
164
- segments:
165
- - 0
166
164
  version: "0"
167
165
  requirements: []
168
166