Skip to content
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
112 changes: 112 additions & 0 deletions spec/std/io/stapled_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
require "spec"

describe IO::Stapled do
it "combines two IOs" do
writer = IO::Memory.new
io = IO::Stapled.new IO::Memory.new("paul"), writer
io.gets.should eq "paul"
io << "peter"
writer.to_s.should eq "peter"
end

it "loops back" do
io = IO::Stapled.new(*IO.pipe)
io.puts "linus"
io.gets.should eq "linus"
end

describe "#close" do
it "does not close underlying IOs" do
reader, writer = IO::Memory.new, IO::Memory.new
io = IO::Stapled.new reader, writer
io.sync_close?.should be_false
io.close
io.closed?.should be_true
reader.closed?.should be_false
writer.closed?.should be_false
end

it "closes underlying IOs when sync_close is true" do
reader, writer = IO::Memory.new, IO::Memory.new
io = IO::Stapled.new reader, writer, sync_close: true
io.sync_close?.should be_true
io.close
io.closed?.should be_true
reader.closed?.should be_true
writer.closed?.should be_true
end

it "stops access to underlying IOs" do
reader, writer = IO::Memory.new("cle"), IO::Memory.new
io = IO::Stapled.new reader, writer
io.close
io.closed?.should be_true
reader.closed?.should be_false
writer.closed?.should be_false

expect_raises(IO::Error, "Closed stream") do
io.gets
end
expect_raises(IO::Error, "Closed stream") do
io.peek
end
expect_raises(IO::Error, "Closed stream") do
io << "closed"
end
end
end

it "#sync_close?" do
reader, writer = IO::Memory.new, IO::Memory.new
io = IO::Stapled.new reader, writer
io.sync_close = false
io.sync_close?.should be_false
io.sync_close = true
io.sync_close?.should be_true
io.close
reader.closed?.should be_true
writer.closed?.should be_true
end

it "#peek delegates to reader" do
reader = IO::Memory.new "cletus"
io = IO::Stapled.new reader, IO::Memory.new
io.peek.should eq "cletus".to_slice
io.gets
io.peek.should eq Bytes.empty
end

describe ".pipe" do
it "creates a bidirectional pipe" do
a, b = IO::Stapled.pipe
begin
a.sync_close?.should be_true
b.sync_close?.should be_true
a.puts "john"
b.gets.should eq "john"
b.puts "paul"
a.gets.should eq "paul"
ensure
a.close
b.close
end
end

it "with block creates a bidirectional pipe" do
ext_a, ext_b = nil, nil
IO::Stapled.pipe do |a, b|
ext_a, ext_b = a, b
a.sync_close?.should be_true
b.sync_close?.should be_true
a.puts "john"
b.gets.should eq "john"
b.puts "paul"
a.gets.should eq "paul"
a.sync_close = false
b.sync_close = false
end
ext_a.not_nil!.closed?.should be_true
ext_b.not_nil!.closed?.should be_true
end
end
end
122 changes: 122 additions & 0 deletions src/io/stapled.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# This class staples together two unidirectional `IO`s to form a single,
# bidirectional `IO`.
#
# Example (loopback):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use another example, maybe using two IO::Memory. When I first saw this I thought "why not use IO.pipe directly?".

# ```
# io = IO::Stapled.new(*IO.pipe)
# io.puts "linus"
# io.gets # => "linus"
# ```
#
# Most methods simply delegate to the underlying `IO`s.
class IO::Stapled < IO
# If `#sync_close?` is `true`, closing this `IO` will close the underlying `IO`s.
property? sync_close : Bool

# Returns `true` if this `IO` is closed.
#
# Underlying ÌO`s might have a different status.
getter? closed : Bool = false

# Creates a new `IO::Stapled` which reads from *reader* and writes to *writer*-
def initialize(@reader : IO, @writer : IO, @sync_close : Bool = false)
end

# Reads a single byte from `reader`.
def read_byte : UInt8?
check_open

@reader.read_byte
end

# Reads a slice from `reader`.
def read(slice : Bytes)
check_open

@reader.read(slice)
end

# Gets a string from `reader`.
def gets(delimiter : Char, limit : Int, chomp = false) : String?
check_open

@reader.gets(delimiter, limit, chomp)
end

# Peeks into *reader*.
def peek : Bytes?
check_open

@reader.peek
end

# Skips `reader`.
def skip(bytes_count : Int) : Nil
check_open

@reader.skip(bytes_count)
end

# Writes a byte to `writer`.
def write_byte(byte : UInt8) : Nil
check_open

@writer.write_byte(byte)
end

# Writes a slice to `writer`.
def write(slice : Bytes) : Nil
check_open

@writer.write(slice)
end

# `Flushes `writer`.
def flush : self
check_open

@writer.flush

self
end

# Closes this ÌO`.
#
# If `sync_close?` is `true`it will also close the underlying ÌO`s.
def close : Nil
return if @closed
@closed = true

if @sync_close
@reader.close
@writer.close
end
end

# Creates a pair of bidirectional pipe endpoints connected with each other
# and passes them to the given block.
#
# Both endpoints and the underlying ÌO`s are closed after the block
# (even if `sync_close?` is `false`).
def self.pipe(read_blocking : Bool = false, write_blocking : Bool = false)
IO.pipe(read_blocking, write_blocking) do |a_read, a_write|
IO.pipe(read_blocking, write_blocking) do |b_read, b_write|
a, b = new(a_read, b_write, true), new(b_read, a_write, true)
begin
yield a, b
ensure
a.close
b.close
end
end
end
end

# Creates a pair of bidirectional pipe endpoints connected with each other
# and returns them in a `Tuple`.
def self.pipe(read_blocking : Bool = false, write_blocking : Bool = false) : {self, self}
a_read, a_write = IO.pipe(read_blocking, write_blocking)
b_read, b_write = IO.pipe(read_blocking, write_blocking)
return new(a_read, b_write, true), new(b_read, a_write, true)
end
end