APIReader
Reader
Buffered byte readers.
A Reader e reads bytes from memory or a Source, through operations that
perform exactly e, so parsing code written against Reader e works over
any of them and adds no IO of its own. The constructors (withBytes,
over, overBytes, limited) are scoped runners: the reader cannot
leave their callback, and the callback’s other effects pass through.
Separate readers advance independently.
Read
type Read = Found Bytes | Ended Bytes | OverflowedSource
The three distinct answers to a delimited read.
Constructors
Instances:
Eq Read
Show Read
Reader
type Reader e = { buffered : () ->{e} Bytes , refill : () ->{e} Bool , skip : Int ->{e} Int }Source
A byte reader whose operations perform e. The three fields are its
primitives, called by projection, as reader.refill(); the module’s
functions build reads from them.
atEnd
atEnd : Reader e ->{e} BoolSource
Whether no bytes remain, in hand or in the source.
import Bytes
import Reader
Reader.withBytes Bytes.empty { reader -> Reader.atEnd reader }
chunks
chunks : Reader e -> Stream Bytes {e}Source
The rest of the input as a stream of the source’s chunks. Traversing it advances the reader, so repeated traversals share its position.
import Bytes
import Reader
import Stream
Reader.withBytes (Bytes.fromString "abc") { reader -> Stream.toList (Reader.chunks reader) } == [Bytes.fromString "abc"]
ensure
ensure : Reader e -> Int ->{e} BoolSource
Grows the buffer until it holds n bytes, answering whether it does. It
consumes nothing.
import Bytes
import Reader
Reader.withBytes (Bytes.fromString "abc") { reader -> Reader.ensure reader 3 }
Reader.withBytes (Bytes.fromString "abc") { reader -> Reader.ensure reader 4 } == False
forEachChunk
forEachChunk : Reader e -> (Bytes ->{e} ()) ->{e} ()Source
Consumes the rest of the input, handing action each chunk as the source
produced it, so no chunk is resized or copied on the way.
limited
limited : Reader e -> Int -> (Reader {#s} ->{#s} a) ->{e} aSource
Runs action with a reader over at most allowance bytes of parent.
Every byte it hands out is consumed from the parent, so afterwards the
parent is positioned after what was read, not after the allowance; skip
the rest before leaving to discard a whole frame. The parent stays usable
inside the callback.
import Bytes
import Reader
framed =
Reader.withBytes (Bytes.fromString "abcdef") { reader ->
inner = Reader.limited reader 3 { frame -> Reader.readUpTo frame 10 }
(inner, Reader.readUpTo reader 10)
}
framed == (Bytes.fromString "abc", Bytes.fromString "def")
limitedRemaining
limitedRemaining : Reader e -> Int -> (Reader {#s} ->{#s} a) ->{e} (a, Int, Bool)Source
limited, also answering the allowance left unread and whether the parent
ended before the allowance was used. It does not pull from the parent to
find out, so a framed protocol can decide afterwards whether to reuse a
connection.
over
over : Source e -> (Reader {#s} ->{#s} a) ->{e} aSource
Runs action with a reader that pulls from source as it needs bytes.
overBytes
overBytes : Bytes -> (Reader {#s} ->{#s} a) ->{e} aSource
Runs action with a reader whose buffer starts with contents, over a
source that has already ended.
readExactly
readExactly : Reader e -> Int ->{e} Maybe BytesSource
Consumes and answers exactly n bytes, or Nothing, consuming nothing,
when fewer remain. The caller still holds what it could not frame.
import Bytes
import Reader
Reader.withBytes (Bytes.fromString "abc") { reader -> Reader.readExactly reader 2 } == Just (Bytes.fromString "ab")
Reader.withBytes (Bytes.fromString "abc") { reader -> Reader.readExactly reader 4 } == Nothing
readLine
readLine : Reader e -> Int ->{e} ReadSource
readUntil at a newline. A CRLF terminator leaves the carriage return on
the run, because whether that byte is framing or content is the caller’s
question; a protocol that says CRLF reads until its own delimiter.
import Bytes
import Reader
lines =
Reader.withBytes (Bytes.fromString "one\r\ntwo") { reader ->
first = Reader.readLine reader 100
second = Reader.readLine reader 100
[first, second]
}
lines == [Reader.Found (Bytes.fromString "one\r"), Reader.Ended (Bytes.fromString "two")]
readUntil
readUntil : Reader e -> Bytes -> Int ->{e} ReadSource
Reads up to delimiter, consuming it without answering it. limit bounds
the run and its delimiter together, which is all the call will ever
buffer: reading an unbounded line from an untrusted peer is a memory bug,
so there is no unlimited spelling. A delimiter straddling a refill is
found, and the empty delimiter is found at once.
import Bytes
import Reader
Reader.withBytes (Bytes.fromString "a;b") { reader -> Reader.readUntil reader (Bytes.fromString ";") 10 } == Reader.Found (Bytes.fromString "a")
Reader.withBytes (Bytes.fromString "ab") { reader -> Reader.readUntil reader (Bytes.fromString ";") 10 } == Reader.Ended (Bytes.fromString "ab")
Reader.withBytes (Bytes.fromString "abcdef;") { reader -> Reader.readUntil reader (Bytes.fromString ";") 4 } == Reader.Overflowed
readUpTo
readUpTo : Reader e -> Int ->{e} BytesSource
Consumes and answers up to n bytes: what is in hand, once there is
anything at all, so it never waits for bytes the caller said were
optional. A short answer is not the end of input; an empty one is.
withBytes
withBytes : Bytes -> (Reader {#s} ->{#s} a) ->{e} aSource
Runs action with a reader over contents in memory. With no other
effects in the callback, the call is pure. Nested calls give independent
readers that one parser can use together.
import Bytes
import Reader
Reader.withBytes (Bytes.fromString "abcdef") { reader -> Reader.readUpTo reader 4 } == Bytes.fromString "abcd"
