Toml.Decode

Reading a TOML file into your own types.

type alias Config =
    { host : String, port_ : Int, debug : Bool }

config : Decoder Config
config =
    Decode.map3 (\host p debug -> { host = host, port_ = p, debug = debug })
        (Decode.field "host" Decode.string)
        (Decode.field "port" Decode.int)
        (Decode.field "debug" Decode.bool)

Decode.fromBytes config source

The shape will be familiar from Json.Decode, and so will the reason for it: a config file is data of an unknown shape until something checks, and a decoder is that check written down once, in the same place as the thing it produces.

This is the convenient layer, not the only one. A program that wants the whole tree can ask for table and walk Toml.Table itself, and one that wants to edit a file rather than read it wants Toml.Ast instead -- decoding, by design, throws away everything an edit needs.

A real example

The file from https://toml.io:

[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00

[database]
enabled = true
ports = [ 8000, 8001, 8002 ]
data = [ ["delta", "phi"], [3.14] ]
temp_targets = { cpu = 79.5, case = 72.0 }

[servers]

[servers.alpha]
ip = "10.0.0.1"
role = "frontend"

[servers.beta]
ip = "10.0.0.2"
role = "backend"

One value at a time, to show what each decoder does:

Decode.fromString (Decode.at [ "owner", "name" ] Decode.string) source
--> Ok "Tom Preston-Werner"

Decode.fromString (Decode.at [ "owner", "dob" ] Decode.offsetDateTime) source
--> Ok <1979-05-27T07:32:00-08:00, offset kept as -08:00>

Decode.fromString (Decode.at [ "database", "ports" ] (Decode.array Decode.int)) source
--> Ok [ 8000, 8001, 8002 ]

temp_targets and servers are both tables whose keys are data -- one is an inline table and the other is three headers, and neither difference reaches this layer. dict is the decoder for both:

Decode.fromString (Decode.at [ "database", "temp_targets" ] (Decode.dict Decode.float)) source
--> Ok (Dict.fromArray [ { key = "case", value = 72 }, { key = "cpu", value = 79.5 } ])

Decode.fromString (Decode.field "servers" (Decode.dict (Decode.field "ip" Decode.string))) source
--> Ok (Dict.fromArray [ { key = "alpha", value = "10.0.0.1" }, { key = "beta", value = "10.0.0.2" } ])

And the whole thing at once, which is how a program would really read it:

type alias Config =
    { owner : String
    , ports : Array Int
    , servers : Dict String Server
    }

type alias Server =
    { ip : String, role : String }

server : Decoder Server
server =
    Decode.map2 (\ip role -> { ip = ip, role = role })
        (Decode.field "ip" Decode.string)
        (Decode.field "role" Decode.string)

config : Decoder Config
config =
    Decode.map3 (\owner ports servers -> { owner = owner, ports = ports, servers = servers })
        (Decode.at [ "owner", "name" ] Decode.string)
        (Decode.at [ "database", "ports" ] (Decode.array Decode.int))
        (Decode.field "servers" (Decode.dict server))

data = [ ["delta", "phi"], [3.14] ] is the awkward one, and deliberately so: TOML 1.1 lets an array hold values of different types, and Gren does not. There is no Array a to decode that into, so something has to say what the two arms have in common. oneOf is where that is said:

Decode.fromString
    (Decode.at [ "database", "data" ]
        (Decode.array
            (Decode.oneOf
                [ Decode.array Decode.string
                , Decode.map (Array.map String.fromFloat) (Decode.array Decode.float)
                ]
            )
        )
    )
    source
--> Ok [ [ "delta", "phi" ], [ "3.14" ] ]

When it goes wrong, the error says where. The path is the one you asked for, not the one the file happens to use:

Decode.fromString (Decode.at [ "owner", "email" ] Decode.string) source
    |> Result.mapError Decode.errorToString
--> Err "at owner.email: no such key"

Decode.fromString (Decode.at [ "database", "enabled" ] Decode.int) source
    |> Result.mapError Decode.errorToString
--> Err "at database.enabled: expected an integer, found a boolean"

int is the one to read the docs for

TOML integers have no width here: they are BigInts, because a BigInt can hold 9223372036854775807 and a Gren Int cannot. int therefore fails on a number too large to be exact as a Gren Int -- above 2^53 -- rather than handing back a number that is nearly right. If you want the whole value, ask for bigInt.

type Decoder a

Something that knows how to turn a TOML value into an a, or say why it cannot.

type Error
= Syntax Error
| Semantics Error
| Field ({ name : String, inner : Error })
| Index ({ at : Int, inner : Error })
| Expected ({ wanted : String, found : String })
| OneOfFailed (Array Error)
| Custom String

Why a file did not become the value you asked for.

The first two are about the file and the rest are about the shape of what was in it. Field and Index wrap the error underneath them, so the whole chain says where it happened -- which is what errorToString renders as a path.

Running a decoder

fromBytes : Decoder a -> Bytes -> Result Error a

Parse, check and decode, in one call. The entry point to reach for.

Bytes rather than a string for the reason Toml.parseBytes gives: an encoding error cannot be found once the bytes are gone.

fromString : Decoder a -> String -> Result Error a

The same for text you already have. It cannot detect an encoding error -- see Toml.parse.

fromDocument : Decoder a -> Document -> Result Error a

Decode a document that has already been parsed.

For a caller who is holding one because they are also editing it -- see Toml.Edit -- and would rather not parse the file twice. The document is lowered to a table on the way, which is where a file is rejected for the things a grammar cannot see, so this can fail with a Semantics error that fromTable never can.

fromTable : Decoder a -> Table -> Result Error a

Run a decoder against a table you already have.

errorToString : Error -> String

An error as a line of text, with the path to where it happened.

"at server.ports[2]: expected an integer, found a string"

Primitives

string : Decoder String

A TOML string.

bool : Decoder Bool

A TOML boolean.

int : Decoder Int

A TOML integer, as a Gren Int.

Fails when the number is too big for one to hold exactly -- above 2^53, since a Gren Int is a double. That is a refusal rather than a rounding, and it is the whole reason bigInt exists.

float : Decoder Float

A TOML float, as a Gren Float.

inf and nan come through as the Float values of those names. A finite one is converted from its exact decimal, and that conversion is where precision is lost -- 0.1 in the file is exactly 0.1 until this point. Ask for bigDecimal to keep it.

bigInt : Decoder BigInt

A TOML integer at its full width, whatever that is.

bigDecimal : Decoder BigDecimal

A finite TOML float as an exact decimal.

inf and nan are not decimals and fail here; float is the one that takes them.

Dates and times

offsetDateTime : Decoder DateTime

1979-05-27T07:32:00-08:00: a date and time that names a moment.

localDateTime : Decoder DateTime

1979-05-27T07:32:00: a date and time that does not.

localDate : Decoder Date

1979-05-27.

localTime : Decoder Time

07:32:00.

Structures

array : Decoder a -> Decoder (Array a)

An array, with every element read the same way.

Both kinds of TOML array are accepted: a [ ... ] literal and one built out of [[ ... ]] headers. They are the same array to a reader, and the difference is only about what may be done to them in the file.

dict : Decoder a -> Decoder (Dict String a)

A table, with every value read the same way. For a table whose keys are data rather than structure.

table : Decoder Table

The table itself, undecoded, for walking by hand.

value : Decoder Value

The value itself, undecoded.

Fields

field : String -> Decoder a -> Decoder a

The value under a key.

at : Array String -> Decoder a -> Decoder a

The value under a path of keys, which is what a [a.b.c] header wrote.

Decode.at [ "servers", "alpha", "ip" ] Decode.string
optionalField : String -> Decoder a -> Decoder (Maybe a)

The value under a key, or Nothing when there is no such key.

A key that is there but holds the wrong kind of value is still an error. Use maybe (field ...) for the reading that forgives that too.

Combining

succeed : a -> Decoder a

A decoder that always produces this value and reads nothing.

fail : String -> Decoder a

A decoder that always fails with this message.

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

Transform what a decoder produces.

map2 : (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c

Combine two decoders.

map3 :
(a -> b -> c -> d)
-> Decoder a
-> Decoder b
-> Decoder c
-> Decoder d

Combine three.

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

Combine any number, by pipeline.

Decode.succeed Config
    |> Decode.andMap (Decode.field "host" Decode.string)
    |> Decode.andMap (Decode.field "port" Decode.int)
    |> Decode.andMap (Decode.field "debug" Decode.bool)
andThen : (a -> Decoder b) -> Decoder a -> Decoder b

Decide what to read next from what was just read.

oneOf : Array (Decoder a) -> Decoder a

The first decoder that succeeds.

maybe : Decoder a -> Decoder (Maybe a)

Nothing instead of an error.

lazy : ({} -> Decoder a) -> Decoder a

Defer building a decoder, for one that refers to itself.