Toml.Table
The other layer: a TOML document as the data it describes.
Toml.Ast is the file. This is what the file says -- keys
resolved, dotted keys expanded, arrays of tables assembled, and every rule the
grammar cannot express checked on the way. It remembers nothing about
whitespace, comments or the order things appeared in, because none of that is
part of what a TOML document means.
An editing API works on the AST. A program that only wants to read a config file works on this.
This is where a document is rejected
About a third of the official suite's invalid files are perfectly good syntax.
[a] twice, a key defined twice, a dotted key reaching into a table that a
header already defined -- a parser cannot see any of that, because none of it is a question about the shape of the
text. It is all decided here, while the tree is being built, which is the only
place where enough is known to decide it.
The rules, in the terms this module thinks in: every table remembers how it came to exist, and what may be done to it afterwards follows from that.
- A table implied by being a prefix -- the
ain[a.b]-- is implicit, and a later[a]may still define it. - A table written as
[a]is explicit, and a second[a]is an error. So is a dotted key reaching into it from somewhere else. - A table created by a dotted key is closed to headers but still open to more
dotted keys beside it, which is what makes
a.b = 1anda.c = 2agree whilea.b = 1and[a]do not. - An inline table is closed to everything the moment its brace shuts.
Reading the example from 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"
Parse it, then lower it:
root : Result String Table
root =
Toml.parse source
|> Result.mapError Toml.errorToString
|> Result.andThen
(Toml.Table.fromDocument >> Result.mapError Toml.Table.errorToString)
Three keys, and they come out sorted rather than in file order, because this
is a Dict and file order is not part of what the document means. The order the
author wrote them in is in the AST, where it belongs:
Result.map Toml.Table.keys root
--> Ok [ "database", "owner", "servers" ]
Result.map Toml.Table.count root
--> Ok 3
Every value is one of the Value constructors, so reaching in is a when:
Result.map (Toml.Table.get "owner") root
--> Ok (Just (TableValue <a table of "dob" and "name">))
Result.map (Toml.Table.get "servers") root
--> Ok (Just (TableValue <a table of "alpha" and "beta">))
and reaching further in is more of them:
ipOfAlpha : Table -> Maybe String
ipOfAlpha root_ =
when Toml.Table.get "servers" root_ is
Just (TableValue servers) ->
when Toml.Table.get "alpha" servers is
Just (TableValue alpha) ->
when Toml.Table.get "ip" alpha is
Just (StringValue found) ->
Just found
_ ->
Nothing
_ ->
Nothing
_ ->
Nothing
Which is four levels of when to read one string, and is exactly why
Toml.Decode exists. Toml.Decode.at [ "servers", "alpha", "ip" ] Toml.Decode.string is the same walk, written once and reported on properly when
it fails. Use this module directly when the shape of the file is not known ahead
of time -- when the keys are the data -- and the decoder otherwise.
Two things in that file are worth noticing here, because they are this module's
business and nobody else's, and both of them are a line away from being an
error. [servers] is an empty header followed by [servers.alpha], so servers
is defined explicitly and then added to, which is allowed -- but a second
[servers] is not:
--> Err "the table is defined twice: servers"
And temp_targets is an inline table, which becomes an ordinary TableValue.
A closed one, though: adding temp_targets.gpu = 60 under [database] is
refused, where the same line for a table a header had written would be fine.
--> Err "this table is already complete and cannot be added to: temp_targets"
Neither file has anything wrong with its syntax. Toml.parse accepts both,
which is the division this package is built on -- see the two layers above.
A TOML table: keys to values, in no particular order.
Order is not part of what a TOML document means, and this type does not keep it. The AST does, and that is where to look if the order matters.
A TOML value.
The two array cases are the same thing to a reader and different things to a
writer: ArrayValue came from a [ ... ] literal and TableArrayValue was
built up out of [[ ... ]] headers. Keeping them apart is what lets the second
be appended to and the first not.
Building
Build the table tree from a parsed document, checking as it goes.
The walk is a fold over the expressions in file order, carrying the root and the
header currently in effect. That order is the whole reason the rules work:
[a] after [a.b] is fine and the reverse is not, and neither statement can be
judged without knowing what came before it.
Reading
The value stored under a key, if there is one.
Every key and value.
Every key.
Whether there is a key of this name.
How many keys the table has. Its own keys only: a sub-table counts once, whatever is inside it.
Whether the table has no keys at all. True of [a] with nothing under it,
which is a table that exists and is empty.
Errors
Why a document does not mean anything, and where.
path is the dotted key the trouble was at, so that a message can say
servers.alpha.ip rather than "a key".
What was wrong at the path. One case for each rule above; a program that
wants to tell a duplicate key from a redefined table can do so without parsing
the text errorToString writes.
An error as a line of text.