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.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

Die generierte Referenz von std/toml zeigt jede öffentliche Deklaration mit Signatur, Doc-Kommentar und Beispielen.

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