protocol-http 0.33.0 → 0.34.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/http/body/buffered.rb +23 -2
- data/lib/protocol/http/body/deflate.rb +1 -1
- data/lib/protocol/http/body/digestable.rb +1 -1
- data/lib/protocol/http/body/file.rb +9 -7
- data/lib/protocol/http/body/inflate.rb +1 -1
- data/lib/protocol/http/body/readable.rb +20 -10
- data/lib/protocol/http/body/stream.rb +9 -7
- data/lib/protocol/http/body/streamable.rb +109 -68
- data/lib/protocol/http/body/writable.rb +61 -20
- data/lib/protocol/http/version.rb +1 -1
- data/readme.md +2 -0
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb1197435097557cab74b1b61b92fe0a15a938892dd07486ee9f9db06360decd
|
4
|
+
data.tar.gz: 78bcbbd73817b03ce63b55b2bbd4408afeaeaa127e3c19f9dfd5d6ddb120f05d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34db4e5940f59773708692bc080706be457f9f41445b6858e01da474f0ba36aba31d8d8c2f3ccd329f2120f412d9c4bcc42c568c057db3e0cef816c83ab34db1
|
7
|
+
data.tar.gz: 23baab35d26b5b367fa556d4a302f81680b944fef07b2ca0a581fab7b36ac8320cb3c4a62cdc50f8cfd21efb408eee01e348c06b4c538a5cfbcf8cd6ce99b28f
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -56,6 +56,17 @@ module Protocol
|
|
56
56
|
self
|
57
57
|
end
|
58
58
|
|
59
|
+
# Ensure that future reads return nil, but allow for rewinding.
|
60
|
+
def close(error = nil)
|
61
|
+
@index = @chunks.length
|
62
|
+
end
|
63
|
+
|
64
|
+
def clear
|
65
|
+
@chunks.clear
|
66
|
+
@length = 0
|
67
|
+
@index = 0
|
68
|
+
end
|
69
|
+
|
59
70
|
def length
|
60
71
|
@length ||= @chunks.inject(0) {|sum, chunk| sum + chunk.bytesize}
|
61
72
|
end
|
@@ -70,6 +81,8 @@ module Protocol
|
|
70
81
|
end
|
71
82
|
|
72
83
|
def read
|
84
|
+
return nil unless @chunks
|
85
|
+
|
73
86
|
if chunk = @chunks[@index]
|
74
87
|
@index += 1
|
75
88
|
|
@@ -81,18 +94,26 @@ module Protocol
|
|
81
94
|
@chunks << chunk
|
82
95
|
end
|
83
96
|
|
97
|
+
def close_write(error)
|
98
|
+
# Nothing to do.
|
99
|
+
end
|
100
|
+
|
84
101
|
def rewindable?
|
85
|
-
|
102
|
+
@chunks != nil
|
86
103
|
end
|
87
104
|
|
88
105
|
def rewind
|
106
|
+
return false unless @chunks
|
107
|
+
|
89
108
|
@index = 0
|
90
109
|
|
91
110
|
return true
|
92
111
|
end
|
93
112
|
|
94
113
|
def inspect
|
95
|
-
|
114
|
+
if @chunks
|
115
|
+
"\#<#{self.class} #{@chunks.size} chunks, #{self.length} bytes>"
|
116
|
+
end
|
96
117
|
end
|
97
118
|
end
|
98
119
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
4
|
+
# Copyright, 2019-2024, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative 'readable'
|
7
7
|
|
@@ -68,13 +68,15 @@ module Protocol
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
-
def stream?
|
72
|
-
|
73
|
-
end
|
71
|
+
# def stream?
|
72
|
+
# true
|
73
|
+
# end
|
74
74
|
|
75
|
-
def call(stream)
|
76
|
-
|
77
|
-
|
75
|
+
# def call(stream)
|
76
|
+
# IO.copy_stream(@file, stream, @remaining)
|
77
|
+
# ensure
|
78
|
+
# stream.close
|
79
|
+
# end
|
78
80
|
|
79
81
|
def join
|
80
82
|
return "" if @remaining == 0
|
@@ -17,7 +17,11 @@ module Protocol
|
|
17
17
|
#
|
18
18
|
# If you don't want to read from a stream, and instead want to close it immediately, you can call `close` on the body. If the body is already completely consumed, `close` will do nothing, but if there is still data to be read, it will cause the underlying stream to be reset (and possibly closed).
|
19
19
|
class Readable
|
20
|
-
# Close the stream immediately.
|
20
|
+
# Close the stream immediately. After invoking this method, the stream should be considered closed, and all internal resources should be released.
|
21
|
+
#
|
22
|
+
# If an error occured while handling the output, it can be passed as an argument. This may be propagated to the client, for example the client may be informed that the stream was not fully read correctly.
|
23
|
+
#
|
24
|
+
# Invoking `#read` after `#close` will return `nil`.
|
21
25
|
def close(error = nil)
|
22
26
|
end
|
23
27
|
|
@@ -68,13 +72,15 @@ module Protocol
|
|
68
72
|
def each
|
69
73
|
return to_enum unless block_given?
|
70
74
|
|
71
|
-
|
72
|
-
|
75
|
+
begin
|
76
|
+
while chunk = self.read
|
77
|
+
yield chunk
|
78
|
+
end
|
79
|
+
rescue => error
|
80
|
+
raise
|
81
|
+
ensure
|
82
|
+
self.close(error)
|
73
83
|
end
|
74
|
-
rescue => error
|
75
|
-
raise
|
76
|
-
ensure
|
77
|
-
self.close(error)
|
78
84
|
end
|
79
85
|
|
80
86
|
# Read all remaining chunks into a single binary string using `#each`.
|
@@ -98,12 +104,14 @@ module Protocol
|
|
98
104
|
false
|
99
105
|
end
|
100
106
|
|
101
|
-
#
|
107
|
+
# Invoke the body with the given stream.
|
102
108
|
#
|
103
|
-
#
|
109
|
+
# The default implementation simply writes each chunk to the stream. If the body is not ready, it will be flushed after each chunk. Closes the stream when finished or if an error occurs.
|
104
110
|
#
|
105
|
-
#
|
111
|
+
# Write the body to the given stream.
|
106
112
|
#
|
113
|
+
# @parameter stream [IO | Object] An `IO`-like object that responds to `#read`, `#write` and `#flush`.
|
114
|
+
# @returns [Boolean] Whether the ownership of the stream was transferred.
|
107
115
|
def call(stream)
|
108
116
|
self.each do |chunk|
|
109
117
|
stream.write(chunk)
|
@@ -113,6 +121,8 @@ module Protocol
|
|
113
121
|
stream.flush
|
114
122
|
end
|
115
123
|
end
|
124
|
+
ensure
|
125
|
+
stream.close
|
116
126
|
end
|
117
127
|
|
118
128
|
# Read all remaining chunks into a buffered body and close the underlying input.
|
@@ -21,6 +21,7 @@ module Protocol
|
|
21
21
|
|
22
22
|
# Will hold remaining data in `#read`.
|
23
23
|
@buffer = nil
|
24
|
+
|
24
25
|
@closed = false
|
25
26
|
@closed_read = false
|
26
27
|
end
|
@@ -251,21 +252,22 @@ module Protocol
|
|
251
252
|
end
|
252
253
|
|
253
254
|
# Close the input body.
|
254
|
-
def close_read
|
255
|
-
if @input
|
255
|
+
def close_read(error = nil)
|
256
|
+
if input = @input
|
257
|
+
@input = nil
|
256
258
|
@closed_read = true
|
257
259
|
@buffer = nil
|
258
260
|
|
259
|
-
|
260
|
-
@input = nil
|
261
|
+
input.close(error)
|
261
262
|
end
|
262
263
|
end
|
263
264
|
|
264
265
|
# Close the output body.
|
265
|
-
def close_write
|
266
|
-
if @output
|
267
|
-
@output&.close
|
266
|
+
def close_write(error = nil)
|
267
|
+
if output = @output
|
268
268
|
@output = nil
|
269
|
+
|
270
|
+
output.close_write(error)
|
269
271
|
end
|
270
272
|
end
|
271
273
|
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright,
|
4
|
+
# Copyright, 2019-2024, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative 'readable'
|
7
|
+
require_relative 'writable'
|
8
|
+
|
7
9
|
require_relative 'stream'
|
8
10
|
|
9
11
|
module Protocol
|
@@ -14,98 +16,137 @@ module Protocol
|
|
14
16
|
# In some cases, it's advantageous to directly read and write to the underlying stream if possible. For example, HTTP/1 upgrade requests, WebSockets, and similar. To handle that case, response bodies can implement `stream?` and return `true`. When `stream?` returns true, the body **should** be consumed by calling `call(stream)`. Server implementations may choose to always invoke `call(stream)` if it's efficient to do so. Bodies that don't support it will fall back to using `#each`.
|
15
17
|
#
|
16
18
|
# When invoking `call(stream)`, the stream can be read from and written to, and closed. However, the stream is only guaranteed to be open for the duration of the `call(stream)` call. Once the method returns, the stream **should** be closed by the server.
|
17
|
-
|
18
|
-
|
19
|
+
module Streamable
|
20
|
+
def self.new(*arguments)
|
21
|
+
if arguments.size == 1
|
22
|
+
DeferredBody.new(*arguments)
|
23
|
+
else
|
24
|
+
Body.new(*arguments)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.request(&block)
|
29
|
+
DeferredBody.new(block)
|
19
30
|
end
|
20
31
|
|
21
|
-
def
|
22
|
-
|
23
|
-
@input = input
|
24
|
-
@output = nil
|
32
|
+
def self.response(request, &block)
|
33
|
+
Body.new(block, request.body)
|
25
34
|
end
|
26
35
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
36
|
+
class Output
|
37
|
+
def self.schedule(input, block)
|
38
|
+
self.new(input, block).tap(&:schedule)
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(input, block)
|
42
|
+
@output = Writable.new
|
43
|
+
@stream = Stream.new(input, @output)
|
44
|
+
@block = block
|
45
|
+
end
|
46
|
+
|
47
|
+
def schedule
|
48
|
+
@fiber ||= Fiber.schedule do
|
49
|
+
@block.call(@stream)
|
50
|
+
end
|
32
51
|
end
|
33
52
|
|
34
|
-
|
35
|
-
@output.
|
53
|
+
def read
|
54
|
+
@output.read
|
55
|
+
end
|
56
|
+
|
57
|
+
def close(error = nil)
|
58
|
+
@output.close_write(error)
|
36
59
|
end
|
37
60
|
end
|
38
61
|
|
39
|
-
|
62
|
+
# Raised when a streaming body is consumed more than once.
|
63
|
+
class ConsumedError < StandardError
|
64
|
+
end
|
40
65
|
|
41
|
-
class
|
42
|
-
def initialize(
|
43
|
-
|
44
|
-
|
45
|
-
@
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
if
|
57
|
-
|
58
|
-
from.transfer(nil)
|
66
|
+
class Body < Readable
|
67
|
+
def initialize(block, input = nil)
|
68
|
+
@block = block
|
69
|
+
@input = input
|
70
|
+
@output = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def stream?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Invokes the block in a fiber which yields chunks when they are available.
|
78
|
+
def read
|
79
|
+
# We are reading chunk by chunk, allocate an output stream and execute the block to generate the chunks:
|
80
|
+
if @output.nil?
|
81
|
+
if @block.nil?
|
82
|
+
raise ConsumedError, "Streaming body has already been consumed!"
|
59
83
|
end
|
84
|
+
|
85
|
+
@output = Output.schedule(@input, @block)
|
86
|
+
@block = nil
|
60
87
|
end
|
88
|
+
|
89
|
+
@output.read
|
61
90
|
end
|
62
91
|
|
63
|
-
#
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
raise RuntimeError, "Stream is not being read!"
|
92
|
+
# Invoke the block with the given stream.
|
93
|
+
#
|
94
|
+
# The block can read and write to the stream, and must close the stream when finishing.
|
95
|
+
def call(stream)
|
96
|
+
if @block.nil?
|
97
|
+
raise ConsumedError, "Streaming block has already been consumed!"
|
70
98
|
end
|
99
|
+
|
100
|
+
block = @block
|
101
|
+
|
102
|
+
@input = @output = @block = nil
|
103
|
+
|
104
|
+
# Ownership of the stream is passed into the block, in other words, the block is responsible for closing the stream.
|
105
|
+
block.call(stream)
|
106
|
+
rescue => error
|
107
|
+
# If, for some reason, the block raises an error, we assume it may not have closed the stream, so we close it here:
|
108
|
+
stream.close
|
109
|
+
raise
|
71
110
|
end
|
72
111
|
|
73
|
-
#
|
112
|
+
# Closing a stream indicates we are no longer interested in reading from it.
|
74
113
|
def close(error = nil)
|
75
|
-
if
|
76
|
-
@
|
77
|
-
|
78
|
-
|
79
|
-
@fiber.raise(error || Closed)
|
114
|
+
if output = @output
|
115
|
+
@output = nil
|
116
|
+
# Closing the output here may take some time, as it may need to finish handling the stream:
|
117
|
+
output.close(error)
|
80
118
|
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def read
|
84
|
-
raise RuntimeError, "Stream is already being read!" if @from
|
85
119
|
|
86
|
-
@
|
120
|
+
if input = @input
|
121
|
+
@input = nil
|
122
|
+
input.close(error)
|
123
|
+
end
|
87
124
|
end
|
88
125
|
end
|
89
126
|
|
90
|
-
#
|
91
|
-
|
92
|
-
|
127
|
+
# A deferred body has an extra `stream` method which can be used to stream data into the body, as the response body won't be available until the request has been sent.
|
128
|
+
class DeferredBody < Body
|
129
|
+
def initialize(block)
|
130
|
+
super(block, Writable.new)
|
131
|
+
end
|
93
132
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
def call(stream)
|
102
|
-
raise "Streaming body has already been read!" if @output
|
133
|
+
# Closing a stream indicates we are no longer interested in reading from it, but in this case that does not mean that the output block is finished generating data.
|
134
|
+
def close(error = nil)
|
135
|
+
if error
|
136
|
+
super
|
137
|
+
end
|
138
|
+
end
|
103
139
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
140
|
+
# Stream the response body into the block's input.
|
141
|
+
def stream(body)
|
142
|
+
body&.each do |chunk|
|
143
|
+
@input.write(chunk)
|
144
|
+
end
|
145
|
+
rescue => error
|
146
|
+
raise
|
147
|
+
ensure
|
148
|
+
@input.close_write(error)
|
149
|
+
end
|
109
150
|
end
|
110
151
|
end
|
111
152
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright,
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative 'readable'
|
7
7
|
|
@@ -16,15 +16,9 @@ module Protocol
|
|
16
16
|
# @param [Integer] length The length of the response body if known.
|
17
17
|
# @param [Async::Queue] queue Specify a different queue implementation, e.g. `Async::LimitedQueue.new(8)` to enable back-pressure streaming.
|
18
18
|
def initialize(length = nil, queue: Thread::Queue.new)
|
19
|
-
@queue = queue
|
20
|
-
|
21
19
|
@length = length
|
22
|
-
|
20
|
+
@queue = queue
|
23
21
|
@count = 0
|
24
|
-
|
25
|
-
@finished = false
|
26
|
-
|
27
|
-
@closed = false
|
28
22
|
@error = nil
|
29
23
|
end
|
30
24
|
|
@@ -34,18 +28,16 @@ module Protocol
|
|
34
28
|
|
35
29
|
# Stop generating output; cause the next call to write to fail with the given error. Does not prevent existing chunks from being read. In other words, this indicates both that no more data will be or should be written to the body.
|
36
30
|
def close(error = nil)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@error = error
|
42
|
-
end
|
31
|
+
@error ||= error
|
32
|
+
|
33
|
+
@queue.clear
|
34
|
+
@queue.close
|
43
35
|
|
44
36
|
super
|
45
37
|
end
|
46
38
|
|
47
39
|
def closed?
|
48
|
-
@closed
|
40
|
+
@queue.closed?
|
49
41
|
end
|
50
42
|
|
51
43
|
def ready?
|
@@ -59,22 +51,71 @@ module Protocol
|
|
59
51
|
|
60
52
|
# Read the next available chunk.
|
61
53
|
def read
|
54
|
+
if @error
|
55
|
+
raise @error
|
56
|
+
end
|
57
|
+
|
62
58
|
@queue.pop
|
63
59
|
end
|
64
60
|
|
65
61
|
# Write a single chunk to the body. Signal completion by calling `#finish`.
|
66
62
|
def write(chunk)
|
67
|
-
|
68
|
-
# The inverse of this is less obvious (*)
|
69
|
-
if @closed
|
63
|
+
if @queue.closed?
|
70
64
|
raise(@error || Closed)
|
71
65
|
end
|
72
66
|
|
73
|
-
@count += 1
|
74
67
|
@queue.push(chunk)
|
68
|
+
@count += 1
|
75
69
|
end
|
76
70
|
|
77
|
-
|
71
|
+
def close_write(error = nil)
|
72
|
+
@error ||= error
|
73
|
+
@queue.close
|
74
|
+
end
|
75
|
+
|
76
|
+
class Output
|
77
|
+
def initialize(writable)
|
78
|
+
@writable = writable
|
79
|
+
@closed = false
|
80
|
+
end
|
81
|
+
|
82
|
+
def closed?
|
83
|
+
@closed || @writable.closed?
|
84
|
+
end
|
85
|
+
|
86
|
+
def write(chunk)
|
87
|
+
@writable.write(chunk)
|
88
|
+
end
|
89
|
+
|
90
|
+
alias << write
|
91
|
+
|
92
|
+
def close(error = nil)
|
93
|
+
@closed = true
|
94
|
+
|
95
|
+
if error
|
96
|
+
@writable.close(error)
|
97
|
+
else
|
98
|
+
@writable.close_write
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Create an output wrapper which can be used to write chunks to the body.
|
104
|
+
def output
|
105
|
+
output = Output.new(self)
|
106
|
+
|
107
|
+
unless block_given?
|
108
|
+
return output
|
109
|
+
end
|
110
|
+
|
111
|
+
begin
|
112
|
+
yield output
|
113
|
+
rescue => error
|
114
|
+
raise error
|
115
|
+
ensure
|
116
|
+
output.close(error)
|
117
|
+
end
|
118
|
+
end
|
78
119
|
|
79
120
|
def inspect
|
80
121
|
"\#<#{self.class} #{@count} chunks written, #{status}>"
|
data/readme.md
CHANGED
@@ -14,6 +14,8 @@ Provides abstractions for working with the HTTP protocol.
|
|
14
14
|
|
15
15
|
Please see the [project documentation](https://socketry.github.io/protocol-http/) for more details.
|
16
16
|
|
17
|
+
- [Streaming](https://socketry.github.io/protocol-http/guides/streaming/index) - This guide gives an overview of how to implement streaming requests and responses.
|
18
|
+
|
17
19
|
- [Getting Started](https://socketry.github.io/protocol-http/guides/getting-started/index) - This guide explains how to use `protocol-http` for building abstract HTTP interfaces.
|
18
20
|
|
19
21
|
- [Design Overview](https://socketry.github.io/protocol-http/guides/design-overview/index) - This guide explains the high level design of `protocol-http` in the context of wider design patterns that can be used to implement HTTP clients and servers.
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: protocol-http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.34.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -47,7 +47,7 @@ cert_chain:
|
|
47
47
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
48
48
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
49
49
|
-----END CERTIFICATE-----
|
50
|
-
date: 2024-09-
|
50
|
+
date: 2024-09-10 00:00:00.000000000 Z
|
51
51
|
dependencies: []
|
52
52
|
description:
|
53
53
|
email:
|
metadata.gz.sig
CHANGED
Binary file
|