Files
interpres/docs/API.md
T
petrbalvin d12da6181d docs: document set and changelog
Assisted-by: GLM 5.3 Flash
2026-08-19 18:44:00 +02:00

432 lines
15 KiB
Markdown

# API
The library exports the surface below from the `sourcedock.dev/petrbalvin/interpres`
package. The snippets assume:
```go
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](#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)`.
```go
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)`.
```go
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](#encoding) section
below. Equivalent to `MarshalContext(context.Background(), v)`.
```go
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. 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 one declared later wins.
Unknown keys are ignored by default; see [Strict decoding](#strict-decoding).
### 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:
```go
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:
```go
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
```mermaid
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.
Note the asymmetry: the encoder inlines untagged embedded structs, while the
decoder expects them under their lower-cased type name. A struct with an
untagged embedded struct therefore does not round-trip through `Unmarshal` into
the same type.
### 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:
```go
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:
```go
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`.
```go
type Port int
func (p Port) MarshalTOML() (any, error) {
return int64(p), nil
}
```
### 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:
```go
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
```mermaid
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`:
```go
if se, ok := errors.AsType[*interpres.SyntaxError](err); ok {
fmt.Println(se.Line, se.Msg)
}
```
### `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 `'''...'''` |
```go
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](#custom-encoding-marshaler).
### `type Unmarshaler interface{ UnmarshalTOML(data any) error }`
See [Custom decoding](#custom-decoding-unmarshaler).
### Date-time wrappers
```go
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
- a plain error for everything else: a non-pointer decode target, a type
mismatch, an overflow, a marshal policy violation, a cancelled context
Decode and encode failures are wrapped with the key path or element index using
`fmt.Errorf`, so `errors.Is` and `errors.AsType` see through them.
## 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.