Skip to content

Commit

Permalink
Improved documentatio for Protocol::HTTP::Body.
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix committed Nov 28, 2024
1 parent 437268a commit dd3b233
Show file tree
Hide file tree
Showing 16 changed files with 390 additions and 42 deletions.
16 changes: 16 additions & 0 deletions lib/protocol/http/body.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2024, by Samuel Williams.

require_relative "body/readable"
require_relative "body/writable"
require_relative "body/wrapper"

module Protocol
module HTTP
# @namespace
module Body
end
end
end
31 changes: 29 additions & 2 deletions lib/protocol/http/body/buffered.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,18 @@ def self.read(body)
self.new(chunks)
end

# Initialize the buffered body with some chunks.
#
# @parameter chunks [Array(String)] the chunks to buffer.
# @parameter length [Integer] the length of the body, if known.
def initialize(chunks = [], length = nil)
@chunks = chunks
@length = length

@index = 0
end

# @attribute [Array(String)] chunks the buffered chunks.
attr :chunks

# 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.
Expand All @@ -59,36 +64,48 @@ def buffered
self.class.new(@chunks)
end

# Finish the body, this is a no-op.
#
# @returns [Buffered] self.
def finish
self
end

# Ensure that future reads return nil, but allow for rewinding.
# Ensure that future reads return `nil`, but allow for rewinding.
#
# @parameter error [Exception | Nil] the error that caused the body to be closed, if any.
def close(error = nil)
@index = @chunks.length

return nil
end

# Clear the buffered chunks.
def clear
@chunks = []
@length = 0
@index = 0
end

# The length of the body. Will compute and cache the length of the body, if it was not provided.
def length
@length ||= @chunks.inject(0) {|sum, chunk| sum + chunk.bytesize}
end

# @returns [Boolean] if the body is empty.
def empty?
@index >= @chunks.length
end

# A buffered response is always ready.
# Whether the body is ready to be read.
# @returns [Boolean] a buffered response is always ready.
def ready?
true
end

# Read the next chunk from the buffered body.
#
# @returns [String | Nil] the next chunk or nil if there are no more chunks.
def read
return nil unless @chunks

Expand All @@ -99,23 +116,30 @@ def read
end
end

# Discard the body. Invokes {#close}.
def discard
# It's safe to call close here because there is no underlying stream to close:
self.close
end

# Write a chunk to the buffered body.
def write(chunk)
@chunks << chunk
end

# Close the body for writing. This is a no-op.
def close_write(error)
# Nothing to do.
end

# Whether the body can be rewound.
#
# @returns [Boolean] if the body has chunks.
def rewindable?
@chunks != nil
end

# Rewind the body to the beginning, causing a subsequent read to return the first chunk.
def rewind
return false unless @chunks

Expand All @@ -124,6 +148,9 @@ def rewind
return true
end

# Inspect the buffered body.
#
# @returns [String] a string representation of the buffered body.
def inspect
if @chunks
"\#<#{self.class} #{@chunks.size} chunks, #{self.length} bytes>"
Expand Down
13 changes: 13 additions & 0 deletions lib/protocol/http/body/completable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ module HTTP
module Body
# Invokes a callback once the body has completed, either successfully or due to an error.
class Completable < Wrapper
# Wrap a message body with a callback. If the body is empty, the callback is invoked immediately.
#
# @parameter message [Request | Response] the message body.
# @parameter block [Proc] the callback to invoke when the body is closed.
def self.wrap(message, &block)
if body = message&.body and !body.empty?
message.body = self.new(message.body, block)
Expand All @@ -18,20 +22,29 @@ def self.wrap(message, &block)
end
end

# Initialize the completable body with a callback.
#
# @parameter body [Readable] the body to wrap.
# @parameter callback [Proc] the callback to invoke when the body is closed.
def initialize(body, callback)
super(body)

@callback = callback
end

# @returns [Boolean] completable bodies are not rewindable.
def rewindable?
false
end

# Rewind the body, is not supported.
def rewind
false
end

# Close the body and invoke the callback. If an error is given, it is passed to the callback.
#
# The calback is only invoked once, and before `super` is invoked.
def close(error = nil)
if @callback
@callback.call(error)
Expand Down
33 changes: 33 additions & 0 deletions lib/protocol/http/body/deflate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,27 @@
module Protocol
module HTTP
module Body
# A body which compresses or decompresses the contents using the DEFLATE or GZIP algorithm.
class ZStream < Wrapper
# The default compression level.
DEFAULT_LEVEL = 7

# The DEFLATE window size.
DEFLATE = -Zlib::MAX_WBITS

# The GZIP window size.
GZIP = Zlib::MAX_WBITS | 16

# The supported encodings.
ENCODINGS = {
"deflate" => DEFLATE,
"gzip" => GZIP,
}

# Initialize the body with the given stream.
#
# @parameter body [Readable] the body to wrap.
# @parameter stream [Zlib::Deflate | Zlib::Inflate] the stream to use for compression or decompression.
def initialize(body, stream)
super(body)

Expand All @@ -30,6 +40,9 @@ def initialize(body, stream)
@output_length = 0
end

# Close the stream.
#
# @parameter error [Exception | Nil] the error that caused the stream to be closed.
def close(error = nil)
if stream = @stream
@stream = nil
Expand All @@ -39,14 +52,21 @@ def close(error = nil)
super
end

# The length of the output, if known. Generally, this is not known due to the nature of compression.
def length
# We don't know the length of the output until after it's been compressed.
nil
end

# @attribute [Integer] input_length the total number of bytes read from the input.
attr :input_length

# @attribute [Integer] output_length the total number of bytes written to the output.
attr :output_length

# The compression ratio, according to the input and output lengths.
#
# @returns [Float] the compression ratio, e.g. 0.5 for 50% compression.
def ratio
if @input_length != 0
@output_length.to_f / @input_length.to_f
Expand All @@ -55,16 +75,29 @@ def ratio
end
end

# Inspect the body, including the compression ratio.
#
# @returns [String] a string representation of the body.
def inspect
"#{super} | \#<#{self.class} #{(ratio*100).round(2)}%>"
end
end

# A body which compresses the contents using the DEFLATE or GZIP algorithm.
class Deflate < ZStream
# Create a new body which compresses the given body using the GZIP algorithm by default.
#
# @parameter body [Readable] the body to wrap.
# @parameter window_size [Integer] the window size to use for compression.
# @parameter level [Integer] the compression level to use.
# @returns [Deflate] the wrapped body.
def self.for(body, window_size = GZIP, level = DEFAULT_LEVEL)
self.new(body, Zlib::Deflate.new(level, window_size))
end

# Read a chunk from the underlying body and compress it. If the body is finished, the stream is flushed and finished, and the remaining data is returned.
#
# @returns [String | Nil] the compressed chunk or `nil` if the stream is closed.
def read
return if @stream.finished?

Expand Down
23 changes: 19 additions & 4 deletions lib/protocol/http/body/digestable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,36 @@ module HTTP
module Body
# Invokes a callback once the body has finished reading.
class Digestable < Wrapper
# Wrap a message body with a callback. If the body is empty, the callback is not invoked, as there is no data to digest.
#
# @parameter message [Request | Response] the message body.
# @parameter digest [Digest] the digest to use.
# @parameter block [Proc] the callback to invoke when the body is closed.
def self.wrap(message, digest = Digest::SHA256.new, &block)
if body = message&.body and !body.empty?
message.body = self.new(message.body, digest, block)
end
end

# Initialize the digestable body with a callback.
#
# @parameter body [Readable] the body to wrap.
# @parameter digest [Digest] the digest to use.
# @parameter callback [Block] The callback is invoked when the digest is complete.
def initialize(body, digest = Digest::SHA256.new, callback = nil)
super(body)

@digest = digest
@callback = callback
end

# @attribute [Digest] digest the digest object.
attr :digest

def digest
@digest
end

# Generate an appropriate ETag for the digest, assuming it is complete. If you call this method before the body is fully read, the ETag will be incorrect.
#
# @parameter weak [Boolean] If true, the ETag is marked as weak.
# @returns [String] the ETag.
def etag(weak: false)
if weak
"W/\"#{digest.hexdigest}\""
Expand All @@ -38,6 +50,9 @@ def etag(weak: false)
end
end

# Read the body and update the digest. When the body is fully read, the callback is invoked with `self` as the argument.
#
# @returns [String | Nil] the next chunk of data, or nil if the body is fully read.
def read
if chunk = super
@digest.update(chunk)
Expand Down
Loading

0 comments on commit dd3b233

Please sign in to comment.