2026-08-19 09:47:00 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
|
|
|
|
//
|
2026-09-20 10:40:57 +02:00
|
|
|
// or, for the document with its key order and comments:
|
2026-08-19 09:47:00 +02:00
|
|
|
//
|
2026-09-20 10:40:57 +02:00
|
|
|
// doc, err := interpres.Parse(data)
|
|
|
|
|
// tree := doc.Map()
|
2026-08-19 09:47:00 +02:00
|
|
|
//
|
|
|
|
|
// A Decoder allows strict decoding that rejects keys without a matching
|
2026-09-22 18:42:18 +02:00
|
|
|
// struct field, mirroring the RejectUnknownFields option of encoding/json/v2.
|
2026-08-19 09:47:00 +02:00
|
|
|
package interpres
|
|
|
|
|
|
|
|
|
|
import (
|
2026-09-21 23:55:58 +02:00
|
|
|
"bytes"
|
2026-08-19 09:47:00 +02:00
|
|
|
"context"
|
2026-09-17 21:20:47 +02:00
|
|
|
"errors"
|
2026-08-19 09:47:00 +02:00
|
|
|
"fmt"
|
2026-09-22 01:12:04 +02:00
|
|
|
"io"
|
|
|
|
|
"iter"
|
2026-09-21 23:51:58 +02:00
|
|
|
"os"
|
2026-09-22 00:15:17 +02:00
|
|
|
"reflect"
|
2026-09-20 22:15:38 +02:00
|
|
|
"slices"
|
2026-09-21 23:55:58 +02:00
|
|
|
"strings"
|
2026-09-22 00:44:23 +02:00
|
|
|
"time"
|
2026-08-19 09:47:00 +02:00
|
|
|
)
|
|
|
|
|
|
2026-09-21 23:55:58 +02:00
|
|
|
// A SyntaxError describes a malformed TOML document. Line is the 1-based line
|
|
|
|
|
// the problem was detected on. Offset is the byte offset in the input the scan
|
|
|
|
|
// stopped at, and Column is the 1-based column on that line; both are new in
|
|
|
|
|
// 2.0 and a struct literal that names Line and Msg alone still builds.
|
2026-08-19 09:47:00 +02:00
|
|
|
type SyntaxError struct {
|
2026-09-21 23:55:58 +02:00
|
|
|
Line int
|
|
|
|
|
Offset int
|
|
|
|
|
Column int
|
|
|
|
|
Msg string
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *SyntaxError) Error() string {
|
|
|
|
|
return fmt.Sprintf("interpres: line %d: %s", e.Line, e.Msg)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 23:55:58 +02:00
|
|
|
// SourceLine returns the source line the error points at, rendered from src,
|
|
|
|
|
// followed by a caret line marking the column. It is meant for a message the
|
|
|
|
|
// reader sees under the input:
|
|
|
|
|
//
|
|
|
|
|
// port = = 8080
|
|
|
|
|
// ^
|
|
|
|
|
//
|
|
|
|
|
// The caret sits at Offset when it falls inside src, and at the start of the
|
|
|
|
|
// line when the error carries no position.
|
|
|
|
|
func (e *SyntaxError) SourceLine(src []byte) string {
|
|
|
|
|
off := min(e.Offset, len(src))
|
|
|
|
|
start := 0
|
|
|
|
|
if i := bytes.LastIndexByte(src[:off], '\n'); i >= 0 {
|
|
|
|
|
start = i + 1
|
|
|
|
|
}
|
|
|
|
|
end := len(src)
|
|
|
|
|
if i := bytes.IndexByte(src[start:], '\n'); i >= 0 {
|
|
|
|
|
end = start + i
|
|
|
|
|
}
|
|
|
|
|
return string(src[start:end]) + "\n" + strings.Repeat(" ", off-start) + "^"
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:34:48 +02:00
|
|
|
// A Path names a value in a document, one segment per level from the root:
|
|
|
|
|
// a key contributes its name and an array element its bracketed index, so the
|
|
|
|
|
// path of the weight field of the first item is the segments
|
|
|
|
|
// ["items", "[0]", "weight"]. String renders the TOML notation,
|
|
|
|
|
// "items[0].weight".
|
|
|
|
|
type Path []string
|
|
|
|
|
|
|
|
|
|
// String renders the path the way a TOML document writes it: keys join with
|
|
|
|
|
// dots and an index attaches to the previous segment in brackets.
|
|
|
|
|
func (p Path) String() string {
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
for _, s := range p {
|
|
|
|
|
if strings.HasPrefix(s, "[") {
|
|
|
|
|
b.WriteString(s)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if b.Len() > 0 {
|
|
|
|
|
b.WriteByte('.')
|
|
|
|
|
}
|
|
|
|
|
b.WriteString(s)
|
|
|
|
|
}
|
|
|
|
|
return b.String()
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 21:20:47 +02:00
|
|
|
// A DecodeError wraps a decoding failure with the key path at which it
|
2026-09-22 00:34:48 +02:00
|
|
|
// happened. Read the path programmatically with errors.AsType:
|
2026-09-17 21:20:47 +02:00
|
|
|
//
|
|
|
|
|
// if de, ok := errors.AsType[*interpres.DecodeError](err); ok {
|
2026-09-22 00:34:48 +02:00
|
|
|
// fmt.Println(de.Path.String(), de.Err)
|
2026-09-17 21:20:47 +02:00
|
|
|
// }
|
|
|
|
|
type DecodeError struct {
|
|
|
|
|
// Path is the key path from the document root, outermost key first.
|
2026-09-22 00:34:48 +02:00
|
|
|
Path Path
|
2026-09-17 21:20:47 +02:00
|
|
|
// Err is the failure at that path.
|
|
|
|
|
Err error
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:34:48 +02:00
|
|
|
func (e *DecodeError) Error() string {
|
|
|
|
|
msg := strings.TrimPrefix(e.Err.Error(), "interpres: ")
|
|
|
|
|
if p := e.Path.String(); p != "" {
|
|
|
|
|
return "interpres: " + p + ": " + msg
|
|
|
|
|
}
|
|
|
|
|
return "interpres: " + msg
|
|
|
|
|
}
|
2026-09-17 21:20:47 +02:00
|
|
|
|
|
|
|
|
// 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
|
2026-09-22 00:34:48 +02:00
|
|
|
// key and index on its way down, so the wrap flattens that inner error's
|
|
|
|
|
// segments onto the front and keeps the failure it pointed at, leaving one
|
|
|
|
|
// path and one failure to render.
|
2026-09-17 21:20:47 +02:00
|
|
|
func newDecodeError(key string, err error) *DecodeError {
|
2026-09-22 00:34:48 +02:00
|
|
|
path := make(Path, 0, 4)
|
2026-09-17 21:20:47 +02:00
|
|
|
path = append(path, key)
|
|
|
|
|
if de, ok := errors.AsType[*DecodeError](err); ok {
|
|
|
|
|
path = append(path, de.Path...)
|
2026-09-22 00:34:48 +02:00
|
|
|
err = de.Err
|
2026-09-17 21:20:47 +02:00
|
|
|
}
|
|
|
|
|
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.
|
2026-09-22 00:34:48 +02:00
|
|
|
Path Path
|
2026-09-17 21:20:47 +02:00
|
|
|
// Err is the failure at that path.
|
|
|
|
|
Err error
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:34:48 +02:00
|
|
|
func (e *EncodeError) Error() string {
|
|
|
|
|
msg := strings.TrimPrefix(e.Err.Error(), "interpres: ")
|
|
|
|
|
if p := e.Path.String(); p != "" {
|
|
|
|
|
return "interpres: " + p + ": " + msg
|
|
|
|
|
}
|
|
|
|
|
return "interpres: " + msg
|
|
|
|
|
}
|
2026-09-17 21:20:47 +02:00
|
|
|
|
|
|
|
|
// Unwrap returns the failure the path points at.
|
|
|
|
|
func (e *EncodeError) Unwrap() error { return e.Err }
|
|
|
|
|
|
2026-09-20 10:40:57 +02:00
|
|
|
// Parse decodes a TOML document into a Document: the values, the order the
|
|
|
|
|
// keys were written in, whether a table was written inline, and the comments.
|
|
|
|
|
// ParseMap gives the plain value tree instead.
|
2026-08-19 09:47:00 +02:00
|
|
|
//
|
|
|
|
|
// Values are mapped to Go types as follows: strings to string, integers to
|
2026-09-20 10:40:57 +02:00
|
|
|
// int64, floats to float64, booleans to bool, offset date-times to
|
|
|
|
|
// OffsetDateTime, the local date-time kinds to their wrappers, arrays to
|
|
|
|
|
// []any, and tables (including inline tables) to map[string]any.
|
2026-08-19 09:47:00 +02:00
|
|
|
//
|
|
|
|
|
// Parse is equivalent to ParseContext with context.Background.
|
2026-09-20 10:40:57 +02:00
|
|
|
func Parse(data []byte) (*Document, error) {
|
2026-08-19 09:47:00 +02:00
|
|
|
return ParseContext(context.Background(), data)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 10:40:57 +02:00
|
|
|
// ParseContext decodes a TOML document into a Document, 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) (*Document, error) {
|
|
|
|
|
_, doc, err := parseWithOptions(ctx, data, parseOptions{}, true)
|
|
|
|
|
return doc, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseMap decodes a TOML document into a nested map[string]any, the value
|
|
|
|
|
// tree without the order and the comments a Document carries. It is the shape
|
|
|
|
|
// this package parsed into before [Document] existed.
|
|
|
|
|
//
|
|
|
|
|
// ParseMap is equivalent to ParseMapContext with context.Background.
|
|
|
|
|
func ParseMap(data []byte) (map[string]any, error) {
|
|
|
|
|
return ParseMapContext(context.Background(), data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseMapContext is the cancellable variant of ParseMap.
|
|
|
|
|
func ParseMapContext(ctx context.Context, data []byte) (map[string]any, error) {
|
|
|
|
|
tree, _, err := parseWithOptions(ctx, data, parseOptions{}, false)
|
|
|
|
|
return tree, err
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-21 23:51:58 +02:00
|
|
|
// ParseFile reads the TOML document at path and parses it into a Document,
|
|
|
|
|
// the shape Parse gives. Every error names the file it came from: a read
|
|
|
|
|
// failure and a parse failure alike carry the path as their first words,
|
|
|
|
|
// wrapped so errors.AsType still reaches the SyntaxError inside.
|
|
|
|
|
func ParseFile(path string) (*Document, error) {
|
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("%s: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
doc, err := Parse(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("%s: %w", path, err)
|
|
|
|
|
}
|
|
|
|
|
return doc, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Valid reports whether data is a valid TOML document: nil when the parser
|
|
|
|
|
// accepts it, and the parse error when it does not. It is the library call
|
|
|
|
|
// the -validate mode of interpres-decode is built on, and it reads nothing
|
|
|
|
|
// but the bytes it is given.
|
|
|
|
|
func Valid(data []byte) error {
|
|
|
|
|
_, err := ParseMapContext(context.Background(), data)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 23:49:39 +02:00
|
|
|
// parseOptions bound the work one parse may do and the shape it produces. A
|
|
|
|
|
// zero field takes the default.
|
2026-09-19 19:36:24 +02:00
|
|
|
type parseOptions struct {
|
|
|
|
|
maxDepth int
|
|
|
|
|
maxInputSize int
|
2026-09-21 23:49:39 +02:00
|
|
|
useNumber bool
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-20 10:40:57 +02:00
|
|
|
// parseWithOptions parses data, building the node tree of a Document when
|
|
|
|
|
// wantDoc asks for it, and returns both the value tree and that document.
|
|
|
|
|
func parseWithOptions(ctx context.Context, data []byte, opts parseOptions, wantDoc bool) (map[string]any, *Document, error) {
|
2026-08-19 09:47:00 +02:00
|
|
|
if err := ctx.Err(); err != nil {
|
2026-09-20 10:40:57 +02:00
|
|
|
return nil, nil, err
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
if opts.maxInputSize > 0 && len(data) > opts.maxInputSize {
|
2026-09-20 10:40:57 +02:00
|
|
|
return nil, nil, fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), opts.maxInputSize)
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
2026-09-20 22:15:10 +02:00
|
|
|
// UTF-8 validity is not checked in a pass of its own: the scanner
|
|
|
|
|
// validates the multi-byte sequences where it meets them, so an invalid
|
|
|
|
|
// byte is reported on its own line instead of always on line 1.
|
2026-09-19 19:36:24 +02:00
|
|
|
maxDepth := opts.maxDepth
|
|
|
|
|
if maxDepth <= 0 {
|
|
|
|
|
maxDepth = maxNestingDepth
|
|
|
|
|
}
|
2026-09-17 23:11:50 +02:00
|
|
|
// The parser scans data in place; it only reads the buffer, and every
|
|
|
|
|
// string it stores in the tree is copied out of it.
|
2026-09-21 23:49:39 +02:00
|
|
|
p := &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, wantDoc: wantDoc, useNumber: opts.useNumber}
|
2026-09-20 10:40:57 +02:00
|
|
|
tree, err := p.parse()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
if !wantDoc {
|
|
|
|
|
return tree, nil, nil
|
|
|
|
|
}
|
|
|
|
|
return tree, &Document{root: p.doc, footer: p.footer}, nil
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
//
|
2026-09-22 18:42:18 +02:00
|
|
|
// 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.
|
|
|
|
|
// Options tune the call; with none, unknown keys are ignored, numbers are
|
|
|
|
|
// evaluated, and the nesting default applies.
|
|
|
|
|
//
|
2026-08-19 09:47:00 +02:00
|
|
|
// 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.
|
|
|
|
|
//
|
2026-09-19 02:41:09 +02:00
|
|
|
// A destination implementing Unmarshaler receives the parsed value as it is,
|
|
|
|
|
// a TOML string fills a destination implementing encoding.TextUnmarshaler, and
|
|
|
|
|
// a time.Duration destination takes a duration literal such as `1h30m` or a
|
|
|
|
|
// bare integer as its nanosecond count.
|
|
|
|
|
//
|
2026-08-19 09:47:00 +02:00
|
|
|
// Unmarshal is equivalent to UnmarshalContext with context.Background.
|
2026-09-22 18:42:18 +02:00
|
|
|
func Unmarshal(data []byte, v any, opts ...UnmarshalOption) error {
|
|
|
|
|
return UnmarshalContext(context.Background(), data, v, opts...)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:36:10 +02:00
|
|
|
// ParseAs decodes a TOML document into T in one call, the generic shorthand
|
|
|
|
|
// for Unmarshal with a destination variable:
|
|
|
|
|
//
|
|
|
|
|
// cfg, err := interpres.ParseAs[Config](data)
|
|
|
|
|
//
|
|
|
|
|
// The zero T comes back with the error.
|
|
|
|
|
func ParseAs[T any](data []byte) (T, error) {
|
|
|
|
|
var v T
|
|
|
|
|
err := Unmarshal(data, &v)
|
|
|
|
|
return v, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewSchema precompiles the codec for T: the struct schema both directions
|
|
|
|
|
// walk and the interface flags the decoder and the encoder resolve through
|
|
|
|
|
// are built once and cached, so the first document pays the cost instead of
|
|
|
|
|
// the hot path. A T that is not a struct warms nothing; there is nothing to
|
|
|
|
|
// precompute for a map or a slice.
|
|
|
|
|
func NewSchema[T any]() {
|
|
|
|
|
t := reflect.TypeFor[T]()
|
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cachedStructSchema(t)
|
|
|
|
|
_ = typeFlags(t)
|
|
|
|
|
_ = encTypeFlags(t)
|
|
|
|
|
pt := reflect.PointerTo(t)
|
|
|
|
|
_ = typeFlags(pt)
|
|
|
|
|
_ = encTypeFlags(pt)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
// UnmarshalContext is the cancellable variant of Unmarshal.
|
2026-09-22 18:42:18 +02:00
|
|
|
func UnmarshalContext(ctx context.Context, data []byte, v any, opts ...UnmarshalOption) error {
|
|
|
|
|
return settingsFor(opts).decode(ctx, data, v)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 18:42:18 +02:00
|
|
|
// UnmarshalRead reads the document from r and decodes it into v, the
|
|
|
|
|
// streaming-shaped entry the json/v2 vocabulary uses. The reader is
|
|
|
|
|
// consumed in full, because the parser scans its source in place; the
|
|
|
|
|
// options and the behaviour are Unmarshal's.
|
|
|
|
|
func UnmarshalRead(r io.Reader, v any, opts ...UnmarshalOption) error {
|
|
|
|
|
data, err := io.ReadAll(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("interpres: read: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return Unmarshal(data, v, opts...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// An UnmarshalOption configures one Unmarshal, UnmarshalContext,
|
|
|
|
|
// UnmarshalRead or ParseAs call. Options are function values over the
|
|
|
|
|
// private decode settings, the shape encoding/json/v2 uses for its own
|
|
|
|
|
// options, and compose by simple listing:
|
|
|
|
|
//
|
|
|
|
|
// err := interpres.Unmarshal(data, &cfg,
|
|
|
|
|
// interpres.RejectUnknownFields(true),
|
|
|
|
|
// interpres.NumbersAsLiterals(true))
|
|
|
|
|
//
|
|
|
|
|
// A destination that the direct skeleton cannot model falls back to the
|
|
|
|
|
// tree path, so every option means the same thing on every document.
|
|
|
|
|
type UnmarshalOption func(*decodeSettings)
|
|
|
|
|
|
|
|
|
|
// decodeSettings is the option carrier of one decode call.
|
|
|
|
|
type decodeSettings struct {
|
2026-08-19 09:47:00 +02:00
|
|
|
disallowUnknown bool
|
2026-09-21 23:49:39 +02:00
|
|
|
useNumber bool
|
2026-09-19 19:36:24 +02:00
|
|
|
maxDepth int
|
|
|
|
|
maxInputSize int
|
2026-09-22 00:44:23 +02:00
|
|
|
localLoc *time.Location
|
2026-09-22 18:42:18 +02:00
|
|
|
ctx context.Context
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 18:42:18 +02:00
|
|
|
func settingsFor(opts []UnmarshalOption) *decodeSettings {
|
|
|
|
|
s := &decodeSettings{ctx: context.Background()}
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(s)
|
|
|
|
|
}
|
|
|
|
|
return s
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 18:42:18 +02:00
|
|
|
// decode runs the decode the settings describe: the targeted parse when the
|
|
|
|
|
// destination takes it, the tree path otherwise or on fallback.
|
|
|
|
|
func (s *decodeSettings) decode(ctx context.Context, data []byte, v any) error {
|
2026-09-22 15:59:03 +02:00
|
|
|
dec := newDecoder()
|
2026-09-22 18:42:18 +02:00
|
|
|
dec.disallowUnknown = s.disallowUnknown
|
2026-09-22 15:59:03 +02:00
|
|
|
dec.ctx = ctx
|
2026-09-22 18:42:18 +02:00
|
|
|
dec.loc = s.localLoc
|
2026-09-22 15:59:03 +02:00
|
|
|
if canTargetDecode(v) {
|
|
|
|
|
// The targeted parse fills struct destinations without the
|
|
|
|
|
// intermediate tree; a document or destination it cannot model falls
|
|
|
|
|
// back to the tree path, whose contracts it keeps. The size limit is
|
|
|
|
|
// checked here, the targeted parse being the parse itself.
|
2026-09-22 18:42:18 +02:00
|
|
|
if s.maxInputSize > 0 && len(data) > s.maxInputSize {
|
|
|
|
|
return fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), s.maxInputSize)
|
2026-09-22 15:59:03 +02:00
|
|
|
}
|
2026-09-22 18:42:18 +02:00
|
|
|
if err := parseIntoTargeted(ctx, data, dec, s.useNumber, s.maxDepth, v); err != errTargetFallback {
|
2026-09-22 15:59:03 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
opts := parseOptions{
|
2026-09-22 18:42:18 +02:00
|
|
|
maxDepth: s.maxDepth,
|
|
|
|
|
maxInputSize: s.maxInputSize,
|
|
|
|
|
useNumber: s.useNumber,
|
2026-09-22 00:15:17 +02:00
|
|
|
}
|
|
|
|
|
tree, doc, err := parseWithOptions(ctx, data, opts, typeWantsOrder(reflect.TypeOf(v)))
|
2026-08-19 09:47:00 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
dec.nodes = indexNodes(doc.Root())
|
2026-08-19 09:47:00 +02:00
|
|
|
return dec.decode(tree, v)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 18:42:18 +02:00
|
|
|
// RejectUnknownFields makes the decode fail when the document contains a
|
|
|
|
|
// key with no matching destination struct field. Off by default: unknown
|
|
|
|
|
// keys are ignored.
|
|
|
|
|
func RejectUnknownFields(v bool) UnmarshalOption {
|
|
|
|
|
return func(s *decodeSettings) { s.disallowUnknown = v }
|
2026-09-22 00:21:46 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 18:42:18 +02:00
|
|
|
// NumbersAsLiterals keeps the numbers of the document as a Number carrying
|
|
|
|
|
// the literal the document wrote, so 0x1f, 1_000, +1.0 and inf survive a
|
|
|
|
|
// round trip with their spelling intact. A destination of a concrete numeric
|
|
|
|
|
// kind still takes the evaluated value; the literal is kept only where a
|
|
|
|
|
// Number, or an any, receives it. Off by default: numbers evaluate to
|
|
|
|
|
// int64 and float64.
|
|
|
|
|
func NumbersAsLiterals(v bool) UnmarshalOption {
|
|
|
|
|
return func(s *decodeSettings) { s.useNumber = v }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LocalTimeLocation sets the zone a local date-time is placed in when it
|
|
|
|
|
// decodes into a time.Time destination. Without the option a local date-time
|
|
|
|
|
// fills only its own wrapper type (LocalDateTime, LocalDate, LocalTime),
|
|
|
|
|
// whose embedded time.Time is UTC; with the option, a time.Time destination
|
|
|
|
|
// takes the value too, carried in the location given. A nil location restores
|
|
|
|
|
// the default.
|
|
|
|
|
func LocalTimeLocation(loc *time.Location) UnmarshalOption {
|
|
|
|
|
return func(s *decodeSettings) { s.localLoc = loc }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MaxNestingDepth bounds how deeply arrays and inline tables may nest in a
|
|
|
|
|
// document the decode accepts. The parser is a recursive descent, so a
|
|
|
|
|
// document that nests without bound would exhaust the stack; one that nests
|
|
|
|
|
// deeper than the limit is rejected with a SyntaxError naming it instead.
|
|
|
|
|
// Use 0 or any negative value for the default of 10000, which no
|
|
|
|
|
// hand-written document approaches.
|
|
|
|
|
func MaxNestingDepth(depth int) UnmarshalOption {
|
|
|
|
|
return func(s *decodeSettings) { s.maxDepth = depth }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MaxInputSize bounds the size of a document the decode accepts, in bytes; a
|
|
|
|
|
// larger one is rejected before parsing starts. Use 0 or any negative value
|
|
|
|
|
// for no limit, which is the default: the caller already holds the bytes, so
|
|
|
|
|
// the size is a policy the caller sets rather than a protection the library
|
|
|
|
|
// imposes on its own.
|
|
|
|
|
func MaxInputSize(size int) UnmarshalOption {
|
|
|
|
|
return func(s *decodeSettings) { s.maxInputSize = size }
|
2026-09-22 00:21:46 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
// 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).
|
2026-09-19 02:41:09 +02:00
|
|
|
//
|
|
|
|
|
// MarshalTOML wins over encoding.TextMarshaler when a type implements both.
|
|
|
|
|
// A type that implements only encoding.TextMarshaler is encoded as a TOML
|
|
|
|
|
// string holding its text, and needs no method here.
|
2026-08-19 09:47:00 +02:00
|
|
|
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,
|
2026-09-19 19:36:24 +02:00
|
|
|
// bool, int64, float64, OffsetDateTime, LocalDateTime, LocalDate, LocalTime,
|
|
|
|
|
// []any, or map[string]any. A tree built by hand may carry a plain time.Time
|
2026-09-22 00:04:16 +02:00
|
|
|
// where the parser would put an OffsetDateTime, and a Decoder configured with
|
|
|
|
|
// UseNumber a Number.
|
2026-09-19 19:36:24 +02:00
|
|
|
//
|
|
|
|
|
// 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).
|
2026-08-19 09:47:00 +02:00
|
|
|
//
|
|
|
|
|
// 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.
|
2026-09-19 02:41:09 +02:00
|
|
|
//
|
|
|
|
|
// UnmarshalTOML wins over encoding.TextUnmarshaler when a type implements
|
|
|
|
|
// both. A type that implements only encoding.TextUnmarshaler is filled from a
|
|
|
|
|
// TOML string holding its text, and needs no method here.
|
2026-08-19 09:47:00 +02:00
|
|
|
type Unmarshaler interface {
|
|
|
|
|
UnmarshalTOML(data any) error
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:04:16 +02:00
|
|
|
// UnmarshalerContext is Unmarshaler with the decode's context handed in. A
|
|
|
|
|
// type that implements both interfaces gets UnmarshalTOMLContext, so a long
|
|
|
|
|
// custom decode can abort on cancellation instead of running to completion.
|
|
|
|
|
// The context a non-cancellable entry point carries is context.Background,
|
|
|
|
|
// never nil.
|
|
|
|
|
type UnmarshalerContext interface {
|
|
|
|
|
UnmarshalTOMLContext(ctx context.Context, data any) error
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 02:24:24 +02:00
|
|
|
// Marshal returns the TOML encoding of v. The output is valid TOML 1.1.
|
2026-08-19 09:47:00 +02:00
|
|
|
//
|
|
|
|
|
// 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
|
2026-09-17 19:57:23 +02:00
|
|
|
// 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.
|
2026-08-19 09:47:00 +02:00
|
|
|
// - 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 = []`.
|
2026-09-17 19:49:50 +02:00
|
|
|
// - 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.
|
2026-08-19 09:47:00 +02:00
|
|
|
// - Scalars encode as TOML scalars: bool, int64, float64, string, time.Time
|
2026-09-19 19:36:24 +02:00
|
|
|
// and OffsetDateTime (offset date-time), and LocalDateTime/LocalDate/
|
|
|
|
|
// LocalTime (local variants). A date-time writes its seconds only when the value carries
|
2026-09-19 12:18:18 +02:00
|
|
|
// them, and drops the trailing zeros of a fractional second.
|
2026-09-19 12:18:30 +02:00
|
|
|
// - A table element of a value array, and a sub-table inlined by
|
|
|
|
|
// Encoder.InlineTables, is written as an inline table, across lines when it
|
|
|
|
|
// does not fit one.
|
2026-08-19 09:47:00 +02:00
|
|
|
// - Values implementing Marshaler are encoded by calling MarshalTOML and
|
|
|
|
|
// using its result.
|
2026-09-19 02:41:09 +02:00
|
|
|
// - Values implementing encoding.TextMarshaler, and not one of the
|
|
|
|
|
// date-time types, encode as a TOML string holding the text the method
|
|
|
|
|
// returns. time.Duration is written in its canonical Go form, `1h30m0s`.
|
2026-08-19 09:47:00 +02:00
|
|
|
// - nil pointer fields are omitted.
|
|
|
|
|
//
|
2026-09-22 00:21:46 +02:00
|
|
|
// Marshal rejects a value that nests deeper than 10000 levels with an error
|
|
|
|
|
// naming the limit, so cyclic data is reported instead of running the stack
|
|
|
|
|
// out. The output is not guaranteed to be byte-identical to
|
2026-08-19 09:47:00 +02:00
|
|
|
// 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.
|
|
|
|
|
//
|
2026-09-22 18:42:18 +02:00
|
|
|
// Marshal returns the TOML encoding of v. Options tune the emission; with
|
|
|
|
|
// none, the layout groups entries by kind, empty arrays emit and sub-tables
|
|
|
|
|
// take the header form.
|
|
|
|
|
//
|
2026-08-19 09:47:00 +02:00
|
|
|
// Marshal is equivalent to MarshalContext with context.Background.
|
2026-09-22 18:42:18 +02:00
|
|
|
func Marshal(v any, opts ...MarshalOption) ([]byte, error) {
|
|
|
|
|
return MarshalContext(context.Background(), v, opts...)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 01:12:04 +02:00
|
|
|
// A Statement is one top-level statement of a document, what Statements
|
|
|
|
|
// yields: a key with its value, a table with its node, or one element of an
|
|
|
|
|
// array of tables with its node.
|
|
|
|
|
type Statement struct {
|
|
|
|
|
// Key is the key as the document wrote it.
|
|
|
|
|
Key string
|
|
|
|
|
// Value is the value of a key/value statement, and the value map of a
|
|
|
|
|
// table statement.
|
|
|
|
|
Value any
|
|
|
|
|
// Table is the node of a table or array-of-tables statement, carrying the
|
|
|
|
|
// written key order and the comments; nil for a plain key/value.
|
|
|
|
|
Table *Table
|
|
|
|
|
// Index is the element's position when the statement is one element of an
|
|
|
|
|
// array of tables, and -1 otherwise.
|
|
|
|
|
Index int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Statements reads a TOML document from r and returns an iterator over its
|
|
|
|
|
// top-level statements in written order: key/value statements, a [table]
|
|
|
|
|
// header as one statement carrying its Table node, and an [[array of
|
|
|
|
|
// tables]] as one statement per element, each with the element's node and
|
|
|
|
|
// its Index. Iteration stops at the first error, which arrives as the second
|
|
|
|
|
// value, and at a false yield: a caller that breaks after the statement it
|
|
|
|
|
// wanted reads no further ones.
|
|
|
|
|
//
|
|
|
|
|
// The reader is consumed in full before the first statement is yielded,
|
|
|
|
|
// because the parser scans the source in place; processing the yielded
|
|
|
|
|
// statements one at a time is what bounds what the caller holds, and a
|
|
|
|
|
// later direct-to-target parse removes the whole-source hold.
|
|
|
|
|
func Statements(r io.Reader) iter.Seq2[Statement, error] {
|
|
|
|
|
return func(yield func(Statement, error) bool) {
|
|
|
|
|
data, err := io.ReadAll(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
yield(Statement{Index: -1}, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
doc, err := Parse(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
yield(Statement{Index: -1}, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, e := range doc.Root().Entries() {
|
|
|
|
|
if els := e.Elements(); len(els) > 0 {
|
|
|
|
|
for i, el := range els {
|
|
|
|
|
if !yield(Statement{Key: e.Key(), Value: e.Value(), Table: el, Index: i}, nil) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if child := e.Table(); child != nil {
|
|
|
|
|
if !yield(Statement{Key: e.Key(), Value: e.Value(), Table: child, Index: -1}, nil) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !yield(Statement{Key: e.Key(), Value: e.Value(), Index: -1}, nil) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 23:58:23 +02:00
|
|
|
// MarshalAppend appends the TOML encoding of v to buf and returns the extended
|
2026-09-22 18:42:18 +02:00
|
|
|
// buffer, the shape json/v2's MarshalAppendTo and json's MarshalAppend have.
|
|
|
|
|
// A failed encoding leaves buf untouched and comes back with a nil slice.
|
|
|
|
|
func MarshalAppend(buf []byte, v any, opts ...MarshalOption) ([]byte, error) {
|
|
|
|
|
out, err := Marshal(v, opts...)
|
2026-09-21 23:58:23 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return append(buf, out...), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
// MarshalContext is the cancellable variant of Marshal.
|
2026-09-22 18:42:18 +02:00
|
|
|
func MarshalContext(ctx context.Context, v any, opts ...MarshalOption) ([]byte, error) {
|
2026-08-19 09:47:00 +02:00
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-09-22 18:42:18 +02:00
|
|
|
return settingsForEncode(opts).marshal(ctx, v)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MarshalWrite encodes v and writes the document to w, the streaming-shaped
|
|
|
|
|
// entry the json/v2 vocabulary uses. The options and the behaviour are
|
|
|
|
|
// Marshal's.
|
|
|
|
|
func MarshalWrite(w io.Writer, v any, opts ...MarshalOption) error {
|
|
|
|
|
out, err := Marshal(v, opts...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if _, err := w.Write(out); err != nil {
|
|
|
|
|
return fmt.Errorf("interpres: write: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 01:09:00 +02:00
|
|
|
// A LayoutKind names the layout the encoder writes a document's entries in.
|
|
|
|
|
type LayoutKind int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// LayoutKindGrouped reorders entries at one level: scalars first, then
|
|
|
|
|
// sub-tables, then arrays of tables. The default.
|
|
|
|
|
LayoutKindGrouped LayoutKind = iota
|
|
|
|
|
// LayoutKindDeclaration preserves the declaration order: struct field
|
|
|
|
|
// order, or sorted key order for maps.
|
|
|
|
|
LayoutKindDeclaration
|
|
|
|
|
)
|
|
|
|
|
|
2026-09-22 18:42:18 +02:00
|
|
|
// A MarshalOption configures one Marshal, MarshalContext, MarshalAppend or
|
|
|
|
|
// MarshalWrite call. Options are function values over the private encode
|
|
|
|
|
// settings, the shape encoding/json/v2 uses for its own, and compose by
|
|
|
|
|
// simple listing:
|
2026-08-19 09:47:00 +02:00
|
|
|
//
|
2026-09-22 18:42:18 +02:00
|
|
|
// out, err := interpres.Marshal(cfg,
|
|
|
|
|
// interpres.Layout(interpres.LayoutKindDeclaration),
|
|
|
|
|
// interpres.InlineTables(60))
|
|
|
|
|
type MarshalOption func(*encodeSettings)
|
|
|
|
|
|
|
|
|
|
// encodeSettings is the option carrier of one encode call.
|
|
|
|
|
type encodeSettings struct {
|
|
|
|
|
ctx context.Context
|
|
|
|
|
cfg encodeConfig
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 18:42:18 +02:00
|
|
|
func settingsForEncode(opts []MarshalOption) *encodeSettings {
|
|
|
|
|
s := &encodeSettings{ctx: context.Background(), cfg: encodeConfig{layout: LayoutKindGrouped}}
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
|
opt(s)
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// marshal runs the encode the settings describe.
|
|
|
|
|
func (s *encodeSettings) marshal(ctx context.Context, v any) ([]byte, error) {
|
|
|
|
|
enc := newEncoder()
|
|
|
|
|
enc.ctx = ctx
|
|
|
|
|
enc.opts = s.cfg
|
|
|
|
|
if err := enc.encode(v); err != nil {
|
|
|
|
|
enc.release()
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
// The output leaves the pooled buffer as a copy, so the next Marshal
|
|
|
|
|
// reuses the buffer without touching what the caller holds.
|
|
|
|
|
out := slices.Clone(enc.buf.Bytes())
|
|
|
|
|
enc.release()
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
|
2026-09-22 01:09:00 +02:00
|
|
|
// Layout sets the layout the encoder writes a document's entries in:
|
|
|
|
|
// LayoutKindGrouped, the default, reorders them scalars first, then tables,
|
|
|
|
|
// then arrays of tables; LayoutKindDeclaration preserves declaration order.
|
2026-09-22 18:42:18 +02:00
|
|
|
func Layout(kind LayoutKind) MarshalOption {
|
|
|
|
|
return func(s *encodeSettings) { s.cfg.layout = kind }
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
2026-09-22 18:42:18 +02:00
|
|
|
func OmitEmptyArrays(v bool) MarshalOption {
|
|
|
|
|
return func(s *encodeSettings) { s.cfg.omitEmptyArrays = v }
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 01:09:00 +02:00
|
|
|
// LiteralMultiline sets the length threshold at which a multi-line string
|
2026-08-19 09:47:00 +02:00
|
|
|
// 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.
|
2026-09-22 18:42:18 +02:00
|
|
|
func LiteralMultiline(threshold int) MarshalOption {
|
|
|
|
|
return func(s *encodeSettings) { s.cfg.literalMultilineAt = threshold }
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-19 12:18:30 +02:00
|
|
|
// InlineTables sets the size limit, in bytes of the single-line rendering, at
|
|
|
|
|
// which a sub-table is written as an inline table instead of a table header,
|
|
|
|
|
// which makes a document of small tables shorter. Use 0 or any negative value
|
|
|
|
|
// to disable (always emit a header).
|
|
|
|
|
//
|
|
|
|
|
// A sub-table is inlined only when doing so keeps every value's type: an array
|
|
|
|
|
// of tables keeps its header form, because its inline form would re-parse as a
|
|
|
|
|
// value array. An inlined table that does not fit the line is written across
|
|
|
|
|
// lines, which TOML 1.1 allows.
|
|
|
|
|
//
|
2026-09-22 01:09:00 +02:00
|
|
|
// With LayoutKindDeclaration the layout is already for presentation only, and an
|
2026-09-19 12:18:30 +02:00
|
|
|
// inlined table follows the same rule as any other value line: it lands in the
|
|
|
|
|
// section of the header that precedes it.
|
2026-09-22 18:42:18 +02:00
|
|
|
func InlineTables(threshold int) MarshalOption {
|
|
|
|
|
return func(s *encodeSettings) { s.cfg.inlineTablesAt = threshold }
|
2026-09-19 12:18:30 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:44:23 +02:00
|
|
|
// EmitFieldComments turns on printing the comment a field's `toml` tag
|
|
|
|
|
// carries in a `comment=` option, above the field's line or header, the
|
|
|
|
|
// comments a round trip through the Go type would otherwise drop:
|
|
|
|
|
//
|
|
|
|
|
// Port int `toml:"port,comment=The port to listen on"`
|
|
|
|
|
//
|
|
|
|
|
// Go doc comments are not visible to reflection, so the tag is the channel
|
|
|
|
|
// that carries the text. Off by default, and a field without a `comment=`
|
|
|
|
|
// option prints none. Multi-line comments carry newlines in the tag, each
|
|
|
|
|
// line printed with its own "# " marker.
|
2026-09-22 18:42:18 +02:00
|
|
|
func EmitFieldComments(v bool) MarshalOption {
|
|
|
|
|
return func(s *encodeSettings) { s.cfg.emitFieldComments = v }
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|