Toml

Reading and writing TOML without losing anything on the way through.

Toml.parseBytes source
    |> Result.map Toml.toString
--> Ok (the same bytes back)

That round trip is exact for a document nobody has edited, and it is exact because of how the AST is shaped rather than because of any effort here: the whitespace, the comments and the source spelling of every number are all in it. See Toml.Ast.

Which module you want

Take the example 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"

parseBytes turns it into a Document, and every module below works on that or on something made from it. Each one has this same file read through in its own docs:

  • Read it into your own types -- Toml.Decode.

    Toml.Decode.fromBytes
        (Toml.Decode.at [ "servers", "alpha", "ip" ] Toml.Decode.string)
        source
    --> Ok "10.0.0.1"
    
  • Change one thing and write it back -- Toml.Edit.

    Toml.parseBytes source
        |> Result.map (Toml.Edit.set [ "database", "enabled" ] (Toml.Edit.bool False))
        |> Result.map Toml.toString
    
  • Build a file like it from nothing -- Toml.Encode.

  • Walk what the file says, when the keys are the data and no decoder can be written ahead of time -- Toml.Table.

  • Walk what the file is, character by character -- Toml.Ast.

type alias Document = Document

A parsed TOML file. See Toml.Ast.Document for what is in one.

Reading

parse : String -> Result Error Document

Read a TOML file from a string.

Offered for convenience and for callers who already have the text. It cannot detect an encoding error, by construction -- see parseBytes.

A byte order mark it can still see. Not every way of reading a file as text removes one, so if the string begins with U+FEFF that is taken to be the file's mark, dropped, and recorded so that toString puts it back. If something removed it before the text got here, the document is recorded as having none, since whatever removed it did not say.

parseBytes : Bytes -> Result Error Document

Read a TOML file from its bytes.

This is the one to use. It is the only entry point that can tell a valid file from an invalid one, because two of the things that make a TOML file invalid are decided before there is a String to look at:

  • The encoding. Bytes.toString is a strict UTF-8 decode, so a file with a bad byte sequence in it -- or a UTF-16 file, or a lone surrogate -- is refused here. By the time a caller holds a String, any such error has already been repaired into U+FFFD, and U+FFFD is a legal TOML character, so it cannot be found again afterwards.
  • The byte order mark. Exactly one leading mark is allowed and is not part of the document. Decoding removes it silently, so this function looks at the first three bytes itself and records what it saw, which is what lets toString put it back.
empty : Document

A document with nothing in it.

This exists for one situation, and it is the situation a program that keeps a config file is in every time it saves: the file may not be there yet.

when found is
    Nothing ->
        -- No file. The first run.
        Ok Toml.empty

    Just bytes ->
        Toml.parseBytes bytes

|> Result.map (Toml.Edit.set [ "theme" ] (Toml.Edit.string "dark"))

With it, creating the file and changing one value in it are the same code -- Toml.Edit.set adds a key that is not there, and every key is missing from an empty document. Without it a program needs a second path that writes the file from scratch, which is a second place to keep the shape of the file and the first place for the two to drift apart.

A file that is there and will not parse is not this. It is tempting to write Toml.parseBytes bytes |> Result.withDefault Toml.empty and be done with both cases at once, and it is the one thing not to write: a config file with a typo in it would parse to Err, become the empty document, and be written back over the user's file, comments and all, on the next save. empty is for a file that does not exist. A file that exists and cannot be read is an error to show the user, and the save has to not happen.

Toml.toString empty is "", and a value set into it lands on line one with no leading blank.

Writing

toString : Document -> String

Write a document back out.

toBytes : Document -> Bytes

Write a document back out as bytes, which is the way back from parseBytes.

The byte order mark comes with it if the source had one. toString puts the mark back too -- it is a character in the string it returns -- so this is Bytes.fromString after it and is offered only so that reading and writing a file are the same shape.

Errors

type alias Error = { row : Int, col : Int, expecting : String }

Where the parser stopped and what it wanted there.

errorToString : Error -> String

An error as a line of text.

"line 4, column 9: expected an '=' after the key"