Current section
Files
Jump to
Current section
Files
README.md
# tomlet
[](https://hex.pm/packages/tomlet)
[](https://hexdocs.pm/tomlet/)
A round-tripping TOML parser and writer for Gleam.
Tomlet parses TOML 1.1.0 into an opaque document that retains comments, key
order, and surrounding trivia. Unedited documents round-trip to their original
text; edited values are written back while preserving nearby comments and
document structure. `Document` stays opaque so Tomlet can evolve its internal
syntax tree without breaking the public API. Inspired by Rust's
[`toml_edit`](https://docs.rs/toml_edit) and Python's
[`tomlkit`](https://github.com/python-poetry/tomlkit).
```sh
gleam add tomlet
```
```gleam
import tomlet
pub fn main() {
let source =
"
# the user's favorite snack
snack = \"tomato\" # raw, with salt
"
case tomlet.parse(source) {
Ok(doc) ->
case tomlet.set_string(doc, ["snack"], "tomato sandwich") {
Ok(updated) -> tomlet.to_string(updated)
// -> "
// # the user's favorite snack
// snack = \"tomato sandwich\" # raw, with salt
// "
Error(error) -> handle_edit_error(error)
}
Error(error) -> handle_parse_error(error)
}
}
```
The `handle_*` functions above are placeholders for your own error handling;
each `case` surfaces the `ParseError`, `GetError`, or `EditError` that tomlet
returns instead of crashing.
## Parsing and typed access
Use `tomlet.parse` for `String` input, or `tomlet.parse_bytes` when raw bytes
need TOML-compliant UTF-8 and BOM validation before parsing.
```gleam
case tomlet.parse_bytes(<<"answer = 42\n":utf8>>) {
Ok(doc) ->
case tomlet.get_int(doc, ["answer"]) {
Ok(answer) -> answer
Error(error) -> handle_get_error(error)
}
Error(error) -> handle_parse_error(error)
}
```
Typed accessors include `get_string`, `get_int`, `get_bool`, `get_float`,
`get_date`, `get_time`, and `get_datetime`. Use `get` when you need to inspect
any TOML value through the public `tomlet.Value` type, including dates, times,
arrays, inline tables, standard tables (`StandardTableValue`), and arrays of
tables.
```gleam
case tomlet.parse("released = 2026-05-25\n") {
Ok(doc) ->
case tomlet.get(doc, ["released"]) {
Ok(tomlet.DateValue(date)) -> tomlet.date_to_string(date)
// -> "2026-05-25"
_ -> handle_unexpected_value()
}
Error(error) -> handle_parse_error(error)
}
```
Inline table values are addressed with the same key path syntax:
```gleam
case tomlet.parse("pkg = { name = \"tomato\" }\n") {
Ok(doc) ->
case tomlet.get_string(doc, ["pkg", "name"]) {
Ok(name) -> name
Error(error) -> handle_get_error(error)
}
Error(error) -> handle_parse_error(error)
}
```
Typed accessor mismatches return `WrongType(path, expected)`, where `expected`
is a stable `ExpectedType` variant such as `ExpectedString`, `ExpectedInt`, or
`ExpectedDateTime`.
Parse errors use stable variants for machine handling. `InvalidSyntax` and
`DuplicateKey` carry byte offsets; use `tomlet.line_column(input, offset)` when
displaying diagnostics to users.
## Dynamic decoding
Use `parse_dynamic` or `decode` when you want to reuse
`gleam/dynamic/decode` decoders instead of reading values manually. Tables and
inline tables become property maps, arrays and arrays of tables become lists,
date/time values become strings containing their TOML lexical form, and special
floats become `"inf"`, `"-inf"`, or `"nan"`.
```gleam
import gleam/dynamic/decode
import tomlet
let package_decoder = {
use name <- decode.field("name", decode.string)
use released <- decode.field("released", tomlet.datetime_decoder())
decode.success(#(name, released))
}
case tomlet.decode(
"name = \"tomlet\"\nreleased = 1979-05-27T07:32:00Z\n",
package_decoder,
) {
Ok(#(name, released)) -> #(
name,
tomlet.datetime_to_string(released),
)
Error(error) -> handle_decode_error(error)
}
```
`date_decoder`, `time_decoder`, and `datetime_decoder` decode those lexical
strings back into Tomlet's opaque date/time types.
## TOML version
`tomlet.parse` and `tomlet.parse_bytes` accept TOML 1.1 by default. The 1.1
features tomlet supports beyond 1.0 are:
- newlines and trailing commas inside inline tables,
- `\xHH` hex and `\e` escape sequences in basic strings and keys,
- optional seconds in local and offset times and date-times (for example
`07:32` and `1979-05-27T07:32Z`).
To parse strictly against TOML 1.0 and reject this 1.1-only syntax, pass the
version directly:
```gleam
let assert Error(_) = tomlet.parse_with("t = 07:32\n", tomlet.Toml10)
```
`parse_with` and `parse_bytes_with` take a `TomlVersion` (`Toml10` or `Toml11`).
## Checked edits
Edit operations return `Result(Document, EditError)` so applications can tell
successful edits from invalid paths, key conflicts, missing keys, and unsafe
comment text. Comment insertion rejects TOML-forbidden comment control
characters. New keys are emitted as bare TOML keys when possible and quoted when
needed. Current edit helpers include `set_string`, `set_int`, `set_bool`,
`set_float`, `set_date`, `set_time`, `set_datetime`, `set_array`,
`set_inline_table`, `append_array_of_tables`, `remove`, and
`insert_comment_before`. Construct typed date, time, and date-time values with
`date_from_string`, `time_from_string`, and `datetime_from_string`.
`set_array` leaves the document unchanged when you supply the same semantic
values. For changed arrays, it keeps the source text and attached comments of
unchanged items. It matches duplicate values one occurrence at a time in their
original order, then writes items in your requested order. Same-line comments
belong to the preceding item. Separate-line comments before an item belong to
that item. If you remove or replace an item, `set_array` removes its attached
comments. It keeps standalone comments before the closing bracket, even if you
remove all items. New arrays use flow style (`[a, b, c]`).
Use the `set_*` helpers to replace or insert values inside inline tables.
For example, `set_array(doc, ["tools", "licence_audit", "deny"], [])` changes
`licence_audit = { allow = ["MIT"] }` under `[tools]` to
`licence_audit = { allow = ["MIT"], deny = [] }`.
Missing intermediate parents use dotted keys inside the nearest existing inline
table. An edit keeps the table inline and appends new entries after existing
ones. Inline tables with comments keep their source layout; other inline
tables use default spacing. A scalar or array that blocks the path, or an
existing dotted key below the requested path, causes `KeyConflict`.
The `set_*` helpers also return `KeyConflict` for paths through arrays of
tables, since those paths do not select a record to edit.
Structural read values that cannot be represented in a write context are
rejected explicitly. For example, passing `StandardTableValue` or
`ArrayOfTablesValue` to `set_array`, `set_inline_table`, or
`append_array_of_tables` returns `Error(InvalidValue)` instead of silently
flattening the table shape.
## Examples
Read typed values:
```gleam
let input =
"title = \"Tomlet\"\n"
<> "version = 1\n"
<> "enabled = true\n"
<> "ratio = 3.14\n"
case tomlet.parse(input) {
Ok(doc) ->
case
tomlet.get_string(doc, ["title"]),
tomlet.get_int(doc, ["version"]),
tomlet.get_bool(doc, ["enabled"]),
tomlet.get_float(doc, ["ratio"])
{
Ok(title), Ok(version), Ok(enabled), Ok(ratio) ->
#(title, version, enabled, ratio)
_, _, _, _ -> handle_get_error()
}
Error(error) -> handle_parse_error(error)
}
```
Edit a document and write it back:
```gleam
let input =
"# package metadata\n"
<> "name = \"tomlet\"\n"
<> "version = 0\n"
<> "draft = true\n"
case tomlet.parse(input) {
Ok(doc) ->
case tomlet.set_int(doc, ["version"], 1) {
Ok(doc) ->
case tomlet.remove(doc, ["draft"]) {
Ok(doc) ->
case
tomlet.insert_comment_before(
doc,
["version"],
"first stable release",
)
{
Ok(doc) -> tomlet.to_string(doc)
// -> "
// # package metadata
// name = \"tomlet\"
// # first stable release
// version = 1
// "
Error(error) -> handle_edit_error(error)
}
Error(error) -> handle_edit_error(error)
}
Error(error) -> handle_edit_error(error)
}
Error(error) -> handle_parse_error(error)
}
```
Start from an empty document:
```gleam
let doc = tomlet.new()
case tomlet.set_string(doc, ["package", "name"], "tomlet") {
Ok(doc) ->
case tomlet.set_int(doc, ["package", "version"], 1) {
Ok(doc) -> tomlet.to_string(doc)
// -> "
// [package]
// name = \"tomlet\"
// version = 1
// "
Error(error) -> handle_edit_error(error)
}
Error(error) -> handle_edit_error(error)
}
```
Report parse errors with line and column information:
```gleam
let input = "name = \n"
case tomlet.parse(input) {
Ok(_) -> Nil
Error(tomlet.InvalidSyntax(_, offset)) -> {
let position = tomlet.line_column(input, offset)
let line = tomlet.position_line(position)
let column = tomlet.position_column(position)
// Show line and column in your application's diagnostic.
}
Error(tomlet.DuplicateKey(_, offset)) -> {
let position = tomlet.line_column(input, offset)
let line = tomlet.position_line(position)
let column = tomlet.position_column(position)
// Show line and column in your application's diagnostic.
}
Error(tomlet.InvalidEncoding) -> Nil
}
```
## Public API
The semver-stable public API is the top-level `tomlet` module. Other
`tomlet/*` modules are internal implementation details and may change without
notice.
### API stability policy
- The top-level `tomlet` module is the only supported API surface.
- Modules listed in `gleam.toml` under `internal_modules` are not public API.
- Public variant types are stable and matchable. Adding, removing, or renaming
variants is treated as a breaking change.
- The project follows Semantic Versioning; breaking API changes are released as
major versions.
## Contributing
See [`DEV.md`](./DEV.md) for contributor setup, test commands, changelog
guidelines, and release-readiness checks.
## License
Licensed under either of
- Apache License, Version 2.0 ([`LICENSE-APACHE`](./LICENSE-APACHE) or
<https://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([`LICENSE-MIT`](./LICENSE-MIT) or
<https://opensource.org/licenses/MIT>)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Tomlet by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.