---
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.
