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

Found

Found : Bytes -> ReadSource

The run before the delimiter, both consumed.

Ended

Ended : Bytes -> ReadSource

The unterminated remainder at the end of input, consumed.

Overflowed

Overflowed : ReadSource

The limit was reached before either; nothing was consumed.

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.

Fields

buffered

buffered : () ->{e} BytesSource

The bytes in hand, without consuming them.

refill

refill : () ->{e} BoolSource

Pulls until the buffer grows, answering True, or the source ends, answering False. It never answers True without growing, so a loop on it makes progress.

skip

skip : Int ->{e} IntSource

Consumes up to n buffered bytes and answers how many it took.

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"