Files
interpres/docs/API.md
T
2026-09-17 21:20:47 +02:00

18 KiB

API

The library exports the surface below from the sourcedock.dev/petrbalvin/interpres package. The snippets assume:

import "sourcedock.dev/petrbalvin/interpres"

Functions

func Parse(data []byte) (map[string]any, error)

Decodes a TOML document into an untyped tree, using the value mapping in the Decoding section below. Returns *SyntaxError on a malformed document. Input that is not valid UTF-8 is rejected before the parser runs. Equivalent to ParseContext(context.Background(), data).

tree, err := interpres.Parse([]byte("title = \"x\"\nport = 8080\n"))

func ParseContext(ctx context.Context, data []byte) (map[string]any, error)

The cancellable variant of Parse. An already-cancelled context returns ctx.Err() before any work. During parsing the context is checked every 64 top-level statements, so a long document aborts without running to completion.

func Unmarshal(data []byte, v any) error

Parses data and stores the result in the value pointed to by v, typically a pointer to a struct or to map[string]any. Equivalent to UnmarshalContext(context.Background(), data, v).

var cfg Config
if err := interpres.Unmarshal(data, &cfg); err != nil {
	return err
}

func UnmarshalContext(ctx context.Context, data []byte, v any) error

The cancellable variant of Unmarshal.

func Marshal(v any) ([]byte, error)

Encodes a struct or map[string]V value, or a non-nil pointer to one, into a TOML 1.0 document. The emission rules are in the Encoding section below. Equivalent to MarshalContext(context.Background(), v).

out, err := interpres.Marshal(cfg)

func MarshalContext(ctx context.Context, v any) ([]byte, error)

The cancellable variant of Marshal. The context is checked before any work and every 64 fields during the reflection walk.

Decoding

Value mapping

Parse and Unmarshal map TOML values to Go types as follows:

TOML value Go type in the parsed tree
string string
integer int64
float float64
boolean bool
offset date-time time.Time
local date-time LocalDateTime
local date LocalDate
local time LocalTime
array []any
table, inline table map[string]any
array of tables []map[string]any

When decoding into a struct, these values convert onto the destination's concrete types: any integer or unsigned width, floats, slices, nested structs and map[string]T.

Target constraints

Unmarshal and (*Decoder).Decode write into a non-nil pointer:

  • *struct, matched per the field rules below
  • *map[string]any or *map[string]T, keys become map keys and values decode into T recursively
  • *any, receives the whole parsed tree unchanged

Anything else returns interpres: decode target must be a non-nil pointer.

Field matching

For a struct destination, a TOML key matches a field as follows:

  1. The toml:"name" tag, using the part before any comma. The literal - excludes the field.
  2. Without a tag, the lower-cased field name.
  3. An anonymous (embedded) field without a tag is inlined: the decoder walks into the embedded struct and matches its own fields against the same keys, mirroring how the encoder flattens it. A nil embedded pointer struct is allocated on demand. An untagged embedded map receives the keys no field claims.
  4. The key itself is lower-cased before lookup, so the match is case-insensitive on both sides: DATABASEURL matches a field named DatabaseUrl.

The match is exact after lower-casing. No separator is inserted, so a TOML key database_url does not match a field named DatabaseUrl; tag such a field (toml:"database_url") or use the lower-cased name as the key. When two fields resolve to the same name, the shallower one wins; at equal depth, the one declared later wins.

Unknown keys are ignored by default, landing in an untagged embedded map when the struct has one; Strict decoding rejects them instead.

Numeric conversion

The parser produces int64 for every integer and float64 for every float. The decoder converts to the destination type with explicit overflow checks:

Destination kind Rule
int, int8, int16, int32, int64 the int64 value must not overflow the destination
uint, uint8, uint16, uint32, uint64 the value must be non-negative; uint8, uint16 and uint32 enforce their own maxima; uint64 accepts any non-negative int64
float32, float64 copied verbatim; an integer also coerces, so TOML 5 decodes into 5.0
bool, string exact kind match only, no coercion across kinds
time.Time offset date-times only; no implicit conversion to or from the local variants

A conversion that the rules do not allow produces an error wrapped with the offending key or index, for example p: interpres: integer 300 overflows uint8.

Date-time values

Offset date-times decode into time.Time and keep their offset. The local variants decode into LocalDateTime, LocalDate and LocalTime, whose embedded time.Time is normalised to UTC (midnight UTC for a local date, the zero date for a local time). There is no implicit conversion between the offset and local kinds; assigning one to the other is an error.

Arrays of tables

A [[a]] block parses into a []map[string]any element of the tree. When the destination is a slice, each element decodes into the slice's element type ([]struct or []map[string]V); a mismatch on one element surfaces as an error wrapped with [i]: and the element index.

Custom decoding: Unmarshaler

A type that wants full control of its decode implements:

type Unmarshaler interface {
	UnmarshalTOML(data any) error
}

data is whatever the parser produced for that key: string, bool, int64, float64, time.Time, LocalDateTime, LocalDate, LocalTime, []any, or map[string]any. The method inspects the value and mutates its own receiver; the decoder keeps whatever state the receiver stored.

The method is usually on a pointer receiver (*T). The decoder invokes it when the destination type or its pointer implements the interface, so a pointer-receiver implementation on an addressable struct field is found automatically, and a nil pointer destination is allocated first. An error returned from UnmarshalTOML halts the decode and propagates wrapped with the key path, for example addr: unmarshal: not a string.

Strict decoding

By default unknown keys are dropped silently. A Decoder built with DisallowUnknownFields rejects them instead:

err := interpres.NewDecoder().
	DisallowUnknownFields().
	Decode(data, &cfg)

A typo such as database_urls then fails with interpres: unknown field "database_urls" for main.Config instead of a silent default-zero run. Strictness applies to every struct the decode reaches, at any depth, including struct elements inside slices; map destinations accept every key by nature.

Cancellation

ParseContext, UnmarshalContext and (*Decoder).DecodeContext accept a context.Context. An already-cancelled context short-circuits with context.Canceled before any work begins; afterwards the context is checked every 64 top-level statements.

Flow

sequenceDiagram
    participant Caller
    participant Unmarshal as Unmarshal
    participant Parser as parser
    participant Decoder as decoder
    Caller->>Unmarshal: data, v
    Unmarshal->>Parser: ParseContext(ctx, data)
    Parser-->>Unmarshal: tree or *SyntaxError
    Unmarshal->>Decoder: decode(tree, reflect value)
    Decoder-->>Unmarshal: nil or wrapped field error
    Unmarshal-->>Caller: error

Encoding

Input constraints

Marshal and (*Encoder).Marshal accept a struct, a map[string]V, or a non-nil pointer to one, where V is any value Marshal itself understands. A different top-level value fails:

Input Error
a bare scalar or array interpres: top-level value must be a struct or map[string]V, got <type>
a nil any interpres: cannot marshal nil value
a nil pointer interpres: cannot marshal nil pointer

Field matching

Struct fields become TOML keys as follows:

  1. The toml:"name" tag, using the part before any comma. The literal - skips the field.
  2. Without a tag, the lower-cased field name. The key emitted for a field named DatabaseUrl is databaseurl; tag the field to emit database_url.
  3. An anonymous (embedded) field without a tag is inlined into the parent table; with a tag it is a regular field under that name.

Keys that match [A-Za-z0-9_-]+ are emitted bare, all others quoted. A map[string]V emits its keys in sorted order for deterministic output, and a nil map emits nothing.

Tag options

The part of a toml tag after the first comma carries options. Both options shape emission only; the decoder ignores them.

  • omitzero skips the field when its value is the zero value of its type. A type with an IsZero() bool method (time.Time among them) decides through that method, so a zero time.Time or an all-zero struct disappears from the output.
  • omitempty skips the field when it holds an empty collection: a nil or empty slice or array, or a nil or empty map. Strings and other scalars are not covered by omitempty; use omitzero for those.
type Config struct {
	Host    string    `toml:"host,omitzero"`
	Started time.Time `toml:"started,omitzero"`
	Tags    []string  `toml:"tags,omitempty"`
}

Options combine after the name: toml:"name,omitempty,omitzero" is valid, and an unknown option is ignored.

Untagged embedded fields round-trip: the decoder inlines embedded structs and routes unclaimed keys into an embedded map exactly where the encoder flattened them.

Group-by-kind layout

By default every table is emitted with its entries grouped by kind:

  1. scalars (string, int64, float64, bool, time.Time, LocalDateTime, LocalDate, LocalTime)
  2. sub-tables (structs and map[string]V values)
  3. arrays of tables ([]struct and []map[string]V)

Within each group the order follows struct field declaration order, or sorted key order for maps. This is the only layout that reliably re-parses to the same tree: once a [header] is written, later scalars at the parent level would be parsed as keys of the sub-table.

Preserving declaration order

GroupByKind(false) on an Encoder walks the entries in declaration order instead, emitting each header immediately before its content:

out, err := interpres.NewEncoder().GroupByKind(false).Marshal(cfg)

The output remains parseable, but a scalar declared after a sub-table lands under that sub-table's header when the document is read back. Use this layout for presentation only, not when the output must round-trip.

Custom encoding: Marshaler

A type that wants a non-default TOML shape implements:

type Marshaler interface {
	MarshalTOML() (any, error)
}

The returned value is encoded as if it had been passed in place of the receiver, so it may be a scalar, a slice, an array of tables, or another struct or map, including the Marshaler result of another type; the encoder recurses. An error returned from MarshalTOML fails the marshal wrapped with the key path, for example interpres: server.port: bad timestamp.

type Port int

func (p Port) MarshalTOML() (any, error) {
	return int64(p), nil
}

Arrays

An array whose every element is a table ([]struct, []map[string]V, after pointer dereference) emits as an array of tables. TOML also lets one array mix tables with scalars; such an array emits as a plain value array, with the table elements rendered as inline tables:

tree, _ := interpres.Parse([]byte(`arr = [1, {a = 2}, "x"]`))
out, _ := interpres.Marshal(tree) // arr = [1, {a = 2}, "x"]

Empty arrays

A nil slice is always omitted. An empty (length 0) array of tables is always omitted, because TOML forbids an empty [[a]]. Other empty arrays emit as key = [] by default; OmitEmptyArrays() skips them as well, so []string{} is treated like a nil slice.

Long strings

By default every string is emitted as a basic "..." string with the escapes TOML requires, and a string containing a newline is emitted as an escaped multi-line basic string. UseLiteralMultiline(threshold) switches strings that contain a newline and are at least threshold bytes long to the literal '''...''' form, which carries the newlines verbatim:

out, err := interpres.NewEncoder().UseLiteralMultiline(80).Marshal(cfg)

Single-line strings keep the basic form regardless of the threshold, and a threshold of 0 or less disables the option.

Cancellation

MarshalContext and (*Encoder).MarshalContext accept a context.Context. The context is checked before any work and every 64 fields during the reflection walk.

What is not preserved

The output is not byte-identical to any document that produced the value:

  • comments are dropped, and whitespace inside expressions is normalised
  • map keys are emitted in sorted order
  • the choice between [table] headers and inline tables is not preserved
  • strings use the basic quoted form unless the literal option above applies
  • floats always carry a . or an exponent, so a float 1 is emitted as 1.0 and stays distinguishable from the integer 1 across a round-trip; negative zero is normalised to 0.0

The output is guaranteed to re-parse through Parse into an equivalent value tree. Marshal cannot encode cyclic data structures.

Flow

sequenceDiagram
    participant Caller
    participant Marshal as Marshal
    participant Walk as reflection walk
    participant Emit as emitter
    Caller->>Marshal: v any
    Marshal->>Walk: build tomlDoc from struct or map
    Walk-->>Marshal: tomlDoc or wrapped error
    Marshal->>Emit: emitDoc(doc)
    Emit-->>Marshal: bytes or error
    Marshal-->>Caller: bytes, error

Types

type SyntaxError struct{ Line int; Msg string }

Describes a malformed TOML document; Line is 1-based and Error() renders as interpres: line N: msg. Read the structured fields with a type assertion or errors.AsType:

if se, ok := errors.AsType[*interpres.SyntaxError](err); ok {
	fmt.Println(se.Line, se.Msg)
}

type DecodeError struct{ Path []string; Err error }

Wraps a decoding failure with the key path at which it happened. Path lists one segment per level from the document root, the outermost key first: a key contributes its name, an array element its bracketed index, so the path of the weight field in the first item reads ["items", "[0]", "weight"]. The rendered message is unchanged by the type; read the fields instead of parsing the message:

if de, ok := errors.AsType[*interpres.DecodeError](err); ok {
	fmt.Println(de.Path, de.Err)
}

type EncodeError struct{ Path string; Err error }

Wraps an encoding failure with the key path of the value that failed, in the document's own notation: server.ports[2]. Read it with errors.AsType the same way.

type Decoder

Configurable strictness for decoding, constructed with NewDecoder. Set up with DisallowUnknownFields, then call Decode or DecodeContext any number of times. A configured Decoder holds no per-call state and is safe for concurrent use.

type Encoder

Configurable emission policy, constructed with NewEncoder. The option state is private; set it with the chainable methods, each of which returns the encoder:

Method Default Effect
GroupByKind(v bool) true group entries as scalars, then sub-tables, then arrays of tables; false preserves declaration order
OmitEmptyArrays() off skip key = [] for empty scalar arrays
UseLiteralMultiline(threshold int) 0 emit multi-line strings of at least threshold bytes as literal '''...'''
out, err := interpres.NewEncoder().
	GroupByKind(false).
	OmitEmptyArrays().
	UseLiteralMultiline(80).
	MarshalContext(ctx, cfg)

A configured Encoder holds no per-call state; each Marshal or MarshalContext call copies the options and is safe for concurrent use, as long as no setter races with a call.

type Marshaler interface{ MarshalTOML() (any, error) }

See Custom encoding.

type Unmarshaler interface{ UnmarshalTOML(data any) error }

See Custom decoding.

Date-time wrappers

type LocalDateTime struct{ time.Time } // 1979-05-27T07:32:00
type LocalDate     struct{ time.Time } // 1979-05-27
type LocalTime     struct{ time.Time } // 07:32:00.999999

Each carries a String() method returning the TOML-canonical rendering, with the fractional second zero-padded to nanosecond precision when present. The types are produced by Parse and accepted by Marshal.

Errors

The entry points return:

  • *SyntaxError for a malformed document, with the 1-based line
  • *DecodeError for a decoding failure, with the key path in Path
  • *EncodeError for an encoding failure, with the key path in Path
  • a plain error for the rest: a non-pointer decode target, a cancelled context, a key that is not valid UTF-8

Decode and encode failures carry the key path or element index in the typed wrappers above, so errors.Is and errors.AsType see through them and the path reads from a field instead of the message text.

Notes

The exported surface is documented in godoc form in the source, and go doc . run from the module root is the authority on signatures and types. This file explains what the surface is for and how the parts fit together.