--- title: "Configuration files in TOML" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Configuration files in TOML} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") ``` TOML is the configuration format of Cargo, Python packaging (`pyproject.toml`), Hugo and many tools. zutoml reads it into ordinary R values and writes R values back as TOML. ```{r} library(zutoml) ``` ## Reading a `pyproject.toml` ```{r} pyproject <- ' [project] name = "spam-eggs" version = "2020.0.0" dependencies = ["httpx", "gidgethub[httpx]>4.0.0"] requires-python = ">= 3.8" authors = [ {name = "Pradyun Gedam", email = "pradyun@example.com"}, {name = "Tzu-Ping Chung", email = "tzu-ping@example.com"}, ] [project.optional-dependencies] cli = ["rich", "click"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" ' p <- toml_parse(pyproject) p$project$name p$project$dependencies p$`build-system`$requires ``` Tables become named lists, with their keys in the order the file gives them. An array becomes a vector when its elements share a type. A one-element array, such as `requires` above, is marked with `I()` so that writing it back gives an array again. To read only part of a document, give a path. The whole file is still checked, but only that part becomes R values: ```{r} toml_parse(pyproject, select = "project.optional-dependencies.cli") toml_parse(pyproject, select = "project.authors[2].email") ``` An array of tables, like `authors`, is a list of named lists, or a data frame on request: ```{r} toml_parse(pyproject, data_frame = TRUE)$project$authors ``` ## Types come from the document TOML types every value by its syntax, so zutoml never has to guess: ```{r} x <- toml_parse(' released = 1979-05-27 built = 1979-05-27T07:32:00Z local = 1979-05-27T07:32:00 alarm = 07:30:00 quoted = "1979-05-27" ') str(x) ``` `released` is a `Date`, `built` a `POSIXct` in UTC, `local` a `POSIXct` in your session's time zone, and `quoted` stays a string. Use `datetimes = "keep"` to get date-times as text, and `local_time = "difftime"` for times of day as seconds since midnight. Integers beyond 2^53, which a double cannot hold exactly, are kept as exact decimal text: ```{r} toml_parse("id = 9223372036854775807")$id ``` ## Reading `Cargo.toml` from a file ```{r} cargo <- tempfile(fileext = ".toml") writeLines(c( "[package]", 'name = "hello"', 'version = "0.1.0"', 'edition = "2021"', "", "[dependencies]", 'serde = { version = "1.0", features = ["derive"] }', 'rand = "0.8"' ), cargo) deps <- toml_read(cargo)$dependencies deps$serde$features ``` ## Writing a configuration `toml_emit()` writes a named list as TOML text, and `toml_write()` writes it to a file: ```{r} config <- list( title = "My service", port = 8080, debug = FALSE, started = as.Date("2026-10-08"), database = list(host = "localhost", ports = c(5432L, 5433L)), servers = list( list(name = "alpha", ip = "10.0.0.1"), list(name = "beta", ip = "10.0.0.2") ) ) cat(toml_emit(config)) ``` `port = 8080` is a double in R, since R has no integer literal, but it is written as the integer a TOML reader expects. A list of named lists becomes an array of tables. The output is the same on every platform, and reads back: ```{r} identical(toml_parse(toml_emit(config))$servers, config$servers) ``` Markers say how one value is written. A `Cargo.toml` dependency, for example, is usually an inline table: ```{r} cat(toml_emit(list(dependencies = list( serde = toml_inline(list(version = "1.0", features = "derive")), rand = "0.8" )))) ``` `toml_literal()` writes strings as literal strings, which need no escapes (Windows paths, regular expressions), and `toml_multiline()` as multi-line strings. ## Pointing at the value at fault With `positions = TRUE`, the result records where each key is, so a check on a configuration can say where the problem is: ```{r} cfg <- toml_parse("[server]\nhost = 'localhost'\nport = 99999\n", positions = TRUE) pos <- attr(cfg, "toml_positions") pos if (cfg$server$port > 65535) { at <- pos[pos$path == "server.port", ] message("server.port at line ", at$line, ", column ", at$column, " is above 65535") } ``` ## When things go wrong Every error has a class, and parse errors say where: ```{r} err <- tryCatch( toml_parse("[server]\nport = 80\nport = 8080\n"), zutoml_parse_error = function(e) e ) conditionMessage(err) c(err$line, err$column) err$kind ``` `toml_validate()` checks a document without building it, and untrusted input is bounded by `max_size`, `max_depth`, `max_items` and `max_string`. See `?zutoml-conditions` for every class.