mpizenberg / elm-bytes-decoder / Bytes.Decode.Branchable

Parse Bytes with custom error reporting and context tracking.

Running the decoder


type Decoder value

A decoder for a certain type of value.

decode : Decoder value -> Bytes -> Maybe value

Run the given decoder on the provided bytes.

import Bytes.Encode as E
import Bytes.Decode.Branchable as D

E.string "hello"
    |> E.encode
    |> D.decode (D.string 5)
--> Just "hello"

E.string "hello"
    |> E.encode
    |> D.decode (D.string 6)
--> Nothing

Static decoders

succeed : value -> Decoder value

Always succeed with the given value.

import Bytes.Encode as E
import Bytes.Decode.Branchable as D

E.encode (E.sequence [])
    |> D.decode (D.succeed "hi there")
--> Just "hi there"

fail : Decoder value

A Decoder that always fails.

import Bytes.Encode as E
import Bytes.Decode.Branchable as D

E.sequence []
    |> E.encode
    |> D.decode D.fail
--> Nothing

Basic decoders

Integers

unsignedInt8 : Decoder Basics.Int

Decode one byte into an integer from 0 to 255.

unsignedInt16 : Bytes.Endianness -> Decoder Basics.Int

Decode two bytes into an integer from 0 to 65535.

unsignedInt32 : Bytes.Endianness -> Decoder Basics.Int

Decode four bytes into an integer from 0 to 4294967295.

signedInt8 : Decoder Basics.Int

Decode one byte into an integer from -128 to 127.

signedInt16 : Bytes.Endianness -> Decoder Basics.Int

Decode two bytes into an integer from -32768 to 32767.

signedInt32 : Bytes.Endianness -> Decoder Basics.Int

Decode four bytes into an integer from -2147483648 to 2147483647.

Floats

float32 : Bytes.Endianness -> Decoder Basics.Float

Decode 4 bytes into a Float.

float64 : Bytes.Endianness -> Decoder Basics.Float

Decode 8 bytes into a Float.

Strings

string : Basics.Int -> Decoder String

Decode count bytes representing UTF-8 characters into a String.

Note that Elm strings use UTF-16. As a result, the String.length will not always agree with the number of bytes that went into it!

import Bytes.Encode as E
import Bytes.Decode.Branchable as D

[ 0xF0, 0x9F, 0x91, 0x8D ]
    |> List.map E.unsignedInt8
    |> E.sequence
    |> E.encode
    |> D.decode (D.string 4)
--> Just "👍"

Decoding Bytes

bytes : Basics.Int -> Decoder Bytes

Parse count bytes as Bytes.

Transforming values

map : (a -> b) -> Decoder a -> Decoder b

Transform the value a decoder produces

import Bytes.Encode as E
import Bytes.Decode.Branchable as D

E.string "hello"
    |> E.encode
    |> D.decode (D.map String.length (D.string 5))
--> Just 5

map2 : (x -> y -> z) -> Decoder x -> Decoder y -> Decoder z

Combine two decoders into a single one.

import Bytes exposing (Bytes)
import Bytes.Encode as E
import Bytes.Decode.Branchable as D exposing (Decoder)

input : Bytes
input =
    [ E.unsignedInt8 2
    , E.string "wat"
    ]
        |> E.sequence
        |> E.encode

map2Example : Decoder String
map2Example =
    D.map2 String.repeat D.unsignedInt8 (D.string 3)

D.decode map2Example input
--> Just "watwat"

Note that the effect of map2 (and, in fact, every map variation) can also be achieved using a combination of succeed and keep.

equivalent : Decoder String
equivalent =
    D.succeed String.repeat
        |> D.keep D.unsignedInt8
        |> D.keep (D.string 3)

D.decode equivalent input
--> Just "watwat"

map3 : (w -> x -> y -> z) -> Decoder w -> Decoder x -> Decoder y -> Decoder z

Combine 3 decoders into a single one.

map4 : (v -> w -> x -> y -> z) -> Decoder v -> Decoder w -> Decoder x -> Decoder y -> Decoder z

Combine 4 decoders into a single one.

map5 : (u -> v -> w -> x -> y -> z) -> Decoder u -> Decoder v -> Decoder w -> Decoder x -> Decoder y -> Decoder z

Combine 5 decoders into a single one.

Combininig decoders

keep : Decoder a -> Decoder (a -> b) -> Decoder b

Keep the value produced by a decoder in a pipeline.

Together with succeed and ignore, this allows writing pretty flexible decoders in a straightforward manner: the order in which things are parsed is apparent.

import Bytes.Encode as E
import Bytes.Decode.Branchable as D exposing (Decoder)

decoder : Decoder (Int, Int)
decoder =
    D.succeed Tuple.pair
        |> D.keep D.unsignedInt8
        |> D.ignore D.unsignedInt8
        |> D.keep D.unsignedInt8

[ E.unsignedInt8 12
, E.unsignedInt8 3
, E.unsignedInt8 45
]
    |> E.sequence
    |> E.encode
    |> D.decode decoder
--> Just ( 12, 45 )

ignore : Decoder ignore -> Decoder keep -> Decoder keep

Ignore the value produced by a decoder.

Note that the decoder must still succeed for the pipeline to succeed. This means you can use this for checking the value of something, without using the value.

import Bytes.Encode as E
import Bytes.Decode.Branchable as D exposing (Decoder)

match : Int -> Decoder Int
match expected =
    D.unsignedInt8
        |> D.andThen
            (\actual ->
                if expected == actual then
                    D.succeed actual
                else
                    D.fail
            )

decoder : Decoder ()
decoder =
    D.succeed ()
        |> D.ignore (match 66)

E.unsignedInt8 66
    |> E.encode
    |> D.decode decoder
--> Just ()

E.unsignedInt8 44
    |> E.encode
    |> D.decode decoder
--> Nothing

skip : Basics.Int -> Decoder value -> Decoder value

Skip a number of bytes in a pipeline.

This is similar to ignore, but rather than decoding a value and discarding it, this just goes ahead and skips them altogether.

Advanced decoders

andThen : (a -> Decoder b) -> Decoder a -> Decoder b

Decode one thing, and then another thing based on the first thing.

This is very useful to make the content of your data drive your decoder. As an example, consider a string encoded as the length of the string, followed by the actual data:

import Bytes.Encode as E
import Bytes.Decode.Branchable as D exposing (Decoder)

string : Decoder String
string =
    D.unsignedInt8
        |> D.andThen D.string

[ E.unsignedInt8 5
, E.string "hello"
]
    |> E.sequence
    |> E.encode
    |> D.decode string
--> Just "hello"

oneOf : List (Decoder value) -> Decoder value

Tries a bunch of decoders and succeeds with the first one to succeed.

All decoder alternatives start at the same location in the bytes.

repeat : Decoder value -> Basics.Int -> Decoder (List value)

Repeat a given decoder count times.

The order of arguments is based on the common occurence of reading the number of times to repeat something through a decoder.

import Bytes.Encode as E
import Bytes.Decode.Branchable as D exposing (Decoder)

intList : Decoder (List Int)
intList =
    D.unsignedInt8
        |> D.andThen (D.repeat D.unsignedInt8)

[ 5, 0, 1, 2, 3, 4 ]
    |> List.map E.unsignedInt8
    |> E.sequence
    |> E.encode
    |> D.decode intList
--> Just [ 0, 1, 2, 3, 4 ]

loop : state -> (state -> Decoder (Bytes.Decode.Step state a)) -> Decoder a

Loop a decoder until it declares it is done looping.

The Step type in the signature comes from the Bytes.Decode module in elm/bytes. Here is how repeat is defined for example.

import Bytes.Decode
import Bytes.Decode.Branchable as D exposing (Decoder)

repeat : Decoder value -> Int -> Decoder (List value)
repeat p count =
    D.loop ( count, [] ) (repeatHelp p)

repeatHelp p ( count, acc ) =
    if count <= 0 then
        succeed (Bytes.Decode.Done (List.reverse acc))

    else
        map (\v -> Bytes.Decode.Loop ( count - 1, v :: acc )) p