Toml.Encode

Writing a TOML file from scratch.

Toml.Encode.toString
    [ Toml.Encode.field "name" (Toml.Encode.string "widget")
    , Toml.Encode.field "server"
        (Toml.Encode.table
            [ Toml.Encode.field "host" (Toml.Encode.string "example.com")
            , Toml.Encode.field "port" (Toml.Encode.int 8080)
            ]
        )
    ]

-->  name = "widget"
-->
-->  [server]
-->  host = "example.com"
-->  port = 8080

This is the other direction from Toml.Decode, and the other half of Toml.Edit: editing is for a file that exists, and this is for one that does not.

You choose the shape, not a heuristic

A TOML table can be written as a [header] section or as { inline = "table" }, and an array of tables as [[header]] sections or as an array of inline ones. Both spellings mean the same thing, so a library has to either guess or ask. This one asks: table and tableArray produce headers, inlineTable and array produce braces and brackets. The only place the choice is taken away is inside an array, where a header is not available and a table is written inline whatever it was built with.

Fields come out in the order you give them, because you chose it. Within a table, though, the scalars are written before the sections, since a key written after a [header] would land inside that header's table rather than beside it.

The style

One space either side of the equals sign, one blank line before each header and before each commented field, none at the top of the file or under a header, no indentation, \\n line endings, and a final newline. There is no way to change any of that, on the grounds that a file this produced has no author to have an opinion -- and the moment one does, the file should be read and edited rather than regenerated, which is what the rest of this package is for.

Comments are the exception that proves it, since a comment is content rather than style: commented puts one on a field, and where the blank line goes around it is still not yours to say.

A whole file

The example from https://toml.io, built from nothing. The nesting of the fields is the nesting of the file, and table at each level is what asks for a [header] rather than braces:

Toml.Encode.toString
    [ Toml.Encode.field "owner"
        (Toml.Encode.table
            [ Toml.Encode.field "name" (Toml.Encode.string "Tom Preston-Werner")
            , Toml.Encode.field "dob" (Toml.Encode.offsetDateTime dob)
            ]
        )
    , Toml.Encode.field "database"
        (Toml.Encode.table
            [ Toml.Encode.field "enabled" (Toml.Encode.bool True)
            , Toml.Encode.field "ports"
                (Toml.Encode.array
                    [ Toml.Encode.int 8000, Toml.Encode.int 8001, Toml.Encode.int 8002 ]
                )
            , Toml.Encode.field "data"
                (Toml.Encode.array
                    [ Toml.Encode.array
                        [ Toml.Encode.string "delta", Toml.Encode.string "phi" ]
                    , Toml.Encode.array [ Toml.Encode.float 3.14 ]
                    ]
                )
            , Toml.Encode.field "temp_targets"
                (Toml.Encode.inlineTable
                    [ Toml.Encode.field "cpu" (Toml.Encode.float 79.5)
                    , Toml.Encode.field "case" (Toml.Encode.float 72.0)
                    ]
                )
            ]
        )
    , Toml.Encode.field "servers"
        (Toml.Encode.table
            [ Toml.Encode.field "alpha"
                (Toml.Encode.table
                    [ Toml.Encode.field "ip" (Toml.Encode.string "10.0.0.1")
                    , Toml.Encode.field "role" (Toml.Encode.string "frontend")
                    ]
                )
            , Toml.Encode.field "beta"
                (Toml.Encode.table
                    [ Toml.Encode.field "ip" (Toml.Encode.string "10.0.0.2")
                    , Toml.Encode.field "role" (Toml.Encode.string "backend")
                    ]
                )
            ]
        )
    ]

where dob is Civil.DateTime.fromString "1979-05-27T07:32:00-08:00", unwrapped from its Maybe. That writes:

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

which is the original file except that the original had a space inside each bracket -- [ 8000, 8001, 8002 ] -- and this writes none. That difference is the whole distinction between this module and Toml.Edit. The spaces belonged to whoever wrote the file, and a file built from nothing has nobody to have written them.

Two things in that example are worth pointing at:

  • [servers] comes out on a line of its own with nothing under it, because servers has no keys of its own and two sub-tables. That is what the nesting said, and it is a legal and ordinary TOML file.
  • temp_targets uses inlineTable rather than table, which is the only reason it comes out in braces. Swap the two and it becomes a [database.temp_targets] section instead. Nothing here guesses.
type Value

A value on its way into a file.

type alias Field = { key : String, value : Value, comments : Comments }

A key, the value under it, and the comments that explain it.

Build one with field and, if it needs explaining, commented. The record is open so that it can be read apart; constructing one by hand works too and only saves a function call.

field : String -> Value -> Field

A key and the value under it.

Toml.Encode.field "port" (Toml.Encode.int 8080)

Comments

type alias Comments = { leading : Array String, trailing : Maybe String }

Comments on a field: a block of whole lines above it, and one after the value on the same line.

Each string is the text after the #, the leading space included, because # note and #note are different files and neither is this module's to choose.

There is no blank-line field here, unlike Toml.Edit.Comments, for the reason the whole of the style gives: a file this module wrote has no author to have an opinion about its spacing, so the spacing is decided here. A commented field gets a blank line above it.

commented : Comments -> Field -> Field

Explain a field.

Toml.Encode.toString
    [ Toml.Encode.field "a" (Toml.Encode.int 1)
    , Toml.Encode.field "port" (Toml.Encode.int 8080)
        |> Toml.Encode.commented
            { leading = [ " What to listen on." ], trailing = Just " http" }
    ]

-->  a = 1
-->
-->  # What to listen on.
-->  port = 8080 # http

The blank line above the block is the style rather than a choice, and it is not written at the top of a file or under a [header], where there would be nothing above it to separate it from.

A file nobody can read is not much better than no file, and comments are the reason to reach for a TOML library that keeps them rather than one that does not. This is that reason applied to the direction that writes a file instead of reading one.

On a field whose value is a table or a tableArray, the comments go with the [header] line, since that is the line the field became. On a tableArray they go with the first [[header]] only, the block above it and the trailing comment beside it -- they explain the key, and repeating them over every item would explain nothing.

On a field inside an array or an inlineTable they are dropped, because there is no line for them to be on. That is the same boundary Toml.Edit draws at a brace and for the same reason: what is inside one is written on one line, and a comment runs to the end of a line.

Writing it out

toDocument : Array Field -> Document

The fields as a document, which Toml.toString will write and which Toml.Edit can go on changing.

toString : Array Field -> String

The fields as text.

Scalars

string : String -> Value

A string.

int : Int -> Value

An integer that fits in a Gren Int.

bigInt : BigInt -> Value

An integer of any size.

float : Float -> Value

A float. inf and nan are written as those words; everything else gets a decimal point even when it does not need one, since 1 without one is an integer in TOML.

bigDecimal : BigDecimal -> Value

An exact decimal.

bool : Bool -> Value

A boolean.

offsetDateTime : DateTime -> Value

A date and time with an offset.

localDateTime : DateTime -> Value

A date and time with no offset.

localDate : Date -> Value

A date.

localTime : Time -> Value

A time of day.

Structures

array : Array Value -> Value

An array, on one line: [1, 2, 3].

Tables inside it are written inline, since there is no header to give them.

table : Array Field -> Value

A table written as a [header] section.

Toml.Encode.table [ Toml.Encode.field "port" (Toml.Encode.int 80) ]

-->  [server]
-->  port = 80

Except inside an array, where there is no header to be had and it comes out as { port = 80 }.

inlineTable : Array Field -> Value

A table written on one line: { port = 80 }.

tableArray : Array (Array Field) -> Value

An array of tables, written as a [[header]] for each.

Toml.Encode.tableArray
    [ [ Toml.Encode.field "name" (Toml.Encode.string "alpha") ]
    , [ Toml.Encode.field "name" (Toml.Encode.string "beta") ]
    ]

-->  [[peer]]
-->  name = "alpha"
-->
-->  [[peer]]
-->  name = "beta"

An empty one has no headers to write, so it is written as peer = [] rather than not at all. A reader cannot tell the two apart, and a key must not vanish from a file because its array happened to be empty.