# 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 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. 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](#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: ```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 ` | | 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. ```go 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: ```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 } ``` ### 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: ```go 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: ```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. A string the literal form cannot carry verbatim (an embedded run of three single quotes, a control character other than tab, or a carriage return outside a CRLF pair) also keeps the basic form, so the output always re-parses to the same value. ### 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 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: ```go 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 `'''...'''` | ```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 - `*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.