toml

Parse and serialize TOML, read and write TOML files, and work with nested tables by dotted path. The API mirrors json exactly.

Oxigen
introduce toml
FunctionSignatureReturns
parseparse(s)map
stringifystringify(val)str
readread(path)map
writewrite(path, val)None
get_inget_in(tbl, path)value or None
set_inset_in(tbl, path, val)the root map
del_indel_in(tbl, path)the root map
has_key_inhas_key_in(tbl, path)bool
set_whereset_where(arr, key, val, fields)array
mergemerge(a, b)map
deep_mergedeep_merge(a, b)map

Basic map operations are global builtins: has(m, key), keys(m), values(m), insert(m, key, val), remove(m, key).

#Type mapping

TOMLOxigen
stringString
integerInteger
floatFloat
booleanTrue / False
datetime / date / timeString
arrayArray
tableMap
array of tablesArray of Map

TOML has no null, so nothing maps to None. Dates come back as strings — pass one through datetime.parse if you need a timestamp.


#Parsing and serializing

#parse(s)

Parse a TOML document. Tables become maps.

Oxigen
introduce toml

t := toml.parse("title = \"MyApp\"\n[server]\nhost = \"localhost\"\nport = 8080\n")
println(t)
// {server: {host: localhost, port: 8080}, title: MyApp}

Note that top-level keys and [section] tables end up in the same map; the order you see is the parser's, not the file's.

Invalid TOML is an Error.

#stringify(val)

Serialize a map to a proper TOML document — [section] headers, [[array_of_tables]] blocks, and blank lines between sections.

Oxigen
config := {"title": "MyApp", "server": {"host": "localhost", "port": 8080}}
println(toml.stringify(config))
toml
title = "MyApp"

[server]
host = "localhost"
port = 8080

Scalars are emitted before tables, because TOML requires any bare key to come before the first [section] header.

#read(path)

Read and parse a TOML file.

Oxigen
settings := toml.read("Cargo.toml")
println(settings["package"]["name"])   // oxigen

A key that is not in the file reads back as None, and indexing None is an error — so reach for get_in when a section may be absent (a workspace Cargo.toml has no [package], for instance).

#write(path, val)

Serialize a map and write it to a file, replacing what was there.

Oxigen
toml.write("output.toml", {"title": "My App", "version": 1})
println(toml.read("output.toml"))   // {title: My App, version: 1}

#Nested access by dotted path

#get_in(tbl, path)

The value at a dotted path, or None when any segment is missing.

Oxigen
println(toml.get_in(t, "server.port"))   // 8080
println(toml.get_in(t, "server.nope"))   // None

A path addresses tables only — it does not descend into arrays. For an [[array of tables]], see set_where.

#set_in(tbl, path, val)

Set a value, creating intermediate tables as needed. Returns the root map.

Oxigen
config = toml.set_in(config, "server.ssl", True)
println(toml.get_in(config, "server.ssl"))   // True

Maps are mutable and shared. set_in modifies the map you passed and returns that same map. So do del_in, merge, deep_merge, and the insert/remove builtins. Copy first (toml.parse(toml.stringify(config))) if you need the original intact.

#del_in(tbl, path)

Remove a key at a dotted path. Returns the root map.

Oxigen
config = toml.del_in(config, "server.ssl")
println(toml.has_key_in(config, "server.ssl"))   // False

#has_key_in(tbl, path)

Oxigen
println(toml.has_key_in(config, "server.host"))   // True

#Arrays of tables

An [[array of tables]] parses to an array of maps, which a dotted path cannot descend into. set_where edits one by field value; the array module has the matching find_where / index_where / filter_where / remove_where.

#set_where(arr, key, val, fields)

Update the first table whose key equals val by deep-merging fields into it — or append a new table when there is none.

Oxigen
introduce toml

cfg := toml.read("products.toml")

cfg.products = toml.set_where(cfg.products, "name", "hammer", {"sku": 738594937})
cfg.products = toml.set_where(cfg.products, "name", "screw", {"sku": 3})

toml.write("products.toml", cfg)
toml
[[products]]
name = "hammer"
sku = 738594937

[[products]]
name = "nail"
sku = 2

[[products]]
name = "screw"
sku = 3

Fields not named survive the update, tables lacking the key are skipped rather than erroring, and only the first match is updated. It returns the array, edited in place — assigning the result back is the clearer form.


#Merging

#merge(a, b)

Shallow merge; keys in b win.

Oxigen
println(toml.merge({"a": 1}, {"a": 2, "b": 3}))   // {a: 2, b: 3}

#deep_merge(a, b)

Recursive merge; nested tables are merged rather than replaced.

Oxigen
a := {"server": {"host": "localhost", "port": 8080}}
b := {"server": {"port": 9090}}

println(toml.deep_merge(a, b))
// {server: {host: localhost, port: 9090}}

#Worked example

Oxigen
introduce toml
introduce strings

// Read a project manifest, bump the patch version, write it back.
fun bump(path <str>) {
    cfg := toml.read(path)
    parts := strings.split(toml.get_in(cfg, "package.version"), ".")
    next := "{parts[0]}.{parts[1]}.{int(parts[2]) + 1}"
    toml.write(path, toml.set_in(cfg, "package.version", next))
    next
}

println(bump("m.toml"))   // 0.1.4

Layered configuration, defaults last-write-wins:

Oxigen
introduce toml
introduce io

defaults := {"server": {"host": "127.0.0.1", "port": 8000}, "debug": False}

user := option {
    io.file_exists("app.toml") -> { toml.read("app.toml") }
    { {} }
}

config := toml.deep_merge(defaults, user)
println(toml.get_in(config, "server.port"))

See also: json — same API, different format; datetime for turning TOML date strings into timestamps.