protocol-http 0.45.0 → 0.46.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/accept_encoding.rb +15 -3
- data/lib/protocol/http/body/buffered.rb +29 -2
- data/lib/protocol/http/body/completable.rb +13 -0
- data/lib/protocol/http/body/deflate.rb +33 -0
- data/lib/protocol/http/body/digestable.rb +19 -4
- data/lib/protocol/http/body/file.rb +37 -1
- data/lib/protocol/http/body/head.rb +8 -0
- data/lib/protocol/http/body/inflate.rb +10 -2
- data/lib/protocol/http/body/readable.rb +32 -11
- data/lib/protocol/http/body/reader.rb +17 -0
- data/lib/protocol/http/body/rewindable.rb +19 -1
- data/lib/protocol/http/body/stream.rb +34 -6
- data/lib/protocol/http/body/streamable.rb +46 -5
- data/lib/protocol/http/body/wrapper.rb +25 -3
- data/lib/protocol/http/body/writable.rb +48 -7
- data/lib/protocol/http/body.rb +16 -0
- data/lib/protocol/http/content_encoding.rb +13 -3
- data/lib/protocol/http/cookie.rb +23 -0
- data/lib/protocol/http/header/authorization.rb +10 -2
- data/lib/protocol/http/header/cache_control.rb +42 -10
- data/lib/protocol/http/header/connection.rb +18 -1
- data/lib/protocol/http/header/cookie.rb +10 -3
- data/lib/protocol/http/header/date.rb +9 -0
- data/lib/protocol/http/header/etag.rb +11 -0
- data/lib/protocol/http/header/etags.rb +35 -4
- data/lib/protocol/http/header/multiple.rb +9 -1
- data/lib/protocol/http/header/priority.rb +65 -0
- data/lib/protocol/http/header/split.rb +17 -3
- data/lib/protocol/http/header/vary.rb +10 -1
- data/lib/protocol/http/headers.rb +82 -19
- data/lib/protocol/http/methods.rb +5 -0
- data/lib/protocol/http/middleware/builder.rb +17 -0
- data/lib/protocol/http/middleware.rb +28 -0
- data/lib/protocol/http/peer.rb +9 -0
- data/lib/protocol/http/reference.rb +33 -6
- data/lib/protocol/http/request.rb +25 -0
- data/lib/protocol/http/response.rb +12 -0
- data/lib/protocol/http/url.rb +34 -8
- data/lib/protocol/http/version.rb +1 -1
- data/readme.md +4 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +4 -2
- metadata.gz.sig +0 -0
@@ -8,8 +8,11 @@ module Protocol
|
|
8
8
|
module HTTP
|
9
9
|
module Body
|
10
10
|
# General operations for interacting with a request or response body.
|
11
|
+
#
|
12
|
+
# This module is included in both {Request} and {Response}.
|
11
13
|
module Reader
|
12
14
|
# Read chunks from the body.
|
15
|
+
#
|
13
16
|
# @yields {|chunk| ...} chunks from the body.
|
14
17
|
def each(&block)
|
15
18
|
if @body
|
@@ -19,6 +22,7 @@ module Protocol
|
|
19
22
|
end
|
20
23
|
|
21
24
|
# Reads the entire request/response body.
|
25
|
+
#
|
22
26
|
# @returns [String] the entire body as a string.
|
23
27
|
def read
|
24
28
|
if @body
|
@@ -30,6 +34,7 @@ module Protocol
|
|
30
34
|
end
|
31
35
|
|
32
36
|
# Gracefully finish reading the body. This will buffer the remainder of the body.
|
37
|
+
#
|
33
38
|
# @returns [Buffered] buffers the entire body.
|
34
39
|
def finish
|
35
40
|
if @body
|
@@ -46,19 +51,27 @@ module Protocol
|
|
46
51
|
@body = nil
|
47
52
|
body.discard
|
48
53
|
end
|
54
|
+
|
55
|
+
return nil
|
49
56
|
end
|
50
57
|
|
51
58
|
# Buffer the entire request/response body.
|
59
|
+
#
|
52
60
|
# @returns [Reader] itself.
|
53
61
|
def buffered!
|
54
62
|
if @body
|
55
63
|
@body = @body.finish
|
56
64
|
end
|
57
65
|
|
66
|
+
# TODO Should this return @body instead? It seems more useful.
|
58
67
|
return self
|
59
68
|
end
|
60
69
|
|
61
70
|
# Write the body of the response to the given file path.
|
71
|
+
#
|
72
|
+
# @parameter path [String] the path to write the body to.
|
73
|
+
# @parameter mode [Integer] the mode to open the file with.
|
74
|
+
# @parameter options [Hash] additional options to pass to `File.open`.
|
62
75
|
def save(path, mode = ::File::WRONLY|::File::CREAT|::File::TRUNC, **options)
|
63
76
|
if @body
|
64
77
|
::File.open(path, mode, **options) do |file|
|
@@ -70,6 +83,8 @@ module Protocol
|
|
70
83
|
end
|
71
84
|
|
72
85
|
# Close the connection as quickly as possible. Discards body. May close the underlying connection if necessary to terminate the stream.
|
86
|
+
#
|
87
|
+
# @parameter error [Exception | Nil] the error that caused the stream to be closed, if any.
|
73
88
|
def close(error = nil)
|
74
89
|
if @body
|
75
90
|
@body.close(error)
|
@@ -78,6 +93,8 @@ module Protocol
|
|
78
93
|
end
|
79
94
|
|
80
95
|
# Whether there is a body?
|
96
|
+
#
|
97
|
+
# @returns [Boolean] whether there is a body.
|
81
98
|
def body?
|
82
99
|
@body and !@body.empty?
|
83
100
|
end
|
@@ -9,8 +9,13 @@ require_relative "buffered"
|
|
9
9
|
module Protocol
|
10
10
|
module HTTP
|
11
11
|
module Body
|
12
|
-
# A body which buffers all it's contents as it is
|
12
|
+
# A body which buffers all it's contents as it is read.
|
13
|
+
#
|
14
|
+
# As the body is buffered in memory, you may want to ensure your server has sufficient (virtual) memory available to buffer the entire body.
|
13
15
|
class Rewindable < Wrapper
|
16
|
+
# Wrap the given message body in a rewindable body, if it is not already rewindable.
|
17
|
+
#
|
18
|
+
# @parameter message [Request | Response] the message to wrap.
|
14
19
|
def self.wrap(message)
|
15
20
|
if body = message.body
|
16
21
|
if body.rewindable?
|
@@ -21,6 +26,9 @@ module Protocol
|
|
21
26
|
end
|
22
27
|
end
|
23
28
|
|
29
|
+
# Initialize the body with the given body.
|
30
|
+
#
|
31
|
+
# @parameter body [Readable] the body to wrap.
|
24
32
|
def initialize(body)
|
25
33
|
super(body)
|
26
34
|
|
@@ -28,10 +36,12 @@ module Protocol
|
|
28
36
|
@index = 0
|
29
37
|
end
|
30
38
|
|
39
|
+
# @returns [Boolean] Whether the body is empty.
|
31
40
|
def empty?
|
32
41
|
(@index >= @chunks.size) && super
|
33
42
|
end
|
34
43
|
|
44
|
+
# @returns [Boolean] Whether the body is ready to be read.
|
35
45
|
def ready?
|
36
46
|
(@index < @chunks.size) || super
|
37
47
|
end
|
@@ -43,6 +53,9 @@ module Protocol
|
|
43
53
|
Buffered.new(@chunks)
|
44
54
|
end
|
45
55
|
|
56
|
+
# Read the next available chunk. This may return a buffered chunk if the stream has been rewound, or a chunk from the underlying stream, if available.
|
57
|
+
#
|
58
|
+
# @returns [String | Nil] The chunk of data, or `nil` if the stream has finished.
|
46
59
|
def read
|
47
60
|
if @index < @chunks.size
|
48
61
|
chunk = @chunks[@index]
|
@@ -58,14 +71,19 @@ module Protocol
|
|
58
71
|
return chunk
|
59
72
|
end
|
60
73
|
|
74
|
+
# Rewind the stream to the beginning.
|
61
75
|
def rewind
|
62
76
|
@index = 0
|
63
77
|
end
|
64
78
|
|
79
|
+
# @returns [Boolean] Whether the stream is rewindable, which it is.
|
65
80
|
def rewindable?
|
66
81
|
true
|
67
82
|
end
|
68
83
|
|
84
|
+
# Inspect the rewindable body.
|
85
|
+
#
|
86
|
+
# @returns [String] a string representation of the body.
|
69
87
|
def inspect
|
70
88
|
"\#<#{self.class} #{@index}/#{@chunks.size} chunks read>"
|
71
89
|
end
|
@@ -11,8 +11,13 @@ module Protocol
|
|
11
11
|
module Body
|
12
12
|
# The input stream is an IO-like object which contains the raw HTTP POST data. When applicable, its external encoding must be “ASCII-8BIT” and it must be opened in binary mode, for Ruby 1.9 compatibility. The input stream must respond to gets, each, read and rewind.
|
13
13
|
class Stream
|
14
|
+
# The default line separator, used by {gets}.
|
14
15
|
NEWLINE = "\n"
|
15
16
|
|
17
|
+
# Initialize the stream with the given input and output.
|
18
|
+
#
|
19
|
+
# @parameter input [Readable] The input stream.
|
20
|
+
# @parameter output [Writable] The output stream.
|
16
21
|
def initialize(input = nil, output = Buffered.new)
|
17
22
|
@input = input
|
18
23
|
@output = output
|
@@ -26,7 +31,10 @@ module Protocol
|
|
26
31
|
@closed_read = false
|
27
32
|
end
|
28
33
|
|
34
|
+
# @attribute [Readable] The input stream.
|
29
35
|
attr :input
|
36
|
+
|
37
|
+
# @attribute [Writable] The output stream.
|
30
38
|
attr :output
|
31
39
|
|
32
40
|
# This provides a read-only interface for data, which is surprisingly tricky to implement correctly.
|
@@ -39,9 +47,9 @@ module Protocol
|
|
39
47
|
#
|
40
48
|
# If buffer is given, then the read data will be placed into buffer instead of a newly created String object.
|
41
49
|
#
|
42
|
-
# @
|
43
|
-
# @
|
44
|
-
# @
|
50
|
+
# @parameterlength [Integer] the amount of data to read
|
51
|
+
# @parameter buffer [String] the buffer which will receive the data
|
52
|
+
# @returns [String] a buffer containing the data
|
45
53
|
def read(length = nil, buffer = nil)
|
46
54
|
return "" if length == 0
|
47
55
|
|
@@ -125,11 +133,14 @@ module Protocol
|
|
125
133
|
end
|
126
134
|
|
127
135
|
# Similar to {read_partial} but raises an `EOFError` if the stream is at EOF.
|
136
|
+
#
|
137
|
+
# @parameter length [Integer] The maximum number of bytes to read.
|
138
|
+
# @parameter buffer [String] The buffer to read into.
|
128
139
|
def readpartial(length, buffer = nil)
|
129
140
|
read_partial(length, buffer) or raise EOFError, "End of file reached!"
|
130
141
|
end
|
131
142
|
|
132
|
-
# Iterate over each chunk of data
|
143
|
+
# Iterate over each chunk of data from the input stream.
|
133
144
|
#
|
134
145
|
# @yields {|chunk| ...} Each chunk of data.
|
135
146
|
def each(&block)
|
@@ -146,6 +157,9 @@ module Protocol
|
|
146
157
|
end
|
147
158
|
|
148
159
|
# Read data from the stream without blocking if possible.
|
160
|
+
#
|
161
|
+
# @parameter length [Integer] The maximum number of bytes to read.
|
162
|
+
# @parameter buffer [String | Nil] The buffer to read into.
|
149
163
|
def read_nonblock(length, buffer = nil, exception: nil)
|
150
164
|
@buffer ||= read_next
|
151
165
|
chunk = nil
|
@@ -323,6 +337,10 @@ module Protocol
|
|
323
337
|
end
|
324
338
|
|
325
339
|
# Close the input body.
|
340
|
+
#
|
341
|
+
# If, while processing the data that was read from this stream, an error is encountered, it should be passed to this method.
|
342
|
+
#
|
343
|
+
# @parameter error [Exception | Nil] The error that was encountered, if any.
|
326
344
|
def close_read(error = nil)
|
327
345
|
if input = @input
|
328
346
|
@input = nil
|
@@ -334,6 +352,10 @@ module Protocol
|
|
334
352
|
end
|
335
353
|
|
336
354
|
# Close the output body.
|
355
|
+
#
|
356
|
+
# If, while generating the data that is written to this stream, an error is encountered, it should be passed to this method.
|
357
|
+
#
|
358
|
+
# @parameter error [Exception | Nil] The error that was encountered, if any.
|
337
359
|
def close_write(error = nil)
|
338
360
|
if output = @output
|
339
361
|
@output = nil
|
@@ -343,6 +365,8 @@ module Protocol
|
|
343
365
|
end
|
344
366
|
|
345
367
|
# Close the input and output bodies.
|
368
|
+
#
|
369
|
+
# @parameter error [Exception | Nil] The error that caused this stream to be closed, if any.
|
346
370
|
def close(error = nil)
|
347
371
|
self.close_read(error)
|
348
372
|
self.close_write(error)
|
@@ -352,18 +376,22 @@ module Protocol
|
|
352
376
|
@closed = true
|
353
377
|
end
|
354
378
|
|
355
|
-
# Whether the stream has been closed.
|
379
|
+
# @returns [Boolean] Whether the stream has been closed.
|
356
380
|
def closed?
|
357
381
|
@closed
|
358
382
|
end
|
359
383
|
|
360
|
-
# Whether there are any output chunks remaining
|
384
|
+
# @returns [Boolean] Whether there are any output chunks remaining.
|
361
385
|
def empty?
|
362
386
|
@output.empty?
|
363
387
|
end
|
364
388
|
|
365
389
|
private
|
366
390
|
|
391
|
+
# Read the next chunk of data from the input stream.
|
392
|
+
#
|
393
|
+
# @returns [String] The next chunk of data.
|
394
|
+
# @raises [IOError] If the input stream was explicitly closed.
|
367
395
|
def read_next
|
368
396
|
if @input
|
369
397
|
return @input.read
|
@@ -13,39 +13,65 @@ module Protocol
|
|
13
13
|
module Body
|
14
14
|
# A body that invokes a block that can read and write to a stream.
|
15
15
|
#
|
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
|
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}.
|
17
17
|
#
|
18
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.
|
19
19
|
module Streamable
|
20
|
+
# Generate a new streaming request body using the given block to generate the body.
|
21
|
+
#
|
22
|
+
# @parameter block [Proc] The block that generates the body.
|
23
|
+
# @returns [RequestBody] The streaming request body.
|
20
24
|
def self.request(&block)
|
21
25
|
RequestBody.new(block)
|
22
26
|
end
|
23
27
|
|
28
|
+
# Generate a new streaming response body using the given block to generate the body.
|
29
|
+
#
|
30
|
+
# @parameter request [Request] The request.
|
31
|
+
# @parameter block [Proc] The block that generates the body.
|
32
|
+
# @returns [ResponseBody] The streaming response body.
|
24
33
|
def self.response(request, &block)
|
25
34
|
ResponseBody.new(block, request.body)
|
26
35
|
end
|
27
36
|
|
37
|
+
# A output stream that can be written to by a block.
|
28
38
|
class Output
|
39
|
+
# Schedule the block to be executed in a fiber.
|
40
|
+
#
|
41
|
+
# @parameter input [Readable] The input stream.
|
42
|
+
# @parameter block [Proc] The block that generates the output.
|
43
|
+
# @returns [Output] The output stream.
|
29
44
|
def self.schedule(input, block)
|
30
45
|
self.new(input, block).tap(&:schedule)
|
31
46
|
end
|
32
47
|
|
48
|
+
# Initialize the output stream with the given input and block.
|
49
|
+
#
|
50
|
+
# @parameter input [Readable] The input stream.
|
51
|
+
# @parameter block [Proc] The block that generates the output.
|
33
52
|
def initialize(input, block)
|
34
53
|
@output = Writable.new
|
35
54
|
@stream = Stream.new(input, @output)
|
36
55
|
@block = block
|
37
56
|
end
|
38
57
|
|
58
|
+
# Schedule the block to be executed in a fiber.
|
59
|
+
#
|
60
|
+
# @returns [Fiber] The fiber.
|
39
61
|
def schedule
|
40
62
|
@fiber ||= Fiber.schedule do
|
41
63
|
@block.call(@stream)
|
42
64
|
end
|
43
65
|
end
|
44
66
|
|
67
|
+
# Read from the output stream (may block).
|
45
68
|
def read
|
46
69
|
@output.read
|
47
70
|
end
|
48
71
|
|
72
|
+
# Close the output stream.
|
73
|
+
#
|
74
|
+
# @parameter error [Exception | Nil] The error that caused this stream to be closed, if any.
|
49
75
|
def close(error = nil)
|
50
76
|
@output.close_write(error)
|
51
77
|
end
|
@@ -55,13 +81,19 @@ module Protocol
|
|
55
81
|
class ConsumedError < StandardError
|
56
82
|
end
|
57
83
|
|
84
|
+
# A streaming body that can be read from and written to.
|
58
85
|
class Body < Readable
|
86
|
+
# Initialize the body with the given block and input.
|
87
|
+
#
|
88
|
+
# @parameter block [Proc] The block that generates the body.
|
89
|
+
# @parameter input [Readable] The input stream, if known.
|
59
90
|
def initialize(block, input = nil)
|
60
91
|
@block = block
|
61
92
|
@input = input
|
62
93
|
@output = nil
|
63
94
|
end
|
64
95
|
|
96
|
+
# @returns [Boolean] Whether the body can be streamed, which is true.
|
65
97
|
def stream?
|
66
98
|
true
|
67
99
|
end
|
@@ -81,9 +113,9 @@ module Protocol
|
|
81
113
|
@output.read
|
82
114
|
end
|
83
115
|
|
84
|
-
# Invoke the block with the given stream.
|
116
|
+
# Invoke the block with the given stream. The block can read and write to the stream, and must close the stream when finishing.
|
85
117
|
#
|
86
|
-
# The
|
118
|
+
# @parameter stream [Stream] The stream to read and write to.
|
87
119
|
def call(stream)
|
88
120
|
if @block.nil?
|
89
121
|
raise ConsumedError, "Streaming block has already been consumed!"
|
@@ -101,6 +133,9 @@ module Protocol
|
|
101
133
|
raise
|
102
134
|
end
|
103
135
|
|
136
|
+
# Close the input. The streaming body will eventually read all the input.
|
137
|
+
#
|
138
|
+
# @parameter error [Exception | Nil] The error that caused this stream to be closed, if any.
|
104
139
|
def close_input(error = nil)
|
105
140
|
if input = @input
|
106
141
|
@input = nil
|
@@ -108,6 +143,9 @@ module Protocol
|
|
108
143
|
end
|
109
144
|
end
|
110
145
|
|
146
|
+
# Close the output, the streaming body will be unable to write any more output.
|
147
|
+
#
|
148
|
+
# @parameter error [Exception | Nil] The error that caused this stream to be closed, if any.
|
111
149
|
def close_output(error = nil)
|
112
150
|
@output&.close(error)
|
113
151
|
end
|
@@ -115,8 +153,8 @@ module Protocol
|
|
115
153
|
|
116
154
|
# A response body is used on the server side to generate the response body using a block.
|
117
155
|
class ResponseBody < Body
|
156
|
+
# Close will be invoked when all the output is written.
|
118
157
|
def close(error = nil)
|
119
|
-
# Close will be invoked when all the output is written.
|
120
158
|
self.close_output(error)
|
121
159
|
end
|
122
160
|
end
|
@@ -125,12 +163,15 @@ module Protocol
|
|
125
163
|
#
|
126
164
|
# As the response body isn't available until the request is sent, the response body must be {stream}ed into the request body.
|
127
165
|
class RequestBody < Body
|
166
|
+
# Initialize the request body with the given block.
|
167
|
+
#
|
168
|
+
# @parameter block [Proc] The block that generates the body.
|
128
169
|
def initialize(block)
|
129
170
|
super(block, Writable.new)
|
130
171
|
end
|
131
172
|
|
173
|
+
# Close will be invoked when all the input is read.
|
132
174
|
def close(error = nil)
|
133
|
-
# Close will be invoked when all the input is read.
|
134
175
|
self.close_input(error)
|
135
176
|
end
|
136
177
|
|
@@ -13,20 +13,26 @@ module Protocol
|
|
13
13
|
# Wrap the body of the given message in a new instance of this class.
|
14
14
|
#
|
15
15
|
# @parameter message [Request | Response] the message to wrap.
|
16
|
-
# @returns [Wrapper |
|
16
|
+
# @returns [Wrapper | Nil] the wrapped body or `nil`` if the body was `nil`.
|
17
17
|
def self.wrap(message)
|
18
18
|
if body = message.body
|
19
19
|
message.body = self.new(body)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
# Initialize the wrapper with the given body.
|
24
|
+
#
|
25
|
+
# @parameter body [Readable] The body to wrap.
|
23
26
|
def initialize(body)
|
24
27
|
@body = body
|
25
28
|
end
|
26
29
|
|
27
|
-
# The wrapped body.
|
30
|
+
# @attribute [Readable] The wrapped body.
|
28
31
|
attr :body
|
29
32
|
|
33
|
+
# Close the body.
|
34
|
+
#
|
35
|
+
# @parameter error [Exception | Nil] The error that caused this stream to be closed, if any.
|
30
36
|
def close(error = nil)
|
31
37
|
@body.close(error)
|
32
38
|
|
@@ -34,39 +40,49 @@ module Protocol
|
|
34
40
|
# super
|
35
41
|
end
|
36
42
|
|
43
|
+
# Forwards to the wrapped body.
|
37
44
|
def empty?
|
38
45
|
@body.empty?
|
39
46
|
end
|
40
47
|
|
48
|
+
# Forwards to the wrapped body.
|
41
49
|
def ready?
|
42
50
|
@body.ready?
|
43
51
|
end
|
44
52
|
|
53
|
+
# Forwards to the wrapped body.
|
45
54
|
def buffered
|
46
55
|
@body.buffered
|
47
56
|
end
|
48
57
|
|
58
|
+
# Forwards to the wrapped body.
|
49
59
|
def rewind
|
50
60
|
@body.rewind
|
51
61
|
end
|
52
62
|
|
63
|
+
# Forwards to the wrapped body.
|
53
64
|
def rewindable?
|
54
65
|
@body.rewindable?
|
55
66
|
end
|
56
67
|
|
68
|
+
# Forwards to the wrapped body.
|
57
69
|
def length
|
58
70
|
@body.length
|
59
71
|
end
|
60
72
|
|
61
|
-
#
|
73
|
+
# Forwards to the wrapped body.
|
62
74
|
def read
|
63
75
|
@body.read
|
64
76
|
end
|
65
77
|
|
78
|
+
# Forwards to the wrapped body.
|
66
79
|
def discard
|
67
80
|
@body.discard
|
68
81
|
end
|
69
82
|
|
83
|
+
# Convert the body to a hash suitable for serialization.
|
84
|
+
#
|
85
|
+
# @returns [Hash] The body as a hash.
|
70
86
|
def as_json(...)
|
71
87
|
{
|
72
88
|
class: self.class.name,
|
@@ -74,10 +90,16 @@ module Protocol
|
|
74
90
|
}
|
75
91
|
end
|
76
92
|
|
93
|
+
# Convert the body to JSON.
|
94
|
+
#
|
95
|
+
# @returns [String] The body as JSON.
|
77
96
|
def to_json(...)
|
78
97
|
as_json.to_json(...)
|
79
98
|
end
|
80
99
|
|
100
|
+
# Inspect the wrapped body. The wrapper, by default, is transparent.
|
101
|
+
#
|
102
|
+
# @returns [String] a string representation of the wrapped body.
|
81
103
|
def inspect
|
82
104
|
@body.inspect
|
83
105
|
end
|
@@ -10,11 +10,14 @@ module Protocol
|
|
10
10
|
module Body
|
11
11
|
# A dynamic body which you can write to and read from.
|
12
12
|
class Writable < Readable
|
13
|
+
# An error indicating that the body has been closed and no further writes are allowed.
|
13
14
|
class Closed < StandardError
|
14
15
|
end
|
15
16
|
|
16
|
-
#
|
17
|
-
#
|
17
|
+
# Initialize the writable body.
|
18
|
+
#
|
19
|
+
# @parameter length [Integer] The length of the response body if known.
|
20
|
+
# @parameter queue [Thread::Queue] Specify a different queue implementation, e.g. `Thread::SizedQueue` to enable back-pressure.
|
18
21
|
def initialize(length = nil, queue: Thread::Queue.new)
|
19
22
|
@length = length
|
20
23
|
@queue = queue
|
@@ -22,11 +25,12 @@ module Protocol
|
|
22
25
|
@error = nil
|
23
26
|
end
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
end
|
28
|
+
# @attribute [Integer] The length of the response body if known.
|
29
|
+
attr :length
|
28
30
|
|
29
31
|
# 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.
|
32
|
+
#
|
33
|
+
# @parameter error [Exception] The error that caused this body to be closed, if any. Will be raised on the next call to {read}.
|
30
34
|
def close(error = nil)
|
31
35
|
@error ||= error
|
32
36
|
|
@@ -36,20 +40,29 @@ module Protocol
|
|
36
40
|
super
|
37
41
|
end
|
38
42
|
|
43
|
+
# Whether the body is closed. A closed body can not be written to or read from.
|
44
|
+
#
|
45
|
+
# @returns [Boolean] Whether the body is closed.
|
39
46
|
def closed?
|
40
47
|
@queue.closed?
|
41
48
|
end
|
42
49
|
|
50
|
+
# @returns [Boolean] Whether the body is ready to be read from, without blocking.
|
43
51
|
def ready?
|
44
52
|
!@queue.empty? || @queue.closed?
|
45
53
|
end
|
46
54
|
|
47
|
-
#
|
55
|
+
# Indicates whether the body is empty. This can occur if the body has been closed, or if the producer has invoked {close_write} and the reader has consumed all available chunks.
|
56
|
+
#
|
57
|
+
# @returns [Boolean] Whether the body is empty.
|
48
58
|
def empty?
|
49
59
|
@queue.empty? && @queue.closed?
|
50
60
|
end
|
51
61
|
|
52
62
|
# Read the next available chunk.
|
63
|
+
#
|
64
|
+
# @returns [String | Nil] The next chunk, or `nil` if the body is finished.
|
65
|
+
# @raises [Exception] If the body was closed due to an error.
|
53
66
|
def read
|
54
67
|
if @error
|
55
68
|
raise @error
|
@@ -65,7 +78,11 @@ module Protocol
|
|
65
78
|
return chunk
|
66
79
|
end
|
67
80
|
|
68
|
-
# Write a single chunk to the body. Signal completion by calling
|
81
|
+
# Write a single chunk to the body. Signal completion by calling {close_write}.
|
82
|
+
#
|
83
|
+
# @parameter chunk [String] The chunk to write.
|
84
|
+
# @raises [Closed] If the body has been closed without error.
|
85
|
+
# @raises [Exception] If the body has been closed due to an error.
|
69
86
|
def write(chunk)
|
70
87
|
if @queue.closed?
|
71
88
|
raise(@error || Closed)
|
@@ -75,27 +92,41 @@ module Protocol
|
|
75
92
|
@count += 1
|
76
93
|
end
|
77
94
|
|
95
|
+
# Signal that no more data will be written to the body.
|
96
|
+
#
|
97
|
+
# @parameter error [Exception] The error that caused this body to be closed, if any.
|
78
98
|
def close_write(error = nil)
|
79
99
|
@error ||= error
|
80
100
|
@queue.close
|
81
101
|
end
|
82
102
|
|
103
|
+
# The output interface for writing chunks to the body.
|
83
104
|
class Output
|
105
|
+
# Initialize the output with the given writable body.
|
106
|
+
#
|
107
|
+
# @parameter writable [Writable] The writable body.
|
84
108
|
def initialize(writable)
|
85
109
|
@writable = writable
|
86
110
|
@closed = false
|
87
111
|
end
|
88
112
|
|
113
|
+
# @returns [Boolean] Whether the output is closed for writing only.
|
89
114
|
def closed?
|
90
115
|
@closed || @writable.closed?
|
91
116
|
end
|
92
117
|
|
118
|
+
# Write a chunk to the body.
|
93
119
|
def write(chunk)
|
94
120
|
@writable.write(chunk)
|
95
121
|
end
|
96
122
|
|
97
123
|
alias << write
|
98
124
|
|
125
|
+
# Close the output stream.
|
126
|
+
#
|
127
|
+
# If an error is given, the error will be used to close the body by invoking {close} with the error. Otherwise, only the write side of the body will be closed.
|
128
|
+
#
|
129
|
+
# @parameter error [Exception | Nil] The error that caused this stream to be closed, if any.
|
99
130
|
def close(error = nil)
|
100
131
|
@closed = true
|
101
132
|
|
@@ -108,6 +139,12 @@ module Protocol
|
|
108
139
|
end
|
109
140
|
|
110
141
|
# Create an output wrapper which can be used to write chunks to the body.
|
142
|
+
#
|
143
|
+
# If a block is given, and the block raises an error, the error will used to close the body by invoking {close} with the error.
|
144
|
+
#
|
145
|
+
# @yields {|output| ...} if a block is given.
|
146
|
+
# @parameter output [Output] The output wrapper.
|
147
|
+
# @returns [Output] The output wrapper.
|
111
148
|
def output
|
112
149
|
output = Output.new(self)
|
113
150
|
|
@@ -124,6 +161,9 @@ module Protocol
|
|
124
161
|
end
|
125
162
|
end
|
126
163
|
|
164
|
+
# Inspect the body.
|
165
|
+
#
|
166
|
+
# @returns [String] A string representation of the body.
|
127
167
|
def inspect
|
128
168
|
if @error
|
129
169
|
"\#<#{self.class} #{@count} chunks written, #{status}, error=#{@error}>"
|
@@ -134,6 +174,7 @@ module Protocol
|
|
134
174
|
|
135
175
|
private
|
136
176
|
|
177
|
+
# @returns [String] A string representation of the body's status.
|
137
178
|
def status
|
138
179
|
if @queue.empty?
|
139
180
|
if @queue.closed?
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative "body/readable"
|
7
|
+
require_relative "body/writable"
|
8
|
+
require_relative "body/wrapper"
|
9
|
+
|
10
|
+
module Protocol
|
11
|
+
module HTTP
|
12
|
+
# @namespace
|
13
|
+
module Body
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|