diff --git a/examples/streaming/bidirectional.rb b/examples/streaming/bidirectional.rb index 7433279..100a8a1 100755 --- a/examples/streaming/bidirectional.rb +++ b/examples/streaming/bidirectional.rb @@ -1,6 +1,9 @@ #!/usr/bin/env ruby # frozen_string_literal: true +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + require 'async' require 'async/http/client' require 'async/http/server' diff --git a/examples/streaming/gems.rb b/examples/streaming/gems.rb index 7cba03c..8ff743d 100644 --- a/examples/streaming/gems.rb +++ b/examples/streaming/gems.rb @@ -1,4 +1,8 @@ # frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + source "https://rubygems.org" gem "async" diff --git a/examples/streaming/unidirectional.rb b/examples/streaming/unidirectional.rb index 016bd13..73991cb 100755 --- a/examples/streaming/unidirectional.rb +++ b/examples/streaming/unidirectional.rb @@ -1,6 +1,9 @@ #!/usr/bin/env ruby # frozen_string_literal: true +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + require 'async' require 'async/http/client' require 'async/http/server' diff --git a/fixtures/protocol/http/body/a_readable_body.rb b/fixtures/protocol/http/body/a_readable_body.rb index 1f45d39..1d7218b 100644 --- a/fixtures/protocol/http/body/a_readable_body.rb +++ b/fixtures/protocol/http/body/a_readable_body.rb @@ -1,13 +1,27 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + module Protocol module HTTP module Body AReadableBody = Sus::Shared("a readable body") do - with "#close" do - it "should close the body" do + with "#read" do + it "after closing, returns nil" do body.close + expect(body.read).to be_nil end end + + with "empty?" do + it "returns true after closing" do + body.close + + expect(body).to be(:empty?) + end + end end end end diff --git a/fixtures/protocol/http/body/a_writable_body.rb b/fixtures/protocol/http/body/a_writable_body.rb new file mode 100644 index 0000000..f56ab71 --- /dev/null +++ b/fixtures/protocol/http/body/a_writable_body.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2024, by Samuel Williams. + +module Protocol + module HTTP + module Body + AWritableBody = Sus::Shared("a readable body") do + with "#read" do + it "after closing the write end, returns all chunks" do + body.write("Hello ") + body.write("World!") + body.close_write + + expect(body.read).to be == "Hello " + expect(body.read).to be == "World!" + expect(body.read).to be_nil + end + end + + with "empty?" do + it "returns false before writing" do + expect(body).not.to be(:empty?) + end + + it "returns true after all chunks are consumed" do + body.write("Hello") + body.close_write + + expect(body).not.to be(:empty?) + expect(body.read).to be == "Hello" + expect(body.read).to be_nil + + expect(body).to be(:empty?) + end + end + end + end + end +end diff --git a/lib/protocol/http/body/buffered.rb b/lib/protocol/http/body/buffered.rb index be7d770..374eb5a 100644 --- a/lib/protocol/http/body/buffered.rb +++ b/lib/protocol/http/body/buffered.rb @@ -52,8 +52,18 @@ def initialize(chunks = [], length = nil) attr :chunks + def finish + self + end + def close(error = nil) - @chunks = [] + @index = @chunks.length + end + + def clear + @chunks.clear + @length = 0 + @index = 0 end def length @@ -70,6 +80,8 @@ def ready? end def read + return nil unless @chunks + if chunk = @chunks[@index] @index += 1 @@ -81,18 +93,28 @@ def write(chunk) @chunks << chunk end + def close_write(error) + # Nothing to do. + end + def rewindable? - true + @chunks != nil end def rewind + return false unless @chunks + @index = 0 return true end def inspect - "\#<#{self.class} #{@chunks.size} chunks, #{self.length} bytes>" + if @chunks + "\#<#{self.class} #{@chunks.size} chunks, #{self.length} bytes>" + else + "\#<#{self.class} closed>" + end end end end diff --git a/lib/protocol/http/body/deflate.rb b/lib/protocol/http/body/deflate.rb index 9c68472..5e98316 100644 --- a/lib/protocol/http/body/deflate.rb +++ b/lib/protocol/http/body/deflate.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require_relative 'wrapper' diff --git a/lib/protocol/http/body/digestable.rb b/lib/protocol/http/body/digestable.rb index f4ff966..ea0c583 100644 --- a/lib/protocol/http/body/digestable.rb +++ b/lib/protocol/http/body/digestable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2020-2023, by Samuel Williams. +# Copyright, 2020-2024, by Samuel Williams. require_relative 'wrapper' diff --git a/lib/protocol/http/body/file.rb b/lib/protocol/http/body/file.rb index 4854f28..bb2b5b6 100644 --- a/lib/protocol/http/body/file.rb +++ b/lib/protocol/http/body/file.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require_relative 'readable' diff --git a/lib/protocol/http/body/inflate.rb b/lib/protocol/http/body/inflate.rb index 12d940d..2693f0e 100644 --- a/lib/protocol/http/body/inflate.rb +++ b/lib/protocol/http/body/inflate.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require 'zlib' diff --git a/lib/protocol/http/body/readable.rb b/lib/protocol/http/body/readable.rb index 778cb94..68e075c 100644 --- a/lib/protocol/http/body/readable.rb +++ b/lib/protocol/http/body/readable.rb @@ -72,13 +72,15 @@ def read def each return to_enum unless block_given? - while chunk = self.read - yield chunk + begin + while chunk = self.read + yield chunk + end + rescue => error + raise + ensure + self.close(error) end - rescue => error - raise - ensure - self.close(error) end # Read all remaining chunks into a single binary string using `#each`. diff --git a/lib/protocol/http/body/stream.rb b/lib/protocol/http/body/stream.rb index 349c7b0..0a13ec4 100644 --- a/lib/protocol/http/body/stream.rb +++ b/lib/protocol/http/body/stream.rb @@ -21,6 +21,7 @@ def initialize(input = nil, output = Buffered.new) # Will hold remaining data in `#read`. @buffer = nil + @closed = false @closed_read = false end @@ -257,7 +258,7 @@ def close_read(error = nil) @closed_read = true @buffer = nil - input&.close(error) + input.close(error) end end @@ -266,12 +267,7 @@ def close_write(error = nil) if output = @output @output = nil - # This is a compatibility hack to work around limitations in protocol-rack and can be removed when external tests are passing without it. - if output.method(:close).arity == 1 - output.close(error) - else - output.close - end + output.close_write(error) end end diff --git a/lib/protocol/http/body/streamable.rb b/lib/protocol/http/body/streamable.rb index 320896d..9d0d072 100644 --- a/lib/protocol/http/body/streamable.rb +++ b/lib/protocol/http/body/streamable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2022, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require_relative 'readable' require_relative 'writable' @@ -137,7 +137,6 @@ def call(stream) # Closing a stream indicates we are no longer interested in reading from it. def close(error = nil) - $stderr.puts "Closing input: #{@input.inspect}" if input = @input @input = nil input.close(error) diff --git a/lib/protocol/http/body/writable.rb b/lib/protocol/http/body/writable.rb index c9bf874..3eb2e8e 100644 --- a/lib/protocol/http/body/writable.rb +++ b/lib/protocol/http/body/writable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. +# Copyright, 2024, by Samuel Williams. require_relative 'readable' @@ -65,17 +65,64 @@ def read # Write a single chunk to the body. Signal completion by calling `#finish`. def write(chunk) # If the reader breaks, the writer will break. - # The inverse of this is less obvious (*) if @closed raise(@error || Closed) end - @count += 1 @queue.push(chunk) + @count += 1 end + # This alias is provided for compatibility with template generation. alias << write + def close_write(error = nil) + @error ||= error + @queue.close + end + + class Output + def initialize(writable) + @writable = writable + @closed = false + end + + def closed? + @closed || @writable.closed? + end + + def write(chunk) + @writable.write(chunk) + end + + def close(error = nil) + @closed = true + + if error + @writable.close(error) + else + @writable.close_write + end + end + end + + # Create an output wrapper which can be used to write chunks to the body. + def output + output = Output.new(self) + + unless block_given? + return output + end + + begin + yield output + rescue => error + raise error + ensure + output.close(error) + end + end + def inspect "\#<#{self.class} #{@count} chunks written, #{status}>" end diff --git a/test/protocol/http/body/deflate.rb b/test/protocol/http/body/deflate.rb index 75e93f3..7d3e8a5 100644 --- a/test/protocol/http/body/deflate.rb +++ b/test/protocol/http/body/deflate.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require 'protocol/http/body/buffered' require 'protocol/http/body/deflate' @@ -17,7 +17,6 @@ it "should round-trip data" do body.write("Hello World!") - body.close expect(decompressed_body.join).to be == "Hello World!" end @@ -26,7 +25,6 @@ it "should round-trip data" do body.write(data) - body.close expect(decompressed_body.read).to be == data expect(decompressed_body.read).to be == nil @@ -39,7 +37,6 @@ 10.times do body.write("Hello World!") end - body.close 10.times do expect(decompressed_body.read).to be == "Hello World!" diff --git a/test/protocol/http/body/digestable.rb b/test/protocol/http/body/digestable.rb index 046b3b6..0dbb5ad 100644 --- a/test/protocol/http/body/digestable.rb +++ b/test/protocol/http/body/digestable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2020-2023, by Samuel Williams. +# Copyright, 2020-2024, by Samuel Williams. require 'protocol/http/body/digestable' require 'protocol/http/body/buffered' diff --git a/test/protocol/http/body/file.rb b/test/protocol/http/body/file.rb index e54d090..f5059a7 100644 --- a/test/protocol/http/body/file.rb +++ b/test/protocol/http/body/file.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2023, by Samuel Williams. +# Copyright, 2019-2024, by Samuel Williams. require 'protocol/http/body/file' diff --git a/test/protocol/http/body/streamable.rb b/test/protocol/http/body/streamable.rb index eb46409..c66a78d 100644 --- a/test/protocol/http/body/streamable.rb +++ b/test/protocol/http/body/streamable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2022, by Samuel Williams. +# Copyright, 2024, by Samuel Williams. require 'protocol/http/body/streamable' @@ -149,7 +149,6 @@ let(:block) do proc do |stream| while chunk = stream.read_partial - $stderr.puts "Got chunk: #{chunk.inspect}" stream.write(chunk) end end diff --git a/test/protocol/http/body/writable.rb b/test/protocol/http/body/writable.rb index 3da5cf6..e2facba 100644 --- a/test/protocol/http/body/writable.rb +++ b/test/protocol/http/body/writable.rb @@ -1,14 +1,17 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2018-2023, by Samuel Williams. +# Copyright, 2024, by Samuel Williams. require 'protocol/http/body/writable' require 'protocol/http/body/deflate' +require 'protocol/http/body/a_writable_body' describe Protocol::HTTP::Body::Writable do let(:body) {subject.new} + it_behaves_like Protocol::HTTP::Body::AWritableBody + with "#length" do it "should be unspecified by default" do expect(body.length).to be_nil