falcon 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66c04e989c96c5f1697da180ebe132a455329f0e803540715ca8f3a2bfcb84fb
4
- data.tar.gz: ae0cc071336afeee37f05b9612a3a570031c0f48025d2199c6c1f0bf61224de6
3
+ metadata.gz: 24e547afb939a6ed6819928e4b2a38dc8044d13d5009f065ee445bb57be69547
4
+ data.tar.gz: ac02c9762122bcac8d751e1d3b494212c85187988b54a071f6965bd5041064ed
5
5
  SHA512:
6
- metadata.gz: b92e6e896f8db1cce3bffa327ccf3273e7bf56e15e83d5f88b915d489df7070140ab7ffcac10efdb6fee1ed53dedc7342af89c0ad8c62eb7cae8bd7223c65bb9
7
- data.tar.gz: 1360099213526fc4a8c50fb5595acc13f173552c655b3fb79716cfcdd535d3d9a9e674d540f61c3b841e3549ba04780218e28331283789d28a9f36cca21a7600
6
+ metadata.gz: 86109950c4f6d80e86d23396a646a31588190172cfd7d9a2f6ee5d05f4156923c71b5ad55cd764a3bf3bded3b4dcf407c84c2410bccb87a3d1fc122df695b401
7
+ data.tar.gz: e21aa13ba9ab5421c00dfbd3e1d00975d931ef879571448853bbb0870412648e219888e62a5a5f22b0b61ff714daf507bc12fe91512b3b3a74cf4f101d3df41b
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env falcon --verbose serve -c
2
2
 
3
+ require 'rack'
4
+
3
5
  def bottles(n)
4
6
  n == 1 ? "#{n} bottle" : "#{n} bottles"
5
7
  end
@@ -7,25 +9,31 @@ end
7
9
  # Browsers don't show streaming content until a certain threshold has been met. For most browers, it's about 1024 bytes. So, we have a comment of about that length which we feed to the client before streaming actual content. For more details see https://stackoverflow.com/questions/16909227
8
10
  COMMENT = "<!--#{'-' * 1024}-->"
9
11
 
12
+ # To test this example with the curl command-line tool, you'll need to add the `--no-buffer` flag or set the COMMENT size to the required 4096 bytes in order for curl to start streaming.
13
+ # curl http://localhost:9292/ --no-buffer
14
+
10
15
  run lambda {|env|
11
16
  task = Async::Task.current
12
- body = Async::HTTP::Body.new
17
+ body = Async::HTTP::Body::Writable.new
18
+
19
+ request = Rack::Request.new(env)
20
+ count = (request.params['count'] || 99).to_i
13
21
 
14
- body.write("<!DOCTYPE html><html><head><title>99 Bottles of Beer</title></head><body>")
22
+ body.write("<!DOCTYPE html><html><head><title>#{count} Bottles of Beer</title></head><body>")
15
23
 
16
24
  task.async do |task|
17
25
  body.write(COMMENT)
18
26
 
19
- 99.downto(1) do |i|
27
+ count.downto(1) do |i|
20
28
  puts "#{bottles(i)} of beer on the wall..."
21
29
  body.write("<p>#{bottles(i)} of beer on the wall, ")
22
- task.sleep(1)
30
+ task.sleep(0.1)
23
31
  body.write("#{bottles(i)} of beer, ")
24
- task.sleep(1)
32
+ task.sleep(0.1)
25
33
  body.write("take one down and pass it around, ")
26
- task.sleep(1)
34
+ task.sleep(0.1)
27
35
  body.write("#{bottles(i-1)} of beer on the wall.</p>")
28
- task.sleep(1)
36
+ task.sleep(0.1)
29
37
  body.write("<script>var child; while (child = document.body.firstChild) child.remove();</script>")
30
38
  end
31
39
 
data/falcon.gemspec CHANGED
@@ -1,4 +1,4 @@
1
- # coding: utf-8
1
+
2
2
  require_relative 'lib/falcon/version'
3
3
 
4
4
  Gem::Specification.new do |spec|
@@ -17,8 +17,8 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ["lib"]
18
18
 
19
19
  spec.add_dependency("async-io", "~> 1.6")
20
- spec.add_dependency("async-http", "~> 0.17")
21
- spec.add_dependency("async-container", "~> 0.3")
20
+ spec.add_dependency("async-http", "~> 0.19.0")
21
+ spec.add_dependency("async-container", "~> 0.4.0")
22
22
 
23
23
  spec.add_dependency("rack", ">= 1.0")
24
24
 
data/lib/falcon.rb CHANGED
@@ -19,3 +19,4 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require_relative "falcon/command"
22
+ require_relative "falcon/server"
@@ -0,0 +1,111 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'async/http/body'
22
+
23
+ module Falcon
24
+ module Adapters
25
+ class Input
26
+ def initialize(body)
27
+ @body = body
28
+ @chunks = []
29
+
30
+ @index = 0
31
+ @buffer = Async::IO::BinaryString.new
32
+ @finished = false
33
+ end
34
+
35
+ def each(&block)
36
+ return to_enum unless block_given?
37
+
38
+ while chunk = read_next
39
+ yield chunk
40
+ end
41
+
42
+ @closed = true
43
+ end
44
+
45
+ def rewind
46
+ @index = 0
47
+ @finished = false
48
+ @buffer.clear
49
+ end
50
+
51
+ def read(length = nil, buffer = nil)
52
+ if length
53
+ fill_buffer(length) if @buffer.bytesize <= length
54
+
55
+ return @buffer.slice!(0, length)
56
+ else
57
+ buffer ||= Async::IO::BinaryString.new
58
+
59
+ buffer << @buffer
60
+ @buffer.clear
61
+
62
+ while chunk = read_next
63
+ buffer << chunk
64
+ end
65
+
66
+ return buffer
67
+ end
68
+ end
69
+
70
+ def eof?
71
+ @finished and @buffer.empty?
72
+ end
73
+
74
+ def gets
75
+ read
76
+ end
77
+
78
+ def close
79
+ @body.finish
80
+ end
81
+
82
+ private
83
+
84
+ def read_next
85
+ return nil if @finished
86
+
87
+ chunk = nil
88
+
89
+ if @index < @chunks.count
90
+ chunk = @chunks[@index]
91
+ @index += 1
92
+ else
93
+ if chunk = @body.read
94
+ @chunks << chunk
95
+ @index += 1
96
+ end
97
+ end
98
+
99
+ @finished = true if chunk.nil?
100
+
101
+ return chunk
102
+ end
103
+
104
+ def fill_buffer(length)
105
+ while @buffer.bytesize < length and chunk = read_next
106
+ @buffer << chunk
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,111 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'input'
22
+
23
+ require 'async/http/body/buffered'
24
+ require 'async/logger'
25
+
26
+ module Falcon
27
+ module Adapters
28
+ class Rack
29
+ def initialize(app, logger = Async.logger)
30
+ @app = app
31
+ @logger = logger
32
+ end
33
+
34
+ def call(request, peer: nil, address: nil)
35
+ request_path, query_string = request.path.split('?', 2)
36
+ server_name, server_port = (request.authority || '').split(':', 2)
37
+
38
+ env = {
39
+ 'rack.version' => [2, 0, 0],
40
+
41
+ 'rack.input' => Input.new(request.body),
42
+ 'rack.errors' => $stderr,
43
+
44
+ 'rack.multithread' => true,
45
+ 'rack.multiprocess' => true,
46
+ 'rack.run_once' => false,
47
+
48
+ # The HTTP request method, such as “GET” or “POST”. This cannot ever be an empty string, and so is always required.
49
+ 'REQUEST_METHOD' => request.method,
50
+
51
+ # The initial portion of the request URL's “path” that corresponds to the application object, so that the application knows its virtual “location”. This may be an empty string, if the application corresponds to the “root” of the server.
52
+ 'SCRIPT_NAME' => '',
53
+
54
+ # The remainder of the request URL's “path”, designating the virtual “location” of the request's target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.
55
+ 'PATH_INFO' => request_path,
56
+
57
+ # The portion of the request URL that follows the ?, if any. May be empty, but is always required!
58
+ 'QUERY_STRING' => query_string || '',
59
+
60
+ # The server protocol, e.g. HTTP/1.1
61
+ 'SERVER_PROTOCOL' => request.version,
62
+ 'rack.url_scheme' => 'http',
63
+
64
+ # I'm not sure what sane defaults should be here:
65
+ 'SERVER_NAME' => server_name || '',
66
+ 'SERVER_PORT' => server_port || '',
67
+ }
68
+
69
+ if content_type = request.headers.delete('content-type')
70
+ env['CONTENT_TYPE'] = content_type
71
+ end
72
+
73
+ if content_length = request.headers.delete('content-length')
74
+ env['CONTENT_LENGTH'] = content_length
75
+ end
76
+
77
+ request.headers.each do |key, value|
78
+ env["HTTP_#{key.upcase.tr('-', '_')}"] = value
79
+ end
80
+
81
+ env['rack.hijack?'] = true
82
+ env['rack.hijack'] = lambda do
83
+ env['rack.hijack_io'] = peer
84
+ end
85
+
86
+ if remote_address = peer.remote_address
87
+ env['REMOTE_ADDR'] = remote_address.ip_address if remote_address.ip?
88
+ end
89
+
90
+ status, headers, body = @app.call(env)
91
+
92
+ # We normalize headers to be lower case.
93
+ headers = headers.map{|key, value| [key.downcase, value]}.to_h
94
+
95
+ if env['rack.hijack_io']
96
+ throw :hijack
97
+ else
98
+ return Async::HTTP::Response[status, headers, Async::HTTP::Body::Buffered.wrap(body)]
99
+ end
100
+ rescue => exception
101
+ @logger.error "#{exception.class}: #{exception.message}\n\t#{$!.backtrace.join("\n\t")}"
102
+
103
+ return failure_response(exception)
104
+ end
105
+
106
+ def failure_response(exception)
107
+ Async::HTTP::Response.for_exception(exception)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -20,6 +20,7 @@
20
20
 
21
21
  require_relative '../server'
22
22
  require_relative '../verbose'
23
+ require_relative '../adapters/rack'
23
24
 
24
25
  require 'async/container'
25
26
  require 'async/io/trap'
@@ -57,6 +58,11 @@ module Falcon
57
58
  def load_app(verbose)
58
59
  app, options = Rack::Builder.parse_file(@options[:config])
59
60
 
61
+ # We adapt the rack app to Async::HTTP::Middleware
62
+ app = Adapters::Rack.new(app)
63
+
64
+ app = Async::HTTP::ContentEncoding.new(app)
65
+
60
66
  if verbose
61
67
  app = Verbose.new(app)
62
68
  end
data/lib/falcon/server.rb CHANGED
@@ -18,9 +18,8 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require_relative 'input'
22
-
23
21
  require 'async/http/server'
22
+ require 'async/http/content_encoding'
24
23
 
25
24
  module Falcon
26
25
  class Server < Async::HTTP::Server
@@ -30,81 +29,8 @@ module Falcon
30
29
  @app = app
31
30
  end
32
31
 
33
- def logger
34
- Async.logger
35
- end
36
-
37
32
  def handle_request(request, peer, address)
38
- request_path, query_string = request.path.split('?', 2)
39
- server_name, server_port = (request.authority || '').split(':', 2)
40
-
41
- env = {
42
- 'rack.version' => [2, 0, 0],
43
-
44
- 'rack.input' => Input.new(request.body),
45
- 'rack.errors' => $stderr,
46
-
47
- 'rack.multithread' => true,
48
- 'rack.multiprocess' => true,
49
- 'rack.run_once' => false,
50
-
51
- # The HTTP request method, such as “GET” or “POST”. This cannot ever be an empty string, and so is always required.
52
- 'REQUEST_METHOD' => request.method,
53
-
54
- # The initial portion of the request URL's “path” that corresponds to the application object, so that the application knows its virtual “location”. This may be an empty string, if the application corresponds to the “root” of the server.
55
- 'SCRIPT_NAME' => '',
56
-
57
- # The remainder of the request URL's “path”, designating the virtual “location” of the request's target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.
58
- 'PATH_INFO' => request_path,
59
-
60
- # The portion of the request URL that follows the ?, if any. May be empty, but is always required!
61
- 'QUERY_STRING' => query_string || '',
62
-
63
- # The server protocol, e.g. HTTP/1.1
64
- 'SERVER_PROTOCOL' => request.version,
65
- 'rack.url_scheme' => 'http',
66
-
67
- # I'm not sure what sane defaults should be here:
68
- 'SERVER_NAME' => server_name || '',
69
- 'SERVER_PORT' => server_port || '',
70
- }
71
-
72
- if content_type = request.headers['content-type']
73
- env['CONTENT_TYPE'] = content_type
74
- end
75
-
76
- request.headers.each do |key, value|
77
- env["HTTP_#{key.upcase.tr('-', '_')}"] = value
78
- end
79
-
80
- env['rack.hijack?'] = true
81
- env['rack.hijack'] = lambda do
82
- env['rack.hijack_io'] = peer
83
- end
84
-
85
- if content_type = request.headers['HTTP_CONTENT_TYPE']
86
- env['CONTENT_TYPE'] = content_type
87
- end
88
-
89
- if remote_address = peer.remote_address
90
- env['REMOTE_ADDR'] = remote_address.ip_address if remote_address.ip?
91
- end
92
-
93
- response = @app.call(env)
94
-
95
- if env['rack.hijack_io']
96
- throw :hijack
97
- else
98
- return response
99
- end
100
- rescue => exception
101
- logger.error "#{exception.class}: #{exception.message}\n\t#{$!.backtrace.join("\n\t")}"
102
-
103
- return failure_response(exception)
104
- end
105
-
106
- def failure_response(exception)
107
- [500, {'Content-Type' => 'text/plain'}, ["Request failed due to: #{exception.class}: #{exception.message}"]]
33
+ @app.call(request, peer: peer, address: address)
108
34
  end
109
35
  end
110
36
  end
@@ -19,45 +19,39 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require 'async/logger'
22
+ require 'async/http/statistics'
22
23
 
23
24
  module Falcon
24
- class Verbose
25
+ class Verbose < Async::HTTP::Middleware
25
26
  def initialize(app, logger = Async.logger)
26
- @app = app
27
+ super(app)
28
+
27
29
  @logger = logger
28
30
  end
29
31
 
30
- def annotate(env, task = Async::Task.current)
31
- request_method = env['REQUEST_METHOD']
32
- request_path = env['PATH_INFO']
33
- remote_address = env['REMOTE_ADDR']
32
+ def annotate(request, peer: nil, address: nil)
33
+ task = Async::Task.current
34
+
35
+ # @logger.debug "#{request.method} #{request.path} #{request.version} from #{address.inspect}"
36
+ # @logger.debug request.headers.inspect
34
37
 
35
- task.annotate("#{request_method} #{request_path} for #{remote_address}")
38
+ task.annotate("#{request.method} #{request.path} from #{address.inspect}")
36
39
  end
37
40
 
38
- def log(start_time, env, response, error)
39
- duration = Time.now - start_time
41
+ def call(request, **options)
42
+ annotate(request, **options)
40
43
 
41
- request_method = env['REQUEST_METHOD']
42
- request_path = env['PATH_INFO']
43
- server_protocol = env['SERVER_PROTOCOL']
44
+ statistics = Async::HTTP::Statistics.start
44
45
 
45
- if response
46
- status, headers, body = response
47
- @logger.info "#{request_method} #{request_path} #{server_protocol} -> #{status}; Content length #{headers.fetch('Content-Length', '-')} bytes; took #{duration} seconds"
48
- else
49
- @logger.info "#{request_method} #{request_path} #{server_protocol} -> #{error}; took #{duration} seconds"
50
- end
51
- end
52
-
53
- def call(env)
54
- start_time = Time.now
46
+ response = super
55
47
 
56
- annotate(env)
48
+ statistics.wrap(response) do |statistics, error|
49
+ @logger.info "#{request.method} #{request.path} #{request.version} -> #{response.status}; #{statistics.inspect}"
50
+ # @logger.info response.headers.inspect
51
+ @logger.error "#{error.class}: #{error.message}" if error
52
+ end
57
53
 
58
- response = @app.call(env)
59
- ensure
60
- log(start_time, env, response, $!)
54
+ return response
61
55
  end
62
56
  end
63
57
  end
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Falcon
22
- VERSION = "0.12.0"
22
+ VERSION = "0.13.0"
23
23
  end
@@ -16,7 +16,7 @@ module Rack
16
16
  def self.run(app, **options)
17
17
  endpoint = endpoint_for(**options)
18
18
 
19
- server = ::Falcon::Server.new(app, endpoint)
19
+ server = ::Falcon::Server.new(::Falcon::Adapters::Rack.new(app), endpoint)
20
20
 
21
21
  Async::Reactor.run do
22
22
  server.run
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: falcon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-11 00:00:00.000000000 Z
11
+ date: 2018-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-io
@@ -30,28 +30,28 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.17'
33
+ version: 0.19.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.17'
40
+ version: 0.19.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: async-container
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.3'
47
+ version: 0.4.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.3'
54
+ version: 0.4.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rack
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -158,9 +158,10 @@ files:
158
158
  - examples/sinatra/config.ru
159
159
  - falcon.gemspec
160
160
  - lib/falcon.rb
161
+ - lib/falcon/adapters/input.rb
162
+ - lib/falcon/adapters/rack.rb
161
163
  - lib/falcon/command.rb
162
164
  - lib/falcon/command/serve.rb
163
- - lib/falcon/input.rb
164
165
  - lib/falcon/server.rb
165
166
  - lib/falcon/verbose.rb
166
167
  - lib/falcon/version.rb
data/lib/falcon/input.rb DELETED
@@ -1,109 +0,0 @@
1
- # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
20
-
21
- require 'async/http/server'
22
-
23
- module Falcon
24
- class Input
25
- def initialize(body)
26
- @body = body
27
- @chunks = []
28
-
29
- @index = 0
30
- @buffer = Async::IO::BinaryString.new
31
- @finished = false
32
- end
33
-
34
- def each(&block)
35
- return to_enum unless block_given?
36
-
37
- while chunk = read_next
38
- yield chunk
39
- end
40
-
41
- @closed = true
42
- end
43
-
44
- def rewind
45
- @index = 0
46
- @finished = false
47
- @buffer.clear
48
- end
49
-
50
- def read(length = nil, buffer = nil)
51
- if length
52
- fill_buffer(length) if @buffer.bytesize <= length
53
-
54
- return @buffer.slice!(0, length)
55
- else
56
- buffer ||= Async::IO::BinaryString.new
57
-
58
- buffer << @buffer
59
- @buffer.clear
60
-
61
- while chunk = read_next
62
- buffer << chunk
63
- end
64
-
65
- return buffer
66
- end
67
- end
68
-
69
- def eof?
70
- @finished and @buffer.empty?
71
- end
72
-
73
- def gets
74
- read
75
- end
76
-
77
- def close
78
- @body.finish
79
- end
80
-
81
- private
82
-
83
- def read_next
84
- return nil if @finished
85
-
86
- chunk = nil
87
-
88
- if @index < @chunks.count
89
- chunk = @chunks[@index]
90
- @index += 1
91
- else
92
- if chunk = @body.read
93
- @chunks << chunk
94
- @index += 1
95
- end
96
- end
97
-
98
- @finished = true if chunk.nil?
99
-
100
- return chunk
101
- end
102
-
103
- def fill_buffer(length)
104
- while @buffer.bytesize < length and chunk = read_next
105
- @buffer << chunk
106
- end
107
- end
108
- end
109
- end