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 a in [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 = 1 and a.c = 2 agree while a.b = 1 and [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.

type Table

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.

type Value
= StringValue String
| IntegerValue BigInt
| FloatValue FloatValue
| BooleanValue Bool
| OffsetDateTimeValue DateTime
| LocalDateTimeValue DateTime
| LocalDateValue Date
| LocalTimeValue Time
| ArrayValue (Array Value)
| TableValue Table
| TableArrayValue (Array Table)

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

fromDocument : Document -> Result Error Table

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

get : String -> Table -> Maybe Value

The value stored under a key, if there is one.

entries : Table -> Dict String Value

Every key and value.

keys : Table -> Array String

Every key.

values : Table -> Array Value

Every value, in the order keys gives their keys.

member : String -> Table -> Bool

Whether there is a key of this name.

count : Table -> Int

How many keys the table has. Its own keys only: a sub-table counts once, whatever is inside it.

isEmpty : Table -> Bool

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

type alias Error = { path : String, reason : Reason }

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".

type Reason
= KeyDefinedTwice
| TableDefinedTwice
| NotATable
| TableIsClosed
| NotAnArrayOfTables

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.

errorToString : Error -> String

An error as a line of text.