// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT // Package interpres is a dependency-free TOML parser for Go. // // interpres reads and writes TOML documents using only the standard library. // It exposes a small, encoding/json-style API: // // var cfg Config // err := interpres.Unmarshal(data, &cfg) // // out, err := interpres.Marshal(cfg) // // or, for an untyped tree: // // tree, err := interpres.Parse(data) // // A Decoder allows strict decoding that rejects keys without a matching // struct field, mirroring (*json.Decoder).DisallowUnknownFields. package interpres import ( "context" "errors" "fmt" "unicode/utf8" ) // A SyntaxError describes a malformed TOML document, including the 1-based // line on which the problem was detected. type SyntaxError struct { Line int Msg string } func (e *SyntaxError) Error() string { return fmt.Sprintf("interpres: line %d: %s", e.Line, e.Msg) } // A DecodeError 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 and 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 it programmatically with errors.AsType: // // if de, ok := errors.AsType[*interpres.DecodeError](err); ok { // fmt.Println(de.Path, de.Err) // } type DecodeError struct { // Path is the key path from the document root, outermost key first. Path []string // Err is the failure at that path. Err error } func (e *DecodeError) Error() string { return e.Path[0] + ": " + e.Err.Error() } // Unwrap returns the failure the path points at. func (e *DecodeError) Unwrap() error { return e.Err } // newDecodeError wraps err with one path segment. The rest of the path comes // from the DecodeError err already carries, if any: the decoder wraps each // key and index on its way down, so the innermost wrap holds the deepest // segments and each outer wrap prepends one. func newDecodeError(key string, err error) *DecodeError { path := make([]string, 0, 4) path = append(path, key) if de, ok := errors.AsType[*DecodeError](err); ok { path = append(path, de.Path...) } return &DecodeError{Path: path, Err: err} } // An EncodeError wraps an encoding failure with the key path of the value // that failed, in the notation of a TOML document: fields join with dots and // an array element carries its bracketed index, so the path of the third // port under server reads "server.ports[2]". The rendered message is // unchanged by the type; read it programmatically with errors.AsType. type EncodeError struct { // Path is the key path of the failing value. Path string // Err is the failure at that path. Err error } func (e *EncodeError) Error() string { return "interpres: " + e.Path + ": " + e.Err.Error() } // Unwrap returns the failure the path points at. func (e *EncodeError) Unwrap() error { return e.Err } // Parse decodes a TOML document into a nested map[string]any. // // Values are mapped to Go types as follows: strings to string, integers to // int64, floats to float64, booleans to bool, date-times to time.Time, arrays // to []any, and tables (including inline tables) to map[string]any. // // Parse is equivalent to ParseContext with context.Background. func Parse(data []byte) (map[string]any, error) { return ParseContext(context.Background(), data) } // ParseContext decodes a TOML document into a nested map[string]any, obeying // ctx. The context is checked between top-level statements so cancellation is // honoured before the parser has done substantial work. func ParseContext(ctx context.Context, data []byte) (map[string]any, error) { if err := ctx.Err(); err != nil { return nil, err } if !utf8.Valid(data) { return nil, &SyntaxError{Line: 1, Msg: "input is not valid UTF-8"} } // The parser scans data in place; it only reads the buffer, and every // string it stores in the tree is copied out of it. p := &parser{src: data, line: 1, ctx: ctx} return p.parse() } // Unmarshal parses a TOML document and stores the result in the value pointed // to by v. v is typically a pointer to a struct or to a map[string]any. // // Struct fields are matched to TOML keys by the `toml:"name"` tag, or by a // case-insensitive match on the field name when no tag is present. A tag of // "-" skips the field. // // Unmarshal is equivalent to UnmarshalContext with context.Background. func Unmarshal(data []byte, v any) error { return UnmarshalContext(context.Background(), data, v) } // UnmarshalContext is the cancellable variant of Unmarshal. func UnmarshalContext(ctx context.Context, data []byte, v any) error { tree, err := ParseContext(ctx, data) if err != nil { return err } return newDecoder().decode(tree, v) } // A Decoder decodes a TOML document into a Go value with configurable // strictness. type Decoder struct { disallowUnknown bool } // NewDecoder returns a Decoder. func NewDecoder() *Decoder { return &Decoder{} } // DisallowUnknownFields causes Decode to return an error when the document // contains a key with no matching destination struct field. func (d *Decoder) DisallowUnknownFields() *Decoder { d.disallowUnknown = true return d } // Decode parses data and stores the result in the value pointed to by v, // honouring the decoder's strictness settings. // // Decode is equivalent to DecodeContext with context.Background. func (d *Decoder) Decode(data []byte, v any) error { return d.DecodeContext(context.Background(), data, v) } // DecodeContext is the cancellable variant of Decode. func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error { tree, err := ParseContext(ctx, data) if err != nil { return err } dec := newDecoder() dec.disallowUnknown = d.disallowUnknown return dec.decode(tree, v) } // Marshaler is the interface implemented by types that can produce a custom // TOML representation of themselves. MarshalTOML returns a value that Marshal // then encodes as if the returned value had been passed in its place, which // is useful for emitting a Go type as a different TOML shape (for example, a // struct as an inline table or a primitive alias as a richer value). type Marshaler interface { MarshalTOML() (any, error) } // Unmarshaler is the inverse of Marshaler: a type that wants control over // how it is decoded from a TOML value may implement UnmarshalTOML. The data // argument is whatever the parser produced for that key: one of string, // bool, int64, float64, time.Time, LocalDateTime, LocalDate, LocalTime, // []any, or map[string]any. UnmarshalTOML may parse, inspect, or transform // the value however it likes, then store the result by mutating its // receiver through the standard pointer-indirection rules of the reflect // package (i.e. via reflect.Value.Set or by reassigning fields through a // pointer the receiver holds). // // UnmarshalTOML is invoked from (*Decoder).Decode / Unmarshal when the // destination type implements the interface. The decoder does not need to // consult the concrete return value; whatever the receiver stores is kept. type Unmarshaler interface { UnmarshalTOML(data any) error } // Marshal returns the TOML encoding of v. The output is valid TOML 1.1. // // Marshal traverses v using reflection and applies the following rules: // // - The top-level value must be a struct or a map[string]V. Pointers are // followed; a nil top-level pointer is an error. // - Struct fields are matched by `toml:"name"` tag (case-insensitive // fallback to field name; `-` skips). The tag options `omitzero` (skip // the zero value of the field's type) and `omitempty` (skip an empty // slice, array, or map) drop a field from the output on encode; the // decoder ignores them. Anonymous (embedded) fields without a tag are // inlined. // - Maps use sorted keys for deterministic output. // - Slices and arrays of structs or maps become TOML arrays of tables; a // nil or empty array of tables is omitted (TOML forbids an empty `[[a]]`), // while other empty arrays emit as `key = []`. // - Other slices and arrays become TOML arrays; a table element inside a // value array (for example an inline table in a mixed array) emits as an // inline table. // - Scalars encode as TOML scalars: bool, int64, float64, string, time.Time // (offset date-time), and LocalDateTime/LocalDate/LocalTime (local // variants). // - Values implementing Marshaler are encoded by calling MarshalTOML and // using its result. // - nil pointer fields are omitted. // // Marshal cannot encode cyclic data structures; passing one will loop until // the stack overflows. The output is not guaranteed to be byte-identical to // the input that produced v: comments, whitespace, key order (for maps), // string quoting style, and the choice between `[table]` headers and inline // tables are not preserved. // // Marshal is equivalent to MarshalContext with context.Background. func Marshal(v any) ([]byte, error) { return MarshalContext(context.Background(), v) } // MarshalContext is the cancellable variant of Marshal. func MarshalContext(ctx context.Context, v any) ([]byte, error) { if err := ctx.Err(); err != nil { return nil, err } return NewEncoder().MarshalContext(ctx, v) } // An Encoder encodes Go values into TOML. // // All options default to behaviour that preserves byte-for-byte compatibility // with previous releases and passes the toml-test compliance suite: // // GroupByKind: true (scalars first, then tables, then arrays of tables) // OmitEmptyArrays: false (a nil/empty []string slice emits [] as a value; // a nil/empty []Item struct slice is still skipped) // LiteralMultilineAt: 0 (always emit basic multi-line strings with // escape sequences, never literal ones) // // Use the chainable option methods to opt out. The option state is private; // callers that need the underlying knobs reach for the methods rather than // reading or mutating fields. type Encoder struct { groupByKind bool // default true; set via (*Encoder).GroupByKind omitEmptyArrays bool // default false; set via (*Encoder).OmitEmptyArrays literalMultilineAt int // default 0; set via (*Encoder).UseLiteralMultiline } // NewEncoder returns an Encoder with default options. func NewEncoder() *Encoder { return &Encoder{groupByKind: true} } // GroupByKind toggles whether fields at the same TOML level are reordered // into the group-by-kind layout (scalars first, then tables, then arrays of // tables). When set to false, the emitter preserves the source declaration // order (struct field order, or sorted key order for maps). func (e *Encoder) GroupByKind(v bool) *Encoder { e.groupByKind = v return e } // OmitEmptyArrays opts in to skipping empty (non-nil, length 0) TOML arrays // of scalars. The default emits them as "key = []". Nil slices and empty // arrays of tables are already always omitted. func (e *Encoder) OmitEmptyArrays() *Encoder { e.omitEmptyArrays = true return e } // UseLiteralMultiline sets the length threshold at which a multi-line string // is emitted as a literal triple-quoted string instead of the escaped form. // Use 0 or any negative value to disable (always escaped). The literal form // is selected only when the value contains an internal newline; otherwise the // single-line basic form is used regardless of this setting. func (e *Encoder) UseLiteralMultiline(threshold int) *Encoder { e.literalMultilineAt = threshold return e } // Marshal encodes v to TOML bytes. It is equivalent to calling Marshal with v. // // Marshal is equivalent to MarshalContext with context.Background. func (e *Encoder) Marshal(v any) ([]byte, error) { return e.MarshalContext(context.Background(), v) } // MarshalContext is the cancellable variant of Marshal. func (e *Encoder) MarshalContext(ctx context.Context, v any) ([]byte, error) { enc := newEncoder() enc.ctx = ctx enc.opts = *e if err := enc.encode(v); err != nil { return nil, err } return enc.bytes(), nil }