Skip to content

Commit 77db065

Browse files
committed
Expand stream interface to support gets/puts.
1 parent fd9ebdf commit 77db065

File tree

3 files changed

+95
-11
lines changed

3 files changed

+95
-11
lines changed

lib/protocol/http/body/stream.rb

+48-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ module HTTP
1111
module Body
1212
# 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.
1313
class Stream
14+
NEWLINE = "\n"
15+
1416
def initialize(input = nil, output = Buffered.new)
1517
@input = input
1618
@output = output
@@ -55,7 +57,7 @@ def read(length = nil, buffer = nil)
5557

5658
# This ensures the subsequent `slice!` works correctly.
5759
buffer.force_encoding(Encoding::BINARY)
58-
60+
5961
# This will be at least one copy:
6062
@buffer = buffer.byteslice(length, buffer.bytesize)
6163

@@ -102,7 +104,7 @@ def readpartial(length)
102104
read_partial(length) or raise EOFError, "End of file reached!"
103105
end
104106

105-
def read_nonblock(length, buffer = nil)
107+
def read_nonblock(length, buffer = nil, exception: nil)
106108
@buffer ||= read_next
107109
chunk = nil
108110

@@ -127,6 +129,40 @@ def read_nonblock(length, buffer = nil)
127129

128130
return buffer
129131
end
132+
133+
# Efficiently read data from the stream until encountering pattern.
134+
# @parameter pattern [String] The pattern to match.
135+
# @returns [String] The contents of the stream up until the pattern, which is consumed but not returned.
136+
def read_until(pattern, offset = 0, chomp: false)
137+
# We don't want to split on the pattern, so we subtract the size of the pattern.
138+
split_offset = pattern.bytesize - 1
139+
140+
@buffer ||= read_next
141+
return nil if @buffer.nil?
142+
143+
until index = @buffer.index(pattern, offset)
144+
offset = @buffer.bytesize - split_offset
145+
146+
offset = 0 if offset < 0
147+
148+
if chunk = read_next
149+
@buffer << chunk
150+
else
151+
return nil
152+
end
153+
end
154+
155+
@buffer.freeze
156+
matched = @buffer.byteslice(0, index+(chomp ? 0 : pattern.bytesize))
157+
@buffer = @buffer.byteslice(index+pattern.bytesize, @buffer.bytesize)
158+
159+
return matched
160+
end
161+
162+
# Read a single line from the stream.
163+
def gets(separator = NEWLINE, **options)
164+
read_until(separator, **options)
165+
end
130166
end
131167

132168
include Reader
@@ -148,6 +184,16 @@ def <<(buffer)
148184
write(buffer)
149185
end
150186

187+
def puts(*arguments, separator: NEWLINE)
188+
buffer = ::String.new
189+
190+
arguments.each do |argument|
191+
buffer << argument << separator
192+
end
193+
194+
write(buffer)
195+
end
196+
151197
def flush
152198
end
153199

test/protocol/http/body/reader.rb

+15-9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
require 'protocol/http/body/reader'
88
require 'protocol/http/body/buffered'
99

10+
require 'tempfile'
11+
1012
class TestReader
1113
include Protocol::HTTP::Body::Reader
1214

@@ -35,22 +37,26 @@ def initialize(body)
3537
end
3638

3739
with '#save' do
38-
let(:path) { File.expand_path('reader_spec.txt', __dir__) }
39-
4040
it 'saves to the provided filename' do
41-
reader.save(path)
42-
expect(File.read(path)).to be == 'thequickbrownfox'
41+
Tempfile.create do |file|
42+
reader.save(file.path)
43+
expect(File.read(file.path)).to be == 'thequickbrownfox'
44+
end
4345
end
4446

4547
it 'saves by truncating an existing file if it exists' do
46-
File.write(path, 'hello' * 100)
47-
reader.save(path)
48-
expect(File.read(path)).to be == 'thequickbrownfox'
48+
Tempfile.create do |file|
49+
File.write(file.path, 'hello' * 100)
50+
reader.save(file.path)
51+
expect(File.read(file.path)).to be == 'thequickbrownfox'
52+
end
4953
end
5054

5155
it 'mirrors the interface of File.open' do
52-
reader.save(path, 'w')
53-
expect(File.read(path)).to be == 'thequickbrownfox'
56+
Tempfile.create do |file|
57+
reader.save(file.path, 'w')
58+
expect(File.read(file.path)).to be == 'thequickbrownfox'
59+
end
5460
end
5561
end
5662
end

test/protocol/http/body/stream.rb

+32
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,29 @@
131131
end
132132
end
133133

134+
with '#read_until' do
135+
it "can read until a pattern is encountered" do
136+
expect(stream.read_until("o")).to be == "Hello"
137+
expect(stream.read_until("d")).to be == "World"
138+
end
139+
end
140+
141+
with '#gets' do
142+
let(:input) {Protocol::HTTP::Body::Buffered.new(["Hello\nWorld\n"])}
143+
144+
it "can read lines" do
145+
expect(stream.gets).to be == "Hello\n"
146+
expect(stream.gets).to be == "World\n"
147+
expect(stream.gets).to be == nil
148+
end
149+
150+
it "can read lines and chomp separators" do
151+
expect(stream.gets(chomp: true)).to be == "Hello"
152+
expect(stream.gets(chomp: true)).to be == "World"
153+
expect(stream.gets(chomp: true)).to be == nil
154+
end
155+
end
156+
134157
with '#close_read' do
135158
it "should close the input" do
136159
stream.read(1)
@@ -166,6 +189,15 @@
166189
end
167190
end
168191

192+
with '#puts' do
193+
it "should write lines to the output" do
194+
stream.puts("Hello", "World")
195+
stream.puts("Goodbye")
196+
197+
expect(output.chunks).to be == ["Hello\nWorld\n", "Goodbye\n"]
198+
end
199+
end
200+
169201
with '#close_write' do
170202
it "should close the input" do
171203
stream.close_write

0 commit comments

Comments
 (0)