Skip to content

Introduce rewind and rewindable? as a general concept. #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions lib/protocol/http/body/buffered.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,29 @@ module HTTP
module Body
# A body which buffers all it's contents.
class Buffered < Readable
# Wraps an array into a buffered body.
# Tries to wrap an object in a {Buffered} instance.
#
# For compatibility, also accepts anything that behaves like an `Array(String)`.
#
# @parameter body [String | Array(String) | Readable | nil] the body to wrap.
# @returns [Readable | nil] the wrapped body or nil if nil was given.
def self.wrap(body)
if body.is_a?(Readable)
return body
elsif body.is_a?(Array)
return self.new(body)
elsif body.is_a?(String)
return self.new([body])
elsif body
return self.for(body)
def self.wrap(object)
if object.is_a?(Readable)
return object
elsif object.is_a?(Array)
return self.new(object)
elsif object.is_a?(String)
return self.new([object])
elsif object
return self.read(object)
end
end

def self.for(body)
# Read the entire body into a buffered representation.
#
# @parameter body [Readable] the body to read.
# @returns [Buffered] the buffered body.
def self.read(body)
chunks = []

body.each do |chunk|
Expand Down Expand Up @@ -77,8 +81,14 @@ def write(chunk)
@chunks << chunk
end

def rewindable?
true
end

def rewind
@index = 0

return true
end

def inspect
Expand Down
8 changes: 8 additions & 0 deletions lib/protocol/http/body/completable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def initialize(body, callback)
@callback = callback
end

def rewindable?
false
end

def rewind
false
end

def finish
super.tap do
if @callback
Expand Down
10 changes: 9 additions & 1 deletion lib/protocol/http/body/readable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ def ready?
false
end

def rewindable?
false
end

def rewind
false
end

def length
nil
end
Expand Down Expand Up @@ -58,7 +66,7 @@ def call(stream)
# @returns [Buffered] The buffered body.
def finish
# Internally, this invokes `self.each` which then invokes `self.close`.
Buffered.for(self)
Buffered.read(self)
end

# Enumerate all chunks until finished, then invoke `#close`.
Expand Down
18 changes: 17 additions & 1 deletion lib/protocol/http/body/rewindable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ module HTTP
module Body
# A body which buffers all it's contents as it is `#read`.
class Rewindable < Wrapper
def self.wrap(message)
if body = message.body
if body.rewindable?
body
else
message.body = self.new(body)
end
end
end

def initialize(body)
super(body)

Expand All @@ -26,7 +36,9 @@ def ready?
(@index < @chunks.size) || super
end

# A rewindable body wraps some other body. Convert it to a buffered body
# A rewindable body wraps some other body. Convert it to a buffered body. The buffered body will share the same chunks as the rewindable body.
#
# @returns [Buffered] the buffered body.
def buffered
Buffered.new(@chunks)
end
Expand Down Expand Up @@ -54,6 +66,10 @@ def rewind
@index = 0
end

def rewindable?
true
end

def inspect
"\#<#{self.class} #{@index}/#{@chunks.size} chunks read>"
end
Expand Down
12 changes: 12 additions & 0 deletions lib/protocol/http/body/wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ module HTTP
module Body
# Wrapping body instance. Typically you'd override `#read`.
class Wrapper < Readable
# Wrap the body of the given message in a new instance of this class.
#
# @parameter message [Request | Response] the message to wrap.
# @returns [Wrapper | nil] the wrapped body or nil if the body was nil.
def self.wrap(message)
if body = message.body
message.body = self.new(body)
Expand Down Expand Up @@ -42,6 +46,14 @@ def ready?
@body.ready?
end

def rewind
@body.rewind
end

def rewindable?
@body.rewindable?
end

def length
@body.length
end
Expand Down
4 changes: 4 additions & 0 deletions test/protocol/http/body/buffered.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@
end

with "#rewind" do
it "is rewindable" do
expect(body).to be(:rewindable?)
end

it "positions the cursor to the beginning" do
expect(body.read).to be == "Hello"
body.rewind
Expand Down
9 changes: 9 additions & 0 deletions test/protocol/http/body/completable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,13 @@
expect(completable.read).to be_nil
end
end

with "#rewindable?" do
it "is not rewindable" do
# Because completion can only happen once, we can't rewind the body.
expect(body).to be(:rewindable?)
expect(completable).not.to be(:rewindable?)
expect(completable.rewind).to be == false
end
end
end
2 changes: 1 addition & 1 deletion test/protocol/http/body/inflate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
let(:sample) {"The quick brown fox jumps over the lazy dog."}
let(:chunks) {[sample] * 1024}

let(:body) {Protocol::HTTP::Body::Buffered.for(chunks)}
let(:body) {Protocol::HTTP::Body::Buffered.new(chunks)}
let(:deflate_body) {Protocol::HTTP::Body::Deflate.for(body)}
let(:compressed_chunks) {deflate_body.join.each_char.to_a}
let(:compressed_body_chunks) {compressed_chunks}
Expand Down
7 changes: 7 additions & 0 deletions test/protocol/http/body/readable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,11 @@
expect(JSON.dump(body)).to be == body.to_json
end
end

with "#rewindable?" do
it "is not rewindable" do
expect(body).not.to be(:rewindable?)
expect(body.rewind).to be == false
end
end
end
27 changes: 27 additions & 0 deletions test/protocol/http/body/rewindable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Copyright, 2019-2023, by Samuel Williams.

require 'protocol/http/body/rewindable'
require 'protocol/http/request'

describe Protocol::HTTP::Body::Rewindable do
let(:source) {Protocol::HTTP::Body::Buffered.new}
Expand Down Expand Up @@ -47,6 +48,26 @@
end
end

with ".wrap" do
with "a buffered body" do
let(:body) {Protocol::HTTP::Body::Buffered.new}
let(:message) {Protocol::HTTP::Request.new(nil, nil, 'GET', '/', nil, Protocol::HTTP::Headers.new, body)}

it "returns the body" do
expect(subject.wrap(message)).to be == body
end
end

with "a non-rewindable body" do
let(:body) {Protocol::HTTP::Body::Readable.new}
let(:message) {Protocol::HTTP::Request.new(nil, nil, 'GET', '/', nil, Protocol::HTTP::Headers.new, body)}

it "returns a new rewindable body" do
expect(subject.wrap(message)).to be_a(Protocol::HTTP::Body::Rewindable)
end
end
end

with '#buffered' do
it "can generate buffered representation" do
3.times do |i|
Expand Down Expand Up @@ -76,6 +97,12 @@
end
end

with "#rewindable?" do
it "is rewindable" do
expect(body).to be(:rewindable?)
end
end

with '#inspect' do
it "can generate string representation" do
expect(body.inspect).to be == "#<Protocol::HTTP::Body::Rewindable 0/0 chunks read>"
Expand Down
14 changes: 14 additions & 0 deletions test/protocol/http/body/wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@
end
end

with "#rewindable?" do
it "should proxy rewindable?" do
expect(source).to receive(:rewindable?).and_return(true)
expect(body.rewindable?).to be == true
end
end

with "#rewind" do
it "should proxy rewind" do
expect(source).to receive(:rewind).and_return(true)
expect(body.rewind).to be == true
end
end

with "#as_json" do
it "generates a JSON representation" do
expect(body.as_json).to have_keys(
Expand Down
Loading