Toml.Edit

Changing a TOML file and leaving the rest of it alone.

Toml.parseBytes source
    |> Result.map (Toml.Edit.set [ "server", "port" ] (Toml.Edit.int 9090))
    |> Result.map Toml.toString

Every byte the edit did not touch comes back as it was: the blank lines, the alignment, the comments, and the 0x1F you wrote instead of 31. That is what this module is for, and it is why the AST underneath it stores whitespace as text -- see Toml.Ast.

And an edit that changes nothing touches nothing. Setting a value that is already what the file says leaves the line exactly as it is written -- see set, where the reason it has to work that way is spelled out. It is what makes "write my whole configuration back" a safe thing for a program to do on every save.

The functions, in short

Most of them come in pairs: the same thing for a key and for a table, or for a whole value and for one element of an array.

Function What it is for
get The value at a path, if a key there holds one
member Whether a key is in the file at all
paths Every key, in the order the file writes them
set Put a value at a path, adding the key if it is missing
respell Rewrite a value the file already means, to change its spelling
introduce set, and comments too when this call invented the key
remove Take a key out, with the comments that belong to it
removeTable Take a [header] out, with everything under it
appendTo One more element on the end of an array
insertAt One more element at a position
setAt Replace one element
respellAt Rewrite one element, to change its spelling
removeAt Take one element out, with the note against it
moveAt Move one element, its note with it
rename Rename a key, keeping its value and its comments
renameTable Rename a table, and the headers nested under it
comments The comments that belong to a key
setComments Write those back
tableComments The comments that belong to a [header] line
setTableComments Write those back

The rest of the module is the values to put in -- string, int and the others at the bottom -- which build the text for a value as well as the value itself.

Which comments belong to a key

Deleting a key should delete the comment that explains it and leave alone the one that explains the file. Nothing in TOML says which is which, so this is a convention, and here it is:

  • Leading. Own-line comments directly above a key, with no blank line between, belong to it. They go when it goes.
  • Trailing. A comment on the same line, after the value, belongs to it.
  • Floating. Anything after a blank line belongs to nobody and survives.
  • Header. Comments before the first key belong to the document.

The blank line is the escape hatch, and it is worth knowing about. A comment block that introduces a whole section rather than the one key under it should have a blank line beneath it, or deleting that key will take the section heading with it.

A [header] line owns comments the same way a key does: the block directly above it and the comment beside it are the header's. removeTable takes them with the table, tableComments reads them and setTableComments writes them.

What a path means here

A path is the key as the table tree sees it: [ "server", "port" ] finds port under [server], and finds it equally whether the file wrote it as a header and a key, as server.port = 9090, or as a header and a dotted key.

It does not reach inside an inline table or an array. a = {b = 1} has the path [ "a" ] and nothing under it; to change b, replace the whole inline table. That boundary is where the round trip stops being obvious -- an inline table has no lines to insert into -- so it is drawn here rather than guessed at.

A path into an array of tables names its last item. That is what TOML itself means by the path: a [peer.tls] header or a peer.x = 1 key after the second [[peer]] belongs to the second one. So [ "peer", "x" ] reads, changes or removes the x of the last [[peer]], and a key that is not there yet is added to the last one too. The earlier items are reached by walking the AST, which is what it is there for.

What this module does not check

Only syntax. Whether the file still means something afterwards is decided by Toml.Table.fromDocument, as it is for a file someone typed, and an edit can produce one it rejects. The ways that happen are the ways a path can point at something a key cannot be added beside:

  • set [ "a", "b" ] when a is an inline table. The path does not reach in, so a [a] header is added for b, and a is now defined twice.
  • set [ "a", "b", "c" ] when a.b = 1 wrote b as a dotted key. A [a.b] header is added, and a header may not land on a table a dotted key built.
  • set [ "a" ] when [a] is a header. The key and the table collide.

None of those are refused here, because the rule that refuses them is a rule about what the document means and lives in the layer that decides that. After editing, read the result back through Toml.Decode.fromDocument or Toml.Table.fromDocument before writing it out, if the paths were not yours to begin with.

A real example

The file from https://toml.io. Everything below starts from it, and only the lines that change are shown -- every other byte of the file comes back as it was, which is the point:

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

Change one value. The key, the equals sign and everything around them are untouched:

Toml.Edit.set [ "servers", "alpha", "ip" ] (Toml.Edit.string "10.0.0.9")

-->  [servers.alpha]
-->  ip = "10.0.0.9"
-->  role = "frontend"

Change an array. A value written fresh is spelled by this module rather than by the file, so the author's spaces inside the brackets are not preserved -- there is nothing left of the old array to preserve them from:

Toml.Edit.set [ "database", "ports" ]
    (Toml.Edit.array [ Toml.Edit.int 9000, Toml.Edit.int 9001 ])

-->  ports = [9000, 9001]

Add a table. Neither [servers.gamma] nor the keys under it exist, so the header comes with the first key and the second lands beside it:

Toml.Edit.set [ "servers", "gamma", "ip" ] (Toml.Edit.string "10.0.0.3")
    >> Toml.Edit.set [ "servers", "gamma", "role" ] (Toml.Edit.string "cache")

-->  [servers.beta]
-->  ip = "10.0.0.2"
-->  role = "backend"
-->
-->  [servers.gamma]
-->  ip = "10.0.0.3"
-->  role = "cache"

Write comments. blankBefore is how a block gets an empty line above it, which is the only way to ask for one -- a document is a list of expressions and has no lines to insert between:

Toml.Edit.setComments [ "database", "enabled" ]
    { blankBefore = False
    , leading = [ " Turn this off to go quiet." ]
    , trailing = Just " on by default"
    }

-->  [database]
-->  # Turn this off to go quiet.
-->  enabled = true # on by default

Explain a section. A header owns the block above it the way a key does, and setTableComments writes it:

Toml.Edit.setTableComments [ "servers", "alpha" ]
    { blankBefore = True, leading = [ " The primary." ], trailing = Nothing }

-->  # The primary.
-->  [servers.alpha]
-->  ip = "10.0.0.1"

Write a value and explain it, but only the first time. introduce is set plus the comments a key gets when this is the call that invented it, and nothing at all to a key already in the file:

Toml.Edit.introduce [ "database", "timeout" ]
    { blankBefore = True, leading = [ " Seconds." ], trailing = Nothing }
    (Toml.Edit.int 30)

-->  ports = [ 8000, 8001, 8002 ]
-->  ...
-->
-->  # Seconds.
-->  timeout = 30

Rename. A key by rename and a table by renameTable, which takes the headers nested under it too:

Toml.Edit.rename [ "servers", "alpha", "role" ] "job"
    >> Toml.Edit.renameTable [ "database" ] "db"

-->  [db]
-->  enabled = true
-->  ...
-->  [servers.alpha]
-->  ip = "10.0.0.1"
-->  job = "frontend"

Remove a table, and its blank line goes with it:

Toml.Edit.removeTable [ "servers", "beta" ]

-->  [servers.alpha]
-->  ip = "10.0.0.1"
-->  role = "frontend"
-->  <end of file>

Put together, an edit is a pipeline from Toml.parseBytes to Toml.toString:

Toml.parseBytes source
    |> Result.map
        (Toml.Edit.set [ "database", "ports" ] (Toml.Edit.array [ Toml.Edit.int 9000 ])
            >> Toml.Edit.set [ "servers", "gamma", "ip" ] (Toml.Edit.string "10.0.0.3")
            >> Toml.Edit.renameTable [ "database" ] "db"
        )
    |> Result.map Toml.toString

To read the edited file back without writing it out and parsing it again, hand the document to Toml.Decode.fromDocument.

The file that is not there yet

A program that keeps a config file has to handle its first run, and the tidy way to do that is not to handle it: Toml.empty is a document with nothing in it, every key is missing from it, and set adds a key that is missing. So

(when found is
    Nothing ->
        Ok Toml.empty

    Just bytes ->
        Toml.parseBytes bytes
)
    |> Result.map (Toml.Edit.introduce [ "theme" ] note (Toml.Edit.string "dark"))

creates the file and updates it with the same code, and there is no second description of the file's shape to drift out of step with the first. The when is not decoration: Toml.empty stands in for a file that is not there, and a file that is there and fails to parse has to stay an Err, or the next save writes an empty document over it -- see Toml.empty.

Toml.Encode is the other way to build a file from nothing, and it is the better one when the shape of the file comes from the shape of the data -- nested tables, arrays of tables, a section per item. This module is the better one when the file has a fixed set of keys and the same code has to cope with the file already existing.

get : Array String -> Document -> Maybe Value

The value at a path, if a key there holds one.

member : Array String -> Document -> Bool

Whether a key is in the document at all.

get answers this too, but answering it with get means holding a Toml.Ast.Value in order to throw it away, and the question a program writing a config file asks is usually this one: is this key already here, or am I the one putting it there? That is what decides whether it needs an explanation written above it -- see introduce.

set : Array String -> Value -> Document -> Document

Put a value at a path.

If a key is already there, only its value changes: the key keeps its spelling, the equals sign keeps the whitespace around it, and the comment on the line stays where it was. Nothing else in the file moves.

If there is no such key, one is added. It goes at the end of the table it belongs to, after the last key already in it, indented to match. If the table does not exist either, a [header] for it is appended to the end of the file with the key beneath and a blank line above, and a file that ended with a newline still does.

A top-level key added to a file that has no top-level keys yet goes before the first [header], with a blank line between, since anything written after a header belongs to that header's table. It goes above the comment block directly over that header too, because that block is the header's by the convention above; a comment with a blank line under it stays where it is.

A value that is already what you are setting it to is left exactly as it is written. This is not thrift, it is the difference between an editor and an encoder, and it is the one thing about this module worth reading twice.

Setting a value replaces the whitespace inside it, because the whitespace was part of the old value and the new one has none of its own. So a list somebody arranged by hand:

ports = [
  8000,  # http
  8001   # https
]

written back with set [ "ports" ] (array [ int 8000, int 8001 ]) would come out as ports = [8000, 8001]. That is right for a list that changed -- there is no old formatting to keep for a new value -- and it is destructive for one that did not.

Which is the case that actually happens. A program that writes its whole configuration out on every save touches every key, so the key the user hand-arranged gets flattened by a save about some other key. Comparing first is what stops that, and doing it here rather than leaving it to every caller is the only way it gets done: the caller would have to hold the old document, take it apart, and know that 1.50 and 1.5 are the same number and 0x1F and 31 are the same integer.

The comparison is by value and not by text. A string is its characters after escapes, an integer or float is its number whatever base or spelling it was written in, a date-time is the instant and the offset, an array is its elements in order, and an inline table is its entries in order. So set over 0x1F with int 31 changes nothing and the file keeps saying 0x1F. To change how a value is written rather than what it means, use respell.

An edit that gives the file a meaning it cannot have -- a key under an inline table, say -- is not refused here. See what this module does not check.

respell : Array String -> Value -> Document -> Document

Write a value even when the file already means it, so that how the value is written changes.

Toml.Edit.respell [ "server", "port" ] (Toml.Edit.int 31)

-->  port = 31          # was: port = 0x1F

set leaves a value alone when it already means what it is being set to. That rule is what makes it safe to write a whole configuration back on every save, but it also means set can never change how a value is spelled. respell is set without that rule.

The key keeps everything except its value: its comments, the whitespace around the equals sign, and its place in the file. Before this function existed the only way to change a spelling was remove followed by set, which loses the key's comments along with the line.

Use it on the one key whose spelling you mean to change, not in the code that saves everything. The usual case is a program that used to write a value with string and now writes it with multilineString: set sees no change and leaves older files spelled the old way, and respell updates them. Calling it on every key on every save would undo the user's own choices, turning their 'C:\Users' back into a basic string each time.

On a key that is not in the document this is the same as set.

introduce : Array String -> Comments -> Value -> Document -> Document

Set a value, and explain the key if this is the call that put it there.

Toml.Edit.introduce [ "theme" ]
    { blankBefore = True
    , leading = [ " Which colour scheme to open in." ]
    , trailing = Nothing
    }
    (Toml.Edit.string "dark")

set and setComments together, except that the comments are written only when the key was not already in the document.

That condition is the whole reason this exists, and it is a rule about the person on the other end of the file rather than about TOML. A program that keeps a config file wants every key it invents to arrive with a sentence saying what the key is for, because a config file whose fields are undocumented is one nobody opens. It does not want to write that sentence again on every save: the user who deleted it meant to delete it, and the user who rewrote it in their own language has not made a mistake for the next save to correct.

On a document with the key already in it this is exactly set, comments and all left alone. On Toml.empty it writes the key and the explanation together, which is how a file that has never existed comes out documented.

remove : Array String -> Document -> Document

Take a key out, and the comments that belong to it with it.

The line goes, its leading comment block goes, and its trailing comment goes with the line it was on. A floating comment -- one with a blank line above it -- stays, which is the whole point of the blank line.

The blank line above the block goes too, but only when the block leaves a blank line or the end of the file below it. This is what makes remove the inverse of introduce: blankBefore is a field of the key's own Comments, so a program that writes a key when a setting is on and removes it when the setting goes off would otherwise leave one empty line behind per cycle. A block with a blank on one side only keeps it, because that blank is what separates what is above the key from what is below it, and both are staying.

Nothing else changes. Removing the last key of a table leaves the [header] behind, because an empty table is still a table and saying otherwise would be this module deciding something the file did not.

removeTable : Array String -> Document -> Document

Take a whole table out: its [header], everything under it, and the comments that belong to the header.

Sub-tables go too, since [a.b] is under [a]. An array of tables named by the path goes entirely, every item of it.

One element of an array

appendTo : Array String -> Value -> Document -> Document

Add one element to the end of the array a key holds. The elements already in it are left exactly as they are written.

Toml.Edit.appendTo [ "zones" ] (Toml.Edit.string "Europe/Oslo")

-->  zones = [
-->    "Asia/Seoul",      # them
-->    "America/Chicago", # me
-->    "Europe/Oslo",
-->  ]

set can write a whole array, and that is fine for a list where every element is new. But a new array has no formatting, so set throws away the line breaks, indentation and comments of a list somebody arranged by hand. appendTo, setAt and removeAt change one element and leave the rest of the text alone.

The new element is laid out like the one before it: on its own line with the same indent if the array is written that way, or after a space if the array is on one line. It gets a trailing comma if the last element had one, and no comma if it did not.

If the key is not there yet, it is written as a one-element array, so "add this to my list" works on a file that does not have the list yet. If the key holds something other than an array, the document is returned unchanged.

An array inside an inline table counts as not there, because a path stops at the brace. appendTo [ "a", "b" ] over a = { b = [1] } does not reach b; it writes an [a] header with a new b under it, and Toml.Table.fromDocument then rejects the file for defining a twice. set has the same limit, and the fix is the same: replace the whole inline table with inlineTable.

insertAt : Array String -> Int -> Value -> Document -> Document

Add one element to the array a key holds, at a position. The elements already in it are left exactly as they are written.

Toml.Edit.insertAt [ "zones" ] 1 (Toml.Edit.string "Europe/Oslo")

-->  zones = [
-->    "Asia/Seoul",      # them
-->    "Europe/Oslo",
-->    "America/Chicago", # me
-->  ]

The new element takes the line of the one it displaces: the same indent, and a comma, since something follows it now. The element it pushed down gets the same separator with nothing written on it, because the note that was on that line was written against the value above and stays where it is.

The new element has no note of its own, which is the only honest default: a note is written against a value, and this value is new. Nothing here writes one either -- a note against an element is the file's, and the one function that moves one is moveAt, which takes an element's own along with it.

An index equal to the length of the array appends, which is the one case where this and appendTo do the same thing -- including writing a one-element array for a key that is not there yet. Any other index that is not in the array leaves the document unchanged, as does a key that holds something other than an array.

setAt : Array String -> Int -> Value -> Document -> Document

Replace one element of the array a key holds, by position. Nothing else changes: not the elements around it, not the whitespace, not the comments.

Toml.Edit.setAt [ "zones" ] 1 (Toml.Edit.string "America/Denver")

-->  zones = [
-->    "Asia/Seoul",     # them
-->    "America/Denver", # me
-->  ]

The document comes back unchanged if the index is out of range, the key is missing, or the key is not an array.

Like set, this leaves an element alone when it already means the new value. Rewriting it would replace the whitespace inside it, and a program that writes its whole configuration back on every save would then flatten an array it was not changing. To rewrite the element anyway, use respellAt.

removeAt : Array String -> Int -> Document -> Document

Remove one element from the array a key holds, by position, along with the comment that belongs to it.

Toml.Edit.removeAt [ "zones" ] 0

-->  zones = [
-->    "America/Chicago", # me
-->  ]

Which comment belongs to which element is a convention, as it is for keys: the comment after an element's comma, up to the end of that line, belongs to that element. It is the note a person writes against the value, so it goes when the value goes. Everything after that line ending is the next element's indent and stays put.

Removing the last element takes a little more care, because the trailing comma and the position of the ] are both written against it. The element before it inherits both.

The document comes back unchanged if the index is out of range, the key is missing, or the key is not an array. A negative index is out of range; it does not count from the end.

respellAt : Array String -> Int -> Value -> Document -> Document

Replace one element of an array even when it already means the new value, so that how the element is written changes.

Toml.Edit.respellAt [ "zones" ] 1
    (Maybe.withDefault (Toml.Edit.string "America/Chicago")
        (Toml.Edit.literalString "America/Chicago")
    )

-->  zones = [
-->    "Asia/Seoul",      # them
-->    'America/Chicago', # me
-->  ]

This is to setAt what respell is to set. setAt skips an element that already means the new value, which is the right default but makes it unable to change a spelling. The element keeps its line, its indent and the comment written against it; only the value is rewritten.

Unlike respell, this never creates anything. An index out of range, a missing key, or a key that is not an array all leave the document unchanged.

moveAt : Array String -> Int -> Int -> Document -> Document

Move one element of the array a key holds from one position to another, and take the note written against it along.

Toml.Edit.moveAt [ "zones" ] 1 0

-->  zones = [
-->    "America/Chicago", # me
-->    "Asia/Seoul",      # them
-->  ]

to is the index the element ends up at, so a Move Up button is moveAt path index (index - 1) and a Move Down is moveAt path index (index + 1).

This is a function of its own rather than a removeAt followed by an insertAt because that pair cannot carry the note: removeAt takes it away with the element, which is right, and insertAt writes an element that has none. Walking the new order down the list with setAt instead keeps every note, but keeps it against the position, so a move leaves # me written against somebody else's city, silently, which is worse than losing it. A note belongs to the value it was written for, and here it goes where that value goes.

Only the value and its note move. The indent and the trailing comma are the destination's, because those describe a place in the list rather than the thing in it, and the last position's are the ones that keep the closing bracket where the file put it.

The document comes back unchanged if either index is out of range, if the key is missing, or if the key is not an array. Moving an element to where it already is changes nothing at all. There is one more: an element with a note cannot move onto a line that another element also ends, since there is nowhere on it to write the note. Below, # one has nowhere to go on the line 2 and 3 share, so moveAt [ "a" ] 0 1 leaves the file alone.

a = [
  1, # one
  2, 3,
]

Renaming

rename : Array String -> String -> Document -> Document

Give a key a new name and change nothing else.

Toml.Edit.rename [ "server", "port" ] "listen"

-->  [server]
-->  listen = 8443       # was: port = 8443

The value stays as it was written, the whitespace around the equals sign stays, and the comments stay with the line. The new name is spelled bare where the grammar allows it and quoted where it does not.

Only the last part of the path is the name: this renames the key port, not the table server. For a table see renameTable.

Whether the new name is already taken is not asked here. It is not a question about syntax, so the answer comes from Toml.Table.fromDocument, like every other question of its kind -- see the two layers in Toml.Table.

renameTable : Array String -> String -> Document -> Document

Give a table a new name: its own [header], every header nested under it, and every dotted key that spells the name out itself.

Toml.Edit.renameTable [ "server" ] "listener"

-->  [listener]
-->  [listener.tls]

Only the last part of the path is the name, as with rename, so a table's ancestors never move and nothing has to be lifted into a different section for the result to mean what it says.

A dotted key changes only where the renamed part is a table it passes through: renameTable [ "a" ] "x" turns a.b = 1 into x.b = 1 and leaves b alone, because b names the value rather than a table. By the same rule this does nothing to a path that names a key: [ "server", "port" ] is not a table, and rename is the one that renames it.

Comments

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

The comments that belong to one key.

{ blankBefore = True
, leading = [ " How long to wait." ]
, trailing = Just " seconds"
}

-->  <a blank line>
-->  # How long to wait.
-->  timeout = 30 # seconds

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

blankBefore is whether an empty line sits above the block. It is here rather than left to whoever is writing the file because there is no other way to ask for one -- a document has no lines to insert, only expressions -- and because a comment block hard against the line above it reads as a continuation of that line. It is always False at the very top of a document, where a file does not begin with a blank line, and for the first key directly under its [header], which belongs against the header rather than held off it.

comments : Array String -> Document -> Comments

The comments that belong to a key, by the convention above.

{ blankBefore = True
, leading = [ " how long to wait" ]
, trailing = Just " seconds"
}

This and setComments are inverses: what one reads, the other writes back unchanged. A key that is not in the document has no comments and reads as { blankBefore = False, leading = [], trailing = Nothing }.

setComments : Array String -> Comments -> Document -> Document

Set the comments that belong to a key, by the same convention comments reads them by.

Toml.Edit.setComments [ "timeout" ]
    { blankBefore = True
    , leading = [ " How long to wait." ]
    , trailing = Just " seconds"
    }

-->  <a blank line>
-->  # How long to wait.
-->  timeout = 30 # seconds

comments and this are inverses, so what one reads the other writes back unchanged -- including the blank line, which is why Comments has a field for it.

leading = [] takes the block above the key away and trailing = Nothing takes the comment off the line. Anything separated from the key by a blank line is not the key's and is left alone, which is the same blank line that protects it from remove. A key that is not there is left alone too.

blankBefore = True puts a blank line above the block if there is not one already, and False takes one away if there is. Asking for one does nothing at the top of the document, where there is no line above to be blank, and does nothing directly under a [header], where the key is the first of its table and a blank would hold it off the header it belongs to -- the same rule Toml.Encode writes by. Taking one away is the one thing here that reaches past the key's own lines, so it is worth knowing what it does: the blank line is what separates a floating comment from an owned one, and removing it makes the block above join this key's -- which is to say it will go when this key goes.

tableComments : Array String -> Document -> Comments

The comments that belong to a table's [header] line: the block directly above it and the comment beside it, by the same convention as a key's, and the same ones removeTable takes with the table.

Toml.Edit.tableComments [ "servers", "alpha" ]

-->  { blankBefore = True, leading = [ " The primary." ], trailing = Nothing }

For an array of tables the path means the first [[header]], which is the other way round from every other path in this module. A path into an array of tables names its last item when it names a value, because that is the item TOML means by it. A comment over a header explains the key -- the whole array -- and the first header is where Toml.Encode writes it. The other items are reached through the AST.

A table with no header line of its own -- one that only [a.b] implied, or one a dotted key built -- has no line for a comment to be on, and reads as { blankBefore = False, leading = [], trailing = Nothing }. So does a table that is not there.

setTableComments : Array String -> Comments -> Document -> Document

Set the comments that belong to a table's [header] line, by the same convention tableComments reads them by.

Toml.Edit.setTableComments [ "servers", "alpha" ]
    { blankBefore = True, leading = [ " The primary." ], trailing = Nothing }

-->  <a blank line>
-->  # The primary.
-->  [servers.alpha]

The two are inverses, as comments and setComments are, and for an array of tables the path means the first [[header]] for the reason given there.

This is how a program explains a section it created. introduce explains a key, and the header it brought with that key has nothing over it until this is called; calling it only when member said the first key was not there yet keeps the explanation to the run that invented the section, which is the same rule introduce keeps for a key.

One difference from setComments: blankBefore = True directly under another header is honoured. The first key of a table belongs against its header, but a nested header directly under its parent is two sections meeting, and the blank between them is the one Toml.Encode keeps. At the top of the file it is still a no-op.

A table with no header line, or none at all, is left alone.

Looking around

paths : Document -> Array (Array String)

Every key in the document, in the order the file writes them.

Table headers are not keys and are not here; the path of a key written under [server] begins with server all the same.

The type a value has

type alias Value = Value

A value on its way into a document, built by one of the functions at the bottom of this module.

The same type as Toml.Ast.Value, named here so that an edit needs one import rather than two. The constructors are in Toml.Ast, for reading a value apart; nothing here needs them, because a value is built by string, int and the rest.

Values to put in

Each of these builds the value and the text for it, since a value that was not read from a file has no text of its own yet. They are Toml.Literal's, offered here so that an edit needs one import rather than two, and shared with Toml.Encode so that the two cannot disagree about how to spell a float.

string : String -> Value

A string, written in the basic form: "..." on one line, with \n for a newline.

multilineString : String -> Value

A string, written in the multi-line form: """...""", with the newlines in it written as real line breaks rather than as \n.

This is the form to use for text a person will read: a note, a query, a block of help text. Every string can be written this way, so this never fails.

A value the file already means is not rewritten, even if it is spelled differently. See set. A key that already holds this text as a basic string keeps the basic string; the form is chosen when the key is first written. Use respell to change it.

literalString : String -> Maybe Value

A string, written in the literal form: '...', with no escapes at all.

Returns Nothing if the string contains an apostrophe, a newline, or a control character other than tab, because a literal string has no way to write those. It does not fall back to another form: the form a key is written in should not depend on what the value happens to contain.

multilineLiteralString : String -> Maybe Value

A string, written in the multi-line literal form: '''...''', with no escapes and with the newlines written as real line breaks.

Returns Nothing if the string cannot be written that way: three apostrophes in a row, an apostrophe at the very end, a lone carriage return, or a control character other than tab and newline.

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.

array : Array Value -> Value

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

Toml.Edit.set [ "ports" ]
    (Toml.Edit.array [ Toml.Edit.int 80, Toml.Edit.int 443 ])

An array already in the file keeps every character of itself for as long as nothing is set over it, newlines and comments inside the brackets included. This is the spelling for one that is being written fresh.

inlineTable : Array { key : String, value : Value } -> Value

An inline table, on one line: { a = 1, b = 2 }.

Toml.Edit.set [ "limits" ]
    (Toml.Edit.inlineTable
        [ { key = "soft", value = Toml.Edit.int 1024 }
        , { key = "hard", value = Toml.Edit.int 4096 }
        ]
    )

This is also how to change something inside an inline table, since a path does not reach in -- see what a path means. Build the table you want and set the whole of it.

There is no builder here for a [header] table, because a header is a line rather than a value and set puts values on lines. A key under a header that does not exist yet already brings one with it.