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.
Something that knows how to turn a TOML value into an a, or say why it
cannot.
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
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.
The same for text you already have. It cannot detect an encoding error --
see Toml.parse.
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.
Run a decoder against a table you already have.
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
A TOML string.
A TOML boolean.
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.
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.
A TOML integer at its full width, whatever that is.
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
1979-05-27T07:32:00-08:00: a date and time that names a moment.
1979-05-27T07:32:00: a date and time that does not.
1979-05-27.
07:32:00.
Structures
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.
A table, with every value read the same way. For a table whose keys are data rather than structure.
The table itself, undecoded, for walking by hand.
The value itself, undecoded.
Fields
The value under a key.
The value under a path of keys, which is what a [a.b.c] header wrote.
Decode.at [ "servers", "alpha", "ip" ] Decode.string
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
A decoder that always produces this value and reads nothing.
A decoder that always fails with this message.
Transform what a decoder produces.
Combine two decoders.
Combine three.
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)
Decide what to read next from what was just read.
The first decoder that succeeds.
Nothing instead of an error.
Defer building a decoder, for one that refers to itself.