std/toml

Toml reads and writes any Encode/Decode type as TOML 1.0 or 1.1, and TomlValue is the tree of a document, with date and time values that keep their nanoseconds and their offset.

The generated reference of std/toml has every public construct with its signature, its doc comment and its examples.

std/toml is TOML as a format of std/encoding, written in TorbScript. Toml reads a document into any Decode type and writes any Encode value, and TomlValue is the tree of a document, for one whose shape is not known ahead of time. The reader takes every released 1.x - 1.0 and 1.1 - and passes the public toml-test suite for both, which is vendored with its notice in std/toml/tests; whatever the reader accepts as valid, the writer writes back as an equal tree.

Import#

use Toml, TomlVersion, TomlError from "std/toml"
use TomlValue, TomlEntry from "std/toml"
use TomlOffsetDateTime, TomlLocalDateTime, TomlDate, TomlTime from "std/toml"
use Toml from "std/toml"

type Service {
  name: String
  replicas: Int = 1
  ports: List<Int> = []
}

const toml = Toml()
match toml.decode<Service>("name = \"web\"\nports = [80, 443]\n") {
  Ok(service) => {
    match toml.encode(service) {
      Ok(text) => print text
      Fail(problem) => print "Not writable: {problem}"
    }
  }
  Fail(problem) => print "Rejected: {problem}"
}

replicas is not in the document, so it takes its default.

Declarations#

Toml#

type Toml {
  naming: Naming = Naming.Unchanged
  strict: Bool = false
  version: TomlVersion = TomlVersion.Toml11
  fn encode<Value: Encode>(value: Value): Result<String, TomlError>
  fn decode<Value: Decode>(text: String): Result<Value, TomlError>
  fn parse(text: String): Result<TomlValue, TomlError>
  fn value<Value: Encode>(value: Value): Result<TomlValue, TomlError>
  fn write(tree: TomlValue): String
}

A Toml is the format with its options. naming spells every field name and strict makes a key of the document that no type asked for an error, as they do for std/json and std/yaml. Toml has no Format implementation and no framer: a TOML document is one table that is read whole, not a stream of items.

A document is a table. encode accepts a record or a map and refuses any other value with a TomlError, since a number or a list is not a TOML document. TOML has no null: a field that holds None is left out, a key the document does not have reads as None or as the field's default, and None inside a list is an error. A number beyond 64 bits is an error as well, and bytes are written as a string of base64. A record that announces no field is its one value, a case without fields is its name and a case with fields a table of one entry, its name.

decode reads one document. A failure is a TomlError: for text that is not TOML it has the line and the column, and for a document that does not fit the type it has the chain of fields it went wrong in.

line 3, column 9: a value was expected here, and the line ends
ports[1]: a whole number is needed, not "http"

parse reads the tree, write writes a tree back, and value is the tree an Encode value is written as. A document is rejected where it defines a key or a table twice, extends a table that an inline table or an earlier section closed, or gives an array of tables the name of a table.

TomlVersion#

type TomlVersion {
  case Toml10
  case Toml11
}

version chooses what the writer may use, and nothing else: the reader accepts what is valid in either version. The two differ in how a control character of a string is written, \uHHHH for Toml10 and \e or \xHH for Toml11, the default. A document written for Toml10 can be read by every TOML 1.x reader; one for Toml11 needs a reader of 1.1.

The reader accepts what 1.1 adds in every document: an inline table over several lines and with a trailing comma, a time without seconds, and the escapes \e and \xHH. A byte order mark in front of the document is skipped.

TomlValue#

type TomlValue with Show, Equals {
  case String(value: String)
  case Integer(value: Int)
  case Float(value: Float64)
  case Boolean(value: Bool)
  case OffsetDateTime(value: TomlOffsetDateTime)
  case LocalDateTime(value: TomlLocalDateTime)
  case LocalDate(value: TomlDate)
  case LocalTime(value: TomlTime)
  case Array(items: List<Self>)
  case Table(entries: List<TomlEntry>)
  fn at(key: String): Self?
  fn items(): List<Self>
  fn entries(): List<TomlEntry>
  fn isTable(): Bool
  fn updated(key: String, value: Self): Self
  fn equals(other: Self): Bool
  fn show(): String
}
type TomlEntry with Show, Equals {
  key: String
  value: TomlValue
  fn show(): String
}

The tree of a document: what it says, in the order it says it. A table keeps its keys in the order of the document, and the tree does not keep comments, the way a table was split into sections, or the kind of quotes a string used. equals ignores the order of the keys of a table - TOML does not give it a meaning - and counts nan as equal to nan, so a document and what the writer wrote of it are equal.

at(key) is the value of a key of a table, items() and entries() are the elements of an array and the entries of a table (empty for any other value), and updated(key, value) is a table with one value set, kept in its place or added at the end.

use Toml, TomlValue from "std/toml"

const toml = Toml()
match toml.parse("[server]\nhost = \"example.org\"\nport = 8080\n") {
  Ok(tree) => {
    const server = tree.at("server") ?? TomlValue.Table([])
    print server.at("port")
    print toml.write(tree.updated("title", TomlValue.String("demo")))
  }
  Fail(problem) => print "Rejected: {problem}"
}

Dates and times#

type TomlDate with Show, Equals, Compare {
  year: Int
  month: Int
  day: Int
  static fn parse(text: String): Result<Self, DecodeError>
  fn daysSinceEpoch(): Int
  fn compare(other: Self): Ordering
  fn show(): String
}
extend TomlDate with Encode, Decode
type TomlTime with Show, Equals, Compare {
  hour: Int
  minute: Int
  second: Int = 0
  nanosecond: Int = 0
  static fn parse(text: String): Result<Self, DecodeError>
  fn nanosecondsOfDay(): Int
  fn compare(other: Self): Ordering
  fn show(): String
}
extend TomlTime with Encode, Decode
type TomlLocalDateTime with Show, Equals, Compare {
  date: TomlDate
  time: TomlTime
  static fn parse(text: String): Result<Self, DecodeError>
  fn compare(other: Self): Ordering
  fn show(): String
}
extend TomlLocalDateTime with Encode, Decode
type TomlOffsetDateTime with Show, Equals {
  dateTime: TomlLocalDateTime
  offsetMinutes: Int = 0
  static fn parse(text: String): Result<Self, DecodeError>
  fn toTimestamp(): Timestamp?
  fn show(): String
}
extend TomlOffsetDateTime with Encode, Decode

TOML has four values for a point in time, and each is a type of its own so a field says which one it wants: a date (1979-05-27), a time (07:32:00.5), a date and time without an offset (1979-05-27T07:32:00) and a date and time with one (1979-05-27T07:32:00Z or 1979-05-27T00:32:00-07:00). They keep nanoseconds and the offset in minutes, and show() is their RFC 3339 text. parse reads that text, with a space instead of the T too. A date that is not on the calendar, such as February 30, is an error.

TomlOffsetDateTime.toTimestamp() is the point in time as a Timestamp of std/time, and None for one beyond what a Timestamp holds.

In a document the four are TOML's own date and time values, not strings. They encode and decode as wrappers named by their type, so a format that has no such value, JSON for one, writes the RFC 3339 text. A String field reads the text of a date or time value as it was written.

use Toml, TomlOffsetDateTime from "std/toml"

type Release {
  name: String
  at: TomlOffsetDateTime
}

match Toml().decode<Release>("name = \"1.0\"\nat = 1979-05-27T00:32:00.5-07:00\n") {
  Ok(release) => {
    print release.at.show()
    print release.at.toTimestamp()
  }
  Fail(problem) => print "Rejected: {problem}"
}

TomlError#

type TomlError with Show, Error {
  static fn syntax(line: Int, column: Int, message: String): Self
  static fn decodeFailed(cause: DecodeError, line: Int = 0): Self
  static fn unwritable(message: String): Self
  fn isSyntaxError(): Bool
  fn line(): Int
  fn column(): Int
  fn message(): String
  fn show(): String
  fn cause(): Error?
}

What went wrong: text that is not TOML (syntax, with its line and column counted from 1), a document that does not fit the type (decodeFailed, with the DecodeError that names the field chain) or a value that TOML cannot say (unwritable). line() and column() are 0 where the problem belongs to none.

TomlEncoder#

type TomlEncoder with Encoder {
  naming: Naming = Naming.Unchanged
  fn result(): Result<TomlValue, TomlError>
  var fn nothing()
  var fn bool(value: Bool)
  var fn int(value: Int64)
  var fn unsigned(value: UInt64)
  var fn float(value: Float64)
  var fn decimal(value: Decimal)
  var fn string(value: String)
  var fn bytes(value: Bytes)
  var fn sequence(length: Int?)
  var fn map(length: Int?)
  var fn record(typeName: String)
  var fn variant(typeName: String, name: String)
  var fn field(name: String)
  var fn finish()
}

TomlEncoder is the Encoder behind encode and value, exported for a format that builds on it. A value is encoded into it, and result() is the tree, or the first thing that TOML cannot say.

Esc