rity 0.0.3 → 0.1.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.
@@ -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