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
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
2026-09-19 02:41:09 +02:00
|
|
|
"encoding"
|
2026-09-17 21:20:47 +02:00
|
|
|
"errors"
|
2026-08-19 09:47:00 +02:00
|
|
|
"fmt"
|
2026-09-17 19:49:50 +02:00
|
|
|
"maps"
|
2026-08-19 09:47:00 +02:00
|
|
|
"math"
|
|
|
|
|
"reflect"
|
|
|
|
|
"slices"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2026-09-20 22:15:38 +02:00
|
|
|
"sync"
|
|
|
|
|
"sync/atomic"
|
2026-08-19 09:47:00 +02:00
|
|
|
"time"
|
|
|
|
|
"unicode/utf8"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
2026-09-19 19:36:24 +02:00
|
|
|
localDateTimeType = reflect.TypeFor[LocalDateTime]()
|
|
|
|
|
localDateType = reflect.TypeFor[LocalDate]()
|
|
|
|
|
localTimeType = reflect.TypeFor[LocalTime]()
|
|
|
|
|
offsetDateTimeType = reflect.TypeFor[OffsetDateTime]()
|
|
|
|
|
timeGoType = reflect.TypeFor[time.Time]()
|
|
|
|
|
durationType = reflect.TypeFor[time.Duration]()
|
|
|
|
|
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
2026-09-20 22:15:38 +02:00
|
|
|
marshalerIfaceType = reflect.TypeFor[Marshaler]()
|
2026-08-19 09:47:00 +02:00
|
|
|
)
|
|
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
// The encoder's per-type flags mirror the decoder's cache: an interface value
|
|
|
|
|
// is built only where the cache says the assertion can succeed. The bits name
|
|
|
|
|
// the receiver the method is found on.
|
|
|
|
|
const (
|
|
|
|
|
encFlagMarshaler uint8 = 1 << iota
|
|
|
|
|
encFlagAddrMarshaler
|
|
|
|
|
encFlagTextMarshaler
|
|
|
|
|
encFlagAddrTextMarshaler
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// encTypeFlagCache holds one flag entry per value type the encoder walks,
|
|
|
|
|
// immutable once published, the same trade-off structSchemaCache makes. The
|
|
|
|
|
// hint below re-points at these published entries, so a hot lookup allocates
|
|
|
|
|
// nothing.
|
|
|
|
|
var encTypeFlagCache sync.Map // reflect.Type -> *flagHintEntry
|
|
|
|
|
|
|
|
|
|
// encTypeFlagHint is the encoder's monomorphic hint, for the same reason the
|
|
|
|
|
// decoder's one exists: an encode walk repeats one type across consecutive
|
|
|
|
|
// fields and elements, and the hint answers without a cache probe.
|
|
|
|
|
var encTypeFlagHint atomic.Pointer[flagHintEntry]
|
|
|
|
|
|
|
|
|
|
func encTypeFlags(t reflect.Type) uint8 {
|
|
|
|
|
if e := encTypeFlagHint.Load(); e != nil && e.typ == t {
|
|
|
|
|
return e.flags
|
|
|
|
|
}
|
|
|
|
|
if v, ok := encTypeFlagCache.Load(t); ok {
|
|
|
|
|
entry := v.(*flagHintEntry)
|
|
|
|
|
encTypeFlagHint.Store(entry)
|
|
|
|
|
return entry.flags
|
|
|
|
|
}
|
|
|
|
|
var f uint8
|
|
|
|
|
if t.Implements(marshalerIfaceType) {
|
|
|
|
|
f |= encFlagMarshaler
|
|
|
|
|
}
|
|
|
|
|
pt := reflect.PointerTo(t)
|
|
|
|
|
if pt.Implements(marshalerIfaceType) {
|
|
|
|
|
f |= encFlagAddrMarshaler
|
|
|
|
|
}
|
|
|
|
|
// The date-time types are excluded from the text path for the reason
|
|
|
|
|
// textValue records; no caller reaches textMarshalerOf for them.
|
|
|
|
|
if !isDateTimeType(t) {
|
|
|
|
|
if t.Implements(textMarshalerType) {
|
|
|
|
|
f |= encFlagTextMarshaler
|
|
|
|
|
}
|
|
|
|
|
if pt.Implements(textMarshalerType) {
|
|
|
|
|
f |= encFlagAddrTextMarshaler
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
actual, _ := encTypeFlagCache.LoadOrStore(t, &flagHintEntry{t, f})
|
|
|
|
|
published := actual.(*flagHintEntry)
|
|
|
|
|
encTypeFlagHint.Store(published)
|
|
|
|
|
return published.flags
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 12:18:18 +02:00
|
|
|
// inlineLimit is the column past which an inline table is written across
|
|
|
|
|
// lines. TOML 1.1 lets an inline table carry newlines and a trailing comma, so
|
|
|
|
|
// a long one stays readable instead of running off the line.
|
|
|
|
|
const inlineLimit = 100
|
|
|
|
|
|
|
|
|
|
// noInlineBreak is the limit a measuring encoder carries, high enough that the
|
|
|
|
|
// form it renders is always the single-line one.
|
|
|
|
|
const noInlineBreak = 1 << 30
|
|
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
// encoderBufRetention is the largest output buffer put back into the pool.
|
|
|
|
|
// The cap is the pool's memory rule: repeated marshals of a document within it
|
|
|
|
|
// reuse one buffer and keep the heap flat, while a document larger than the
|
|
|
|
|
// cap pins nothing per processor once written.
|
|
|
|
|
const encoderBufRetention = 1 << 20
|
|
|
|
|
|
|
|
|
|
// encoderBufPool holds output buffers between Marshal calls.
|
|
|
|
|
var encoderBufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
|
|
|
|
|
|
|
|
|
|
// getEncoderBuf takes a cleared output buffer from the pool.
|
|
|
|
|
func getEncoderBuf() *bytes.Buffer {
|
|
|
|
|
b := encoderBufPool.Get().(*bytes.Buffer)
|
|
|
|
|
b.Reset()
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
// encoder produces a TOML document from a Go value via a small intermediate
|
|
|
|
|
// representation that preserves the order in which fields were declared.
|
|
|
|
|
type encoder struct {
|
2026-09-20 22:15:38 +02:00
|
|
|
buf *bytes.Buffer
|
2026-08-19 09:47:00 +02:00
|
|
|
ctx context.Context
|
|
|
|
|
opts Encoder
|
2026-09-19 12:18:18 +02:00
|
|
|
|
|
|
|
|
// inlineDepth is the nesting level inside inline tables, which decides
|
|
|
|
|
// their indentation.
|
|
|
|
|
inlineDepth int
|
|
|
|
|
|
|
|
|
|
// limit is the column at which an inline table is broken; only a
|
|
|
|
|
// measuring encoder raises it.
|
|
|
|
|
limit int
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
func newEncoder() *encoder {
|
|
|
|
|
e := &encoder{limit: inlineLimit}
|
|
|
|
|
e.buf = getEncoderBuf()
|
|
|
|
|
return e
|
|
|
|
|
}
|
2026-09-19 12:18:18 +02:00
|
|
|
|
|
|
|
|
// flat returns an encoder that measures a value by rendering it on one line,
|
|
|
|
|
// so a caller can decide which form to write before writing it.
|
|
|
|
|
func (e *encoder) flat() *encoder {
|
2026-09-20 22:15:38 +02:00
|
|
|
f := &encoder{ctx: e.ctx, opts: e.opts, limit: noInlineBreak}
|
|
|
|
|
f.buf = getEncoderBuf()
|
|
|
|
|
return f
|
2026-09-19 12:18:18 +02:00
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
// release returns the encoder's output buffer to the pool, within the
|
|
|
|
|
// retention cap. It is safe to call twice; the buffer travels only once.
|
|
|
|
|
func (e *encoder) release() {
|
|
|
|
|
if e.buf == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
b := e.buf
|
|
|
|
|
e.buf = nil
|
|
|
|
|
if b.Cap() <= encoderBufRetention {
|
|
|
|
|
encoderBufPool.Put(b)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
|
2026-09-19 12:18:18 +02:00
|
|
|
// column reports how many bytes the current line already holds, so a form can
|
|
|
|
|
// be measured against the limit before it is written.
|
|
|
|
|
func (e *encoder) column() int {
|
|
|
|
|
if i := bytes.LastIndexByte(e.buf.Bytes(), '\n'); i >= 0 {
|
|
|
|
|
return e.buf.Len() - i - 1
|
|
|
|
|
}
|
|
|
|
|
return e.buf.Len()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
func (e *encoder) checkCtx() error {
|
|
|
|
|
if e.ctx == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return e.ctx.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encode converts v into a TOML document. v must be a struct or a
|
|
|
|
|
// map[string]V (or a non-nil pointer to one).
|
|
|
|
|
func (e *encoder) encode(v any) error {
|
|
|
|
|
if err := e.checkCtx(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-09-20 10:40:57 +02:00
|
|
|
switch x := v.(type) {
|
|
|
|
|
case *Document:
|
|
|
|
|
if x == nil {
|
|
|
|
|
return fmt.Errorf("interpres: cannot marshal nil value")
|
|
|
|
|
}
|
2026-09-22 01:22:46 +02:00
|
|
|
return e.writeDocument(x)
|
2026-09-20 10:40:57 +02:00
|
|
|
case Document:
|
2026-09-22 01:22:46 +02:00
|
|
|
if x.root == nil {
|
|
|
|
|
return fmt.Errorf("interpres: cannot marshal nil value")
|
|
|
|
|
}
|
|
|
|
|
return e.writeDocument(&x)
|
2026-09-22 00:15:17 +02:00
|
|
|
case OrderedMap:
|
|
|
|
|
return e.encodeOrderedMap(&x)
|
|
|
|
|
case *OrderedMap:
|
|
|
|
|
if x == nil {
|
|
|
|
|
return fmt.Errorf("interpres: cannot marshal nil value")
|
|
|
|
|
}
|
|
|
|
|
return e.encodeOrderedMap(x)
|
2026-09-20 10:40:57 +02:00
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
rv := reflect.ValueOf(v)
|
|
|
|
|
if !rv.IsValid() {
|
|
|
|
|
return fmt.Errorf("interpres: cannot marshal nil value")
|
|
|
|
|
}
|
|
|
|
|
if rv.Kind() == reflect.Pointer {
|
|
|
|
|
if rv.IsNil() {
|
|
|
|
|
return fmt.Errorf("interpres: cannot marshal nil pointer")
|
|
|
|
|
}
|
|
|
|
|
rv = rv.Elem()
|
|
|
|
|
}
|
|
|
|
|
doc := &tomlDoc{ctx: e.ctx, opts: e.opts}
|
2026-09-20 22:15:38 +02:00
|
|
|
root := encPath{}
|
2026-08-19 09:47:00 +02:00
|
|
|
switch rv.Kind() {
|
|
|
|
|
case reflect.Struct:
|
2026-09-20 22:15:38 +02:00
|
|
|
if err := buildStructDoc(rv, doc, root); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
case reflect.Map:
|
2026-09-20 22:15:38 +02:00
|
|
|
if err := buildMapDoc(rv, doc, root); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("interpres: top-level value must be a struct or map[string]V, got %s", rv.Type())
|
|
|
|
|
}
|
|
|
|
|
return e.emitDoc(doc, nil)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:15:17 +02:00
|
|
|
// encodeOrderedMap emits an OrderedMap as a document, its keys in the order
|
|
|
|
|
// they were set, which is the reason the type exists.
|
|
|
|
|
func (e *encoder) encodeOrderedMap(om *OrderedMap) error {
|
|
|
|
|
if err := e.checkCtx(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
doc := &tomlDoc{ctx: e.ctx, opts: e.opts}
|
|
|
|
|
if err := buildOrderedDoc(om, doc, encPath{}); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return e.emitDoc(doc, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildOrderedDoc adds the entries of om to doc in the order its keys were
|
|
|
|
|
// set. Nested OrderedMaps recurse; every other value goes through addField,
|
|
|
|
|
// which resolves Marshaler, text and the ordinary kinds the same way it does
|
|
|
|
|
// for a struct field.
|
|
|
|
|
func buildOrderedDoc(om *OrderedMap, doc *tomlDoc, path encPath) error {
|
|
|
|
|
if cap(doc.entries) == 0 {
|
|
|
|
|
doc.entries = make([]entry, 0, om.Len())
|
|
|
|
|
}
|
|
|
|
|
for _, key := range om.Keys() {
|
|
|
|
|
if err := doc.checkCtx(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
val := om.values[key]
|
|
|
|
|
switch x := val.(type) {
|
|
|
|
|
case OrderedMap:
|
2026-09-22 00:21:46 +02:00
|
|
|
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
|
|
|
|
if atDepthLimit(sub.depth) {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: path.key(key).segments(), Err: errDepthLimit()}
|
2026-09-22 00:21:46 +02:00
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
if err := buildOrderedDoc(&x, sub, path.key(key)); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
doc.addTable(key, sub)
|
|
|
|
|
continue
|
|
|
|
|
case *OrderedMap:
|
|
|
|
|
if x == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-22 00:21:46 +02:00
|
|
|
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
|
|
|
|
if atDepthLimit(sub.depth) {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: path.key(key).segments(), Err: errDepthLimit()}
|
2026-09-22 00:21:46 +02:00
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
if err := buildOrderedDoc(x, sub, path.key(key)); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
doc.addTable(key, sub)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
rv := reflect.ValueOf(val)
|
|
|
|
|
if !rv.IsValid() {
|
|
|
|
|
// A nil value has no TOML form, the rule nil pointer fields follow.
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
if err := addField(doc, key, rv, path, false); err != nil {
|
2026-09-22 00:15:17 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
// --- intermediate representation -----------------------------------------
|
|
|
|
|
|
|
|
|
|
// entryKind discriminates the three forms an entry in a tomlDoc may take.
|
|
|
|
|
type entryKind int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
entryScalar entryKind = iota
|
|
|
|
|
entryTable
|
|
|
|
|
entryArray
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// entry is one binding in a tomlDoc. entries live in a single slice in the
|
2026-09-20 22:15:38 +02:00
|
|
|
// order they were added; emission walks that order directly, either as it is
|
2026-09-22 01:09:00 +02:00
|
|
|
// (Encoder with LayoutKindDeclaration) or in kind-grouped passes over the same
|
2026-09-20 22:15:38 +02:00
|
|
|
// slice (the default).
|
2026-08-19 09:47:00 +02:00
|
|
|
type entry struct {
|
|
|
|
|
kind entryKind
|
|
|
|
|
key string
|
2026-09-22 01:46:26 +02:00
|
|
|
val any // entryScalar, boxed
|
|
|
|
|
rv reflect.Value // entryScalar, the value the walk kept unboxed
|
|
|
|
|
doc *tomlDoc // entryTable
|
2026-08-19 09:47:00 +02:00
|
|
|
docs []*tomlDoc
|
2026-09-20 22:15:38 +02:00
|
|
|
|
2026-09-22 00:28:52 +02:00
|
|
|
// inline forces a table entry to emit as `key = {…}`; it is set by the
|
|
|
|
|
// `,inline` tag option. An array of tables keeps the header form.
|
|
|
|
|
inline bool
|
|
|
|
|
|
2026-09-22 00:44:23 +02:00
|
|
|
// comments are the comment lines written above this entry's line or
|
|
|
|
|
// header, which the `comment=` tag option carries when
|
|
|
|
|
// Encoder.EmitFieldComments is on.
|
|
|
|
|
comments []string
|
|
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
// emitted records that the grouped emission wrote this table inline, so
|
|
|
|
|
// the header pass that follows skips it. The representation is built
|
|
|
|
|
// fresh per Marshal call.
|
|
|
|
|
emitted bool
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:21:46 +02:00
|
|
|
// tomlDoc holds the entries of one TOML table in declaration order. depth is
|
|
|
|
|
// the nesting level the table sits at, which bounds the walk: cyclic data
|
|
|
|
|
// would recurse forever, and hits the limit instead of the stack.
|
2026-08-19 09:47:00 +02:00
|
|
|
type tomlDoc struct {
|
|
|
|
|
entries []entry
|
|
|
|
|
ctx context.Context // inherited from encoder; nil-safe
|
|
|
|
|
opts Encoder // inherited from encoder; options drive emit-time behaviour
|
2026-09-22 00:21:46 +02:00
|
|
|
depth int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// maxEncodeDepth bounds the encoding walk. It matches the parser's nesting
|
|
|
|
|
// limit: a document that deep cannot be written by this encoder either, and
|
|
|
|
|
// a value that nests that far without being an array or a table is cyclic.
|
|
|
|
|
const maxEncodeDepth = maxNestingDepth
|
|
|
|
|
|
|
|
|
|
// atDepthLimit reports whether a table nested depth levels is past the walk's
|
|
|
|
|
// limit.
|
|
|
|
|
func atDepthLimit(depth int) bool {
|
|
|
|
|
return depth > maxEncodeDepth
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// errDepthLimit is the failure a cyclic value walks into.
|
|
|
|
|
func errDepthLimit() error {
|
|
|
|
|
return fmt.Errorf("value nests deeper than the limit of %d levels; the value may be cyclic", maxEncodeDepth)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d *tomlDoc) checkCtx() error {
|
|
|
|
|
if d.ctx == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return d.ctx.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d *tomlDoc) addScalar(key string, val any) {
|
|
|
|
|
d.entries = append(d.entries, entry{kind: entryScalar, key: key, val: val})
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 01:46:26 +02:00
|
|
|
// addScalarReflect adds a scalar whose value stays in its reflect cell until
|
|
|
|
|
// emission writes it, the path that keeps plain scalars from being boxed.
|
|
|
|
|
func (d *tomlDoc) addScalarReflect(key string, v reflect.Value) {
|
|
|
|
|
d.entries = append(d.entries, entry{kind: entryScalar, key: key, rv: v})
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:28:52 +02:00
|
|
|
func (d *tomlDoc) addTable(key string, sub *tomlDoc) *entry {
|
|
|
|
|
e := entry{kind: entryTable, key: key, doc: sub}
|
|
|
|
|
d.entries = append(d.entries, e)
|
|
|
|
|
return &d.entries[len(d.entries)-1]
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d *tomlDoc) addArray(key string, subs []*tomlDoc) {
|
|
|
|
|
d.entries = append(d.entries, entry{kind: entryArray, key: key, docs: subs})
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
// --- error paths -----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// encPath names a value the way an error message needs it, "server.ports[2]",
|
|
|
|
|
// without building the string unless an error actually asks for one. A zero
|
|
|
|
|
// encPath is the document root. The chain is stack-allocated: a segment holds
|
|
|
|
|
// a pointer to its parent's frame-local value, and the rendered string exists
|
|
|
|
|
// only while an error is being built.
|
|
|
|
|
type encPath struct {
|
|
|
|
|
parent *encPath
|
|
|
|
|
name string
|
|
|
|
|
index int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// key returns the child path of a named key.
|
|
|
|
|
func (p *encPath) key(name string) encPath {
|
|
|
|
|
return encPath{parent: p, name: name, index: -1}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// elem returns the child path of an array element by index.
|
|
|
|
|
func (p *encPath) elem(i int) encPath {
|
|
|
|
|
return encPath{parent: p, index: i}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:34:48 +02:00
|
|
|
// String renders the path root first, in the notation Path.String uses.
|
2026-09-20 22:15:38 +02:00
|
|
|
func (p encPath) String() string {
|
2026-09-22 00:34:48 +02:00
|
|
|
return Path(p.segments()).String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// segments returns the path's segments, root first: a key contributes its
|
|
|
|
|
// name, an array element its bracketed index.
|
|
|
|
|
func (p encPath) segments() []string {
|
2026-09-20 22:15:38 +02:00
|
|
|
var parts []string
|
|
|
|
|
for s := &p; s != nil; s = s.parent {
|
|
|
|
|
switch {
|
|
|
|
|
case s.name == "" && s.parent == nil:
|
|
|
|
|
// The root: nothing to write.
|
|
|
|
|
case s.name == "":
|
|
|
|
|
parts = append(parts, "["+strconv.Itoa(s.index)+"]")
|
|
|
|
|
default:
|
|
|
|
|
parts = append(parts, s.name)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-22 00:34:48 +02:00
|
|
|
slices.Reverse(parts)
|
|
|
|
|
return parts
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- reflection walk: struct ---------------------------------------------
|
|
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
func buildStructDoc(v reflect.Value, doc *tomlDoc, path encPath) error {
|
2026-09-22 00:21:46 +02:00
|
|
|
return walkStructDoc(v, doc, path, nil, cachedStructSchema(v.Type()), doc.depth)
|
2026-09-17 23:08:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// walkStructDoc emits the fields of v into doc. prefix is v's index path from
|
|
|
|
|
// the struct whose schema resolves key conflicts; an embedded struct is walked
|
|
|
|
|
// with the outer schema and a longer prefix, so every leaf competes under the
|
|
|
|
|
// decoder's rule: the shallower field wins, the later declaration at equal
|
|
|
|
|
// depth. A field another field shadows is skipped, because emitting both
|
2026-09-22 00:21:46 +02:00
|
|
|
// would duplicate the key and the output would not re-parse. depth is the
|
|
|
|
|
// nesting the walk has reached, which an embedded struct raises; a cycle
|
|
|
|
|
// through embedded pointers ends at the limit instead of the stack.
|
|
|
|
|
func walkStructDoc(v reflect.Value, doc *tomlDoc, path encPath, prefix []int, schema structSchema, depth int) error {
|
|
|
|
|
if atDepthLimit(depth) {
|
|
|
|
|
return fmt.Errorf("interpres: %s: %w", path.String(), errDepthLimit())
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
t := v.Type()
|
2026-09-20 22:15:38 +02:00
|
|
|
if cap(doc.entries) == 0 {
|
|
|
|
|
doc.entries = make([]entry, 0, t.NumField())
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
for i := range t.NumField() {
|
|
|
|
|
if i%ctxCheckInterval == 0 {
|
|
|
|
|
if err := doc.checkCtx(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
f := t.Field(i)
|
|
|
|
|
if f.PkgPath != "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
fpath := append(append([]int{}, prefix...), i)
|
2026-08-19 09:47:00 +02:00
|
|
|
if f.Anonymous {
|
|
|
|
|
tag, _ := f.Tag.Lookup("toml")
|
|
|
|
|
if tag == "-" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if tag == "" {
|
|
|
|
|
fv := followPtr(v.Field(i))
|
|
|
|
|
if !fv.IsValid() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
switch fv.Kind() {
|
|
|
|
|
case reflect.Struct:
|
|
|
|
|
if isScalarStruct(fv.Type()) {
|
|
|
|
|
name := strings.ToLower(f.Name)
|
2026-09-20 22:15:38 +02:00
|
|
|
if !schema.ownsKey(name, fpath) {
|
2026-09-17 23:08:42 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
doc.addScalar(name, fv.Interface())
|
2026-08-19 09:47:00 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2026-09-22 00:21:46 +02:00
|
|
|
if err := walkStructDoc(fv, doc, path, fpath, schema, depth+1); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
case reflect.Map:
|
2026-09-20 22:15:38 +02:00
|
|
|
if err := buildMapDoc(fv, doc, path); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
name := fieldName(f)
|
|
|
|
|
if name == "-" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
if !schema.ownsKey(strings.ToLower(name), fpath) {
|
2026-09-17 23:08:42 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2026-09-17 19:57:23 +02:00
|
|
|
if fieldOmitted(f, v.Field(i)) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-22 00:44:23 +02:00
|
|
|
before := len(doc.entries)
|
2026-09-22 00:28:52 +02:00
|
|
|
if err := addField(doc, name, v.Field(i), path, tagHasOption(f.Tag.Get("toml"), "inline")); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
2026-09-22 00:44:23 +02:00
|
|
|
// The comment a `comment=` tag option carries lands on the entry the
|
|
|
|
|
// field emitted, when the option to print field comments is on.
|
|
|
|
|
if len(doc.entries) > before && doc.opts.emitFieldComments {
|
|
|
|
|
if text := tagComment(f.Tag.Get("toml")); text != "" {
|
|
|
|
|
doc.entries[len(doc.entries)-1].comments = strings.Split(text, "\n")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 19:57:23 +02:00
|
|
|
// isZeroer mirrors encoding/json's omitzero: a type that knows its own zero
|
|
|
|
|
// state decides through that method before reflection is consulted.
|
|
|
|
|
type isZeroer interface{ IsZero() bool }
|
|
|
|
|
|
2026-09-22 00:28:52 +02:00
|
|
|
// tagOptions returns the option part of a `toml` tag, the part after the
|
|
|
|
|
// first comma.
|
|
|
|
|
func tagOptions(tag string) string {
|
|
|
|
|
_, opts, _ := strings.Cut(tag, ",")
|
|
|
|
|
return opts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tagHasOption reports whether want is one of the tag's comma-separated
|
|
|
|
|
// options.
|
|
|
|
|
func tagHasOption(tag, want string) bool {
|
|
|
|
|
opts := tagOptions(tag)
|
|
|
|
|
for opts != "" {
|
|
|
|
|
var opt string
|
|
|
|
|
opt, opts, _ = strings.Cut(opts, ",")
|
|
|
|
|
if opt == want {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:44:23 +02:00
|
|
|
// tagComment returns the text a `comment=` tag option carries, without the
|
|
|
|
|
// option name. An unset comment comes back empty.
|
|
|
|
|
func tagComment(tag string) string {
|
|
|
|
|
opts := tagOptions(tag)
|
|
|
|
|
for opts != "" {
|
|
|
|
|
var opt string
|
|
|
|
|
opt, opts, _ = strings.Cut(opts, ",")
|
|
|
|
|
if text, ok := strings.CutPrefix(opt, "comment="); ok {
|
|
|
|
|
return text
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 19:57:23 +02:00
|
|
|
// fieldOmitted reports whether the field's tag options drop it from the
|
|
|
|
|
// output: omitzero skips the zero value of the field's type, omitempty skips
|
2026-09-22 00:28:52 +02:00
|
|
|
// an empty value in the encoding/json sense, an empty string, a zero number,
|
|
|
|
|
// false, a nil pointer or interface, and an empty slice, array or map. The
|
|
|
|
|
// decoder ignores both options; they shape emission only.
|
2026-09-17 19:57:23 +02:00
|
|
|
func fieldOmitted(f reflect.StructField, v reflect.Value) bool {
|
|
|
|
|
tag, ok := f.Tag.Lookup("toml")
|
|
|
|
|
if !ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
if tagHasOption(tag, "omitzero") && isZeroValue(v) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if tagHasOption(tag, "omitempty") {
|
|
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.Slice, reflect.Array, reflect.Map:
|
|
|
|
|
if v.Len() == 0 {
|
2026-09-17 19:57:23 +02:00
|
|
|
return true
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
case reflect.String:
|
|
|
|
|
if v.Len() == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
case reflect.Bool:
|
|
|
|
|
if !v.Bool() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
|
|
|
if v.Int() == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
|
if v.Uint() == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
|
|
|
if v.Float() == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
case reflect.Pointer, reflect.Interface:
|
|
|
|
|
if v.IsNil() {
|
|
|
|
|
return true
|
2026-09-17 19:57:23 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isZeroValue(v reflect.Value) bool {
|
|
|
|
|
if v.CanInterface() {
|
|
|
|
|
if z, ok := v.Interface().(isZeroer); ok {
|
|
|
|
|
return z.IsZero()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return v.IsZero()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
// fieldName returns the TOML key for a struct field, honouring the `toml`
|
|
|
|
|
// tag (name or `-`) and falling back to a lower-cased field name.
|
|
|
|
|
func fieldName(f reflect.StructField) string {
|
|
|
|
|
if tag, ok := f.Tag.Lookup("toml"); ok {
|
|
|
|
|
name, _, _ := strings.Cut(tag, ",")
|
|
|
|
|
if name == "-" {
|
|
|
|
|
return "-"
|
|
|
|
|
}
|
|
|
|
|
if name != "" {
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return strings.ToLower(f.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- reflection walk: map ------------------------------------------------
|
|
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
func buildMapDoc(v reflect.Value, doc *tomlDoc, path encPath) error {
|
2026-09-22 00:21:46 +02:00
|
|
|
if atDepthLimit(doc.depth) {
|
|
|
|
|
return fmt.Errorf("interpres: %s: %w", path.String(), errDepthLimit())
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
if v.Type().Key().Kind() != reflect.String {
|
|
|
|
|
return fmt.Errorf("interpres: map key must be string, got %s", v.Type().Key())
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
if cap(doc.entries) == 0 {
|
|
|
|
|
doc.entries = make([]entry, 0, v.Len())
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
keys := v.MapKeys()
|
|
|
|
|
slices.SortFunc(keys, func(a, b reflect.Value) int {
|
|
|
|
|
return strings.Compare(a.String(), b.String())
|
|
|
|
|
})
|
|
|
|
|
for i, k := range keys {
|
|
|
|
|
if i%ctxCheckInterval == 0 {
|
|
|
|
|
if err := doc.checkCtx(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
if err := addField(doc, k.String(), v.MapIndex(k), path, false); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- reflection walk: field dispatch -------------------------------------
|
|
|
|
|
|
2026-09-17 23:11:10 +02:00
|
|
|
// errNilMarshalTOML reports a Marshaler whose method returned a nil value
|
|
|
|
|
// with no error. nil has no TOML representation, so dropping the field
|
|
|
|
|
// silently or panicking on the invalid reflect.Value would both hide the
|
|
|
|
|
// contract violation.
|
|
|
|
|
var errNilMarshalTOML = errors.New("MarshalTOML returned a nil value")
|
|
|
|
|
|
2026-09-22 00:28:52 +02:00
|
|
|
// addField adds one value under name, the shape addField picks deciding
|
|
|
|
|
// whether it is a scalar line, a sub-table or an array. forceInline marks a
|
|
|
|
|
// table-valued field carrying the `,inline` tag option: it emits as
|
|
|
|
|
// `key = {…}` instead of a header section.
|
|
|
|
|
func addField(doc *tomlDoc, name string, v reflect.Value, path encPath, forceInline bool) error {
|
2026-09-19 19:36:24 +02:00
|
|
|
if m, ok := marshalerOf(v); ok {
|
|
|
|
|
mv, err := m.MarshalTOML()
|
|
|
|
|
if err != nil {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: path.key(name).segments(), Err: err}
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
if mv == nil {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: path.key(name).segments(), Err: errNilMarshalTOML}
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
v = reflect.ValueOf(mv)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-19 02:41:09 +02:00
|
|
|
// A type that renders itself as text becomes a TOML string, whether it is
|
|
|
|
|
// a scalar kind or a struct.
|
|
|
|
|
s, isText, err := textValue(v)
|
|
|
|
|
if err != nil {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: path.key(name).segments(), Err: err}
|
2026-09-19 02:41:09 +02:00
|
|
|
}
|
|
|
|
|
if isText {
|
2026-09-20 22:15:38 +02:00
|
|
|
doc.addScalar(name, s)
|
|
|
|
|
return nil
|
2026-09-19 02:41:09 +02:00
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
v = followPtr(v)
|
|
|
|
|
if !v.IsValid() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if v.Kind() == reflect.Interface {
|
|
|
|
|
if v.IsNil() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
v = v.Elem()
|
|
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
if v.Type() == orderedMapType {
|
|
|
|
|
om := v.Interface().(OrderedMap)
|
2026-09-22 00:21:46 +02:00
|
|
|
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
|
|
|
|
if atDepthLimit(sub.depth) {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: path.key(name).segments(), Err: errDepthLimit()}
|
2026-09-22 00:21:46 +02:00
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
if err := buildOrderedDoc(&om, sub, path.key(name)); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
doc.addTable(name, sub).inline = forceInline
|
2026-09-22 00:15:17 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.Struct:
|
|
|
|
|
if isScalarStruct(v.Type()) {
|
2026-09-20 22:15:38 +02:00
|
|
|
doc.addScalar(name, v.Interface())
|
|
|
|
|
return nil
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
return addSubTable(doc, name, v, path, forceInline)
|
2026-08-19 09:47:00 +02:00
|
|
|
case reflect.Map:
|
2026-09-22 00:28:52 +02:00
|
|
|
return addSubTable(doc, name, v, path, forceInline)
|
2026-08-19 09:47:00 +02:00
|
|
|
case reflect.Slice, reflect.Array:
|
2026-09-22 00:28:52 +02:00
|
|
|
return addArrayValue(doc, name, v, path, forceInline)
|
2026-08-19 09:47:00 +02:00
|
|
|
default:
|
2026-09-22 01:46:26 +02:00
|
|
|
// A plain scalar stays in its reflect cell until emission, which
|
|
|
|
|
// writes it without the boxing Interface() would cost. The kinds the
|
|
|
|
|
// boxed rules rewrite (duration, Number, the date-time structs) take
|
|
|
|
|
// the boxed path as before.
|
|
|
|
|
if t := v.Type(); t != durationType && t != numberType {
|
|
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16,
|
|
|
|
|
reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16,
|
|
|
|
|
reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
|
|
|
|
|
doc.addScalarReflect(name, v)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
val, err := normaliseValue(v)
|
|
|
|
|
if err != nil {
|
2026-09-20 22:15:38 +02:00
|
|
|
return fmt.Errorf("interpres: %s: %w", path.key(name), err)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
doc.addScalar(name, val)
|
|
|
|
|
return nil
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:28:52 +02:00
|
|
|
func addSubTable(doc *tomlDoc, name string, v reflect.Value, path encPath, forceInline bool) error {
|
2026-09-22 00:21:46 +02:00
|
|
|
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
|
|
|
|
if atDepthLimit(sub.depth) {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: path.key(name).segments(), Err: errDepthLimit()}
|
2026-09-22 00:21:46 +02:00
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.Struct:
|
2026-09-20 22:15:38 +02:00
|
|
|
if err := buildStructDoc(v, sub, path.key(name)); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
case reflect.Map:
|
2026-09-20 22:15:38 +02:00
|
|
|
if err := buildMapDoc(v, sub, path.key(name)); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
doc.addTable(name, sub).inline = forceInline
|
2026-08-19 09:47:00 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:28:52 +02:00
|
|
|
func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath, forceInline bool) error {
|
2026-08-19 09:47:00 +02:00
|
|
|
if v.Kind() == reflect.Slice && v.IsNil() {
|
|
|
|
|
// A nil slice has no explicit representation in TOML, so it is skipped.
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
n := v.Len()
|
|
|
|
|
if n == 0 {
|
|
|
|
|
if isTableElementType(v.Type().Elem()) {
|
|
|
|
|
// Empty array of tables has no valid TOML form, so it is skipped.
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if doc.opts.omitEmptyArrays {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
doc.addScalar(name, []any{})
|
|
|
|
|
return nil
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
// The element paths hang off this one; they render only when an error
|
|
|
|
|
// names them.
|
|
|
|
|
apath := path.key(name)
|
|
|
|
|
|
2026-09-22 01:46:26 +02:00
|
|
|
// The per-element resolution runs only where the array could still take
|
|
|
|
|
// the [[header]] form: an interface element can never (its [[form]] would
|
|
|
|
|
// re-parse as []map[string]any), and a plain scalar element can only when
|
|
|
|
|
// a Marshaler rewrites it into a table. Everywhere else the array is a
|
|
|
|
|
// value array and the elements are written straight from the slice.
|
|
|
|
|
elem := v.Type().Elem()
|
|
|
|
|
maybeTables := true
|
|
|
|
|
if elem.Kind() == reflect.Interface {
|
|
|
|
|
maybeTables = false
|
|
|
|
|
} else {
|
|
|
|
|
switch elem.Kind() {
|
|
|
|
|
case reflect.Struct, reflect.Map, reflect.Pointer:
|
|
|
|
|
default:
|
|
|
|
|
f := encTypeFlags(elem)
|
|
|
|
|
if f&encFlagMarshaler == 0 && f&encFlagAddrMarshaler == 0 {
|
|
|
|
|
maybeTables = false
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-22 01:46:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A typed array of plain scalars takes the direct emission path: no
|
|
|
|
|
// per-element slice, no boxing, and no error the boxed rules could raise.
|
|
|
|
|
// Everything wider, []any first among it, keeps the boxed path, because
|
|
|
|
|
// its element errors carry the element's path.
|
|
|
|
|
directValueArray := false
|
|
|
|
|
switch elem.Kind() {
|
|
|
|
|
case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16,
|
|
|
|
|
reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
|
|
|
|
|
f := encTypeFlags(elem)
|
|
|
|
|
if elem != durationType && elem != numberType &&
|
|
|
|
|
f&(encFlagMarshaler|encFlagAddrMarshaler|encFlagTextMarshaler|encFlagAddrTextMarshaler) == 0 {
|
|
|
|
|
directValueArray = true
|
|
|
|
|
}
|
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
|
f := encTypeFlags(elem)
|
|
|
|
|
if f&(encFlagMarshaler|encFlagAddrMarshaler|encFlagTextMarshaler|encFlagAddrTextMarshaler) == 0 {
|
|
|
|
|
directValueArray = true
|
|
|
|
|
// The one error the boxed path raises for these is the int64
|
|
|
|
|
// overflow, checked here so the direct path keeps the contract.
|
|
|
|
|
for i := range n {
|
|
|
|
|
if u := v.Index(i).Uint(); u > math.MaxInt64 {
|
|
|
|
|
return &EncodeError{Path: apath.elem(i).segments(), Err: fmt.Errorf("unsigned value %d overflows int64", u)}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var elems []reflect.Value
|
|
|
|
|
if maybeTables {
|
|
|
|
|
// Every element is resolved through MarshalTOML first, so an element
|
|
|
|
|
// that renders itself as a scalar, a table or a value array is
|
|
|
|
|
// classified by what it produces rather than by its Go kind, and its
|
|
|
|
|
// method runs once.
|
|
|
|
|
elems = make([]reflect.Value, n)
|
|
|
|
|
for i := range n {
|
|
|
|
|
if i%ctxCheckInterval == 0 {
|
|
|
|
|
if err := doc.checkCtx(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ev, err := resolveElement(v.Index(i), apath.elem(i))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
elems[i] = ev
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 19:49:50 +02:00
|
|
|
// An array keeps the [[header]] form only when every element is a table.
|
|
|
|
|
// TOML lets one array mix tables with scalars, and that mix renders as a
|
|
|
|
|
// value array with the table elements written inline.
|
2026-09-22 01:46:26 +02:00
|
|
|
allTables := maybeTables
|
|
|
|
|
if allTables {
|
|
|
|
|
for _, ev := range elems {
|
|
|
|
|
if !ev.IsValid() || !isTableElementValue(ev) {
|
|
|
|
|
allTables = false
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// A []any of tables is what Parse produces for a value array of inline
|
|
|
|
|
// tables; the [[header]] form would re-parse as []map[string]any and so
|
|
|
|
|
// change the value's Go type across a round-trip. The header form is
|
|
|
|
|
// reserved for typed table slices.
|
|
|
|
|
if v.Type().Elem().Kind() == reflect.Interface {
|
2026-09-17 19:49:50 +02:00
|
|
|
allTables = false
|
|
|
|
|
}
|
2026-09-17 23:10:35 +02:00
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
// A `,inline` tag on an array of tables asks for a form that would change
|
|
|
|
|
// the value's Go type on re-parse, so the error is the honest answer.
|
|
|
|
|
if forceInline && allTables {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: path.key(name).segments(), Err: errors.New("an array of tables has no inline form")}
|
2026-09-22 00:28:52 +02:00
|
|
|
}
|
2026-09-17 19:49:50 +02:00
|
|
|
if allTables {
|
2026-08-19 09:47:00 +02:00
|
|
|
subs := make([]*tomlDoc, n)
|
2026-09-19 19:36:24 +02:00
|
|
|
for i, ev := range elems {
|
2026-08-19 09:47:00 +02:00
|
|
|
if i%ctxCheckInterval == 0 {
|
|
|
|
|
if err := doc.checkCtx(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-22 00:21:46 +02:00
|
|
|
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
|
|
|
|
|
if atDepthLimit(sub.depth) {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: apath.elem(i).segments(), Err: errDepthLimit()}
|
2026-09-22 00:21:46 +02:00
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
switch {
|
|
|
|
|
case ev.Type() == orderedMapType:
|
|
|
|
|
om := ev.Interface().(OrderedMap)
|
|
|
|
|
if err := buildOrderedDoc(&om, sub, apath.elem(i)); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
case ev.Kind() == reflect.Struct:
|
2026-08-19 09:47:00 +02:00
|
|
|
if isScalarStruct(ev.Type()) {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: apath.elem(i).segments(), Err: errors.New("heterogeneous array contains scalar")}
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
if err := buildStructDoc(ev, sub, apath.elem(i)); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
case ev.Kind() == reflect.Map:
|
2026-09-20 22:15:38 +02:00
|
|
|
if err := buildMapDoc(ev, sub, apath.elem(i)); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
default:
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: apath.elem(i).segments(), Err: errors.New("heterogeneous array, expected table")}
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
subs[i] = sub
|
|
|
|
|
}
|
|
|
|
|
doc.addArray(name, subs)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 01:46:26 +02:00
|
|
|
if directValueArray {
|
|
|
|
|
doc.addScalarReflect(name, v)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 19:49:50 +02:00
|
|
|
// Value array. Table elements normalise to map[string]any and the emitter
|
|
|
|
|
// writes them as inline tables.
|
2026-08-19 09:47:00 +02:00
|
|
|
items := make([]any, n)
|
2026-09-22 01:46:26 +02:00
|
|
|
for i := range n {
|
2026-08-19 09:47:00 +02:00
|
|
|
if i%ctxCheckInterval == 0 {
|
|
|
|
|
if err := doc.checkCtx(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-22 01:46:26 +02:00
|
|
|
ev := v.Index(i)
|
|
|
|
|
if elems != nil {
|
|
|
|
|
ev = elems[i]
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
val, err := normaliseValue(ev)
|
|
|
|
|
if err != nil {
|
2026-09-22 00:34:48 +02:00
|
|
|
return &EncodeError{Path: apath.elem(i).segments(), Err: err}
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
items[i] = val
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
doc.addScalar(name, items)
|
|
|
|
|
return nil
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-22 01:46:26 +02:00
|
|
|
// writeArrayValue writes a value array from its reflect value, its elements
|
|
|
|
|
// written one by one, each falling back to the boxed path only where the
|
|
|
|
|
// boxed rules rewrite it.
|
|
|
|
|
func (e *encoder) writeArrayValue(v reflect.Value, depth int) error {
|
|
|
|
|
if atDepthLimit(depth) {
|
|
|
|
|
return errDepthLimit()
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteByte('[')
|
|
|
|
|
for i := range v.Len() {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
e.buf.WriteString(", ")
|
|
|
|
|
}
|
|
|
|
|
if err := e.writeArrayElem(v.Index(i), depth+1); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteByte(']')
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeArrayElem writes one element of a value array. The kinds the boxed
|
|
|
|
|
// rules rewrite are handed to normaliseValue and the boxed writer; the rest
|
|
|
|
|
// write directly, including nested arrays and inline tables.
|
|
|
|
|
func (e *encoder) writeArrayElem(v reflect.Value, depth int) error {
|
|
|
|
|
if v.Kind() == reflect.Interface {
|
|
|
|
|
if v.IsNil() {
|
|
|
|
|
return fmt.Errorf("interpres: cannot encode nil value")
|
|
|
|
|
}
|
|
|
|
|
v = v.Elem()
|
|
|
|
|
}
|
|
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
|
|
|
return e.writeArrayValue(v, depth)
|
|
|
|
|
case reflect.Map:
|
|
|
|
|
return e.writeInlineMapFromReflect(v, depth)
|
|
|
|
|
}
|
|
|
|
|
if _, isMarshaler := marshalerOf(v); isMarshaler {
|
|
|
|
|
return e.writeNormalisedElem(v, depth)
|
|
|
|
|
}
|
|
|
|
|
if _, isText, err := textValue(v); err != nil || isText {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return e.writeNormalisedElem(v, depth)
|
|
|
|
|
}
|
|
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16,
|
|
|
|
|
reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16,
|
|
|
|
|
reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
|
|
|
|
|
if t := v.Type(); t != durationType && t != numberType {
|
|
|
|
|
return e.writeScalarValue(v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return e.writeNormalisedElem(v, depth)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeNormalisedElem normalises one element through the boxed rules and
|
|
|
|
|
// writes the result.
|
|
|
|
|
func (e *encoder) writeNormalisedElem(v reflect.Value, depth int) error {
|
|
|
|
|
val, err := normaliseValueAt(v, depth)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return e.writeValue(val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeInlineMapFromReflect renders a map from its reflect value as an inline
|
|
|
|
|
// table, the shape the boxed path gives the table elements of a value array:
|
|
|
|
|
// sorted keys, single line when it fits, across lines when it does not.
|
|
|
|
|
func (e *encoder) writeInlineMapFromReflect(v reflect.Value, depth int) error {
|
|
|
|
|
if e.limit >= noInlineBreak {
|
|
|
|
|
return e.writeInlineMapFlatReflect(v, depth)
|
|
|
|
|
}
|
|
|
|
|
flat := e.flat()
|
|
|
|
|
err := flat.writeInlineMapFlatReflect(v, depth)
|
|
|
|
|
if err != nil {
|
|
|
|
|
flat.release()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
fits := e.column()+flat.buf.Len() <= e.limit
|
|
|
|
|
if fits {
|
|
|
|
|
e.buf.Write(flat.buf.Bytes())
|
|
|
|
|
}
|
|
|
|
|
flat.release()
|
|
|
|
|
if fits {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return e.writeInlineMapMultilineReflect(v, depth)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeInlineMapFlatReflect renders the single-line form.
|
|
|
|
|
func (e *encoder) writeInlineMapFlatReflect(v reflect.Value, depth int) error {
|
|
|
|
|
if v.Type().Key().Kind() != reflect.String {
|
|
|
|
|
return fmt.Errorf("map key must be string, got %s", v.Type().Key())
|
|
|
|
|
}
|
|
|
|
|
keys := make([]string, 0, v.Len())
|
|
|
|
|
for _, k := range v.MapKeys() {
|
|
|
|
|
keys = append(keys, k.String())
|
|
|
|
|
}
|
|
|
|
|
slices.Sort(keys)
|
|
|
|
|
e.buf.WriteByte('{')
|
|
|
|
|
for i, k := range keys {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
e.buf.WriteString(", ")
|
|
|
|
|
}
|
|
|
|
|
if err := e.writeKey(k); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(" = ")
|
|
|
|
|
if err := e.writeArrayElem(v.MapIndex(reflect.ValueOf(k)), depth+1); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteByte('}')
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeInlineMapMultilineReflect renders the across-lines form.
|
|
|
|
|
func (e *encoder) writeInlineMapMultilineReflect(v reflect.Value, depth int) error {
|
|
|
|
|
keys := make([]string, 0, v.Len())
|
|
|
|
|
for _, k := range v.MapKeys() {
|
|
|
|
|
keys = append(keys, k.String())
|
|
|
|
|
}
|
|
|
|
|
slices.Sort(keys)
|
|
|
|
|
e.buf.WriteString("{\n")
|
|
|
|
|
e.inlineDepth++
|
|
|
|
|
for _, k := range keys {
|
|
|
|
|
e.writeInlineIndent()
|
|
|
|
|
if err := e.writeKey(k); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(" = ")
|
|
|
|
|
if err := e.writeArrayElem(v.MapIndex(reflect.ValueOf(k)), depth+1); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(",\n")
|
|
|
|
|
}
|
|
|
|
|
e.inlineDepth--
|
|
|
|
|
e.writeInlineIndent()
|
|
|
|
|
e.buf.WriteByte('}')
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 19:36:24 +02:00
|
|
|
// marshalerOf finds the Marshaler a value carries: on the value itself, or on
|
|
|
|
|
// its address, so a pointer-receiver MarshalTOML is found on an addressable
|
|
|
|
|
// struct field or slice element, exactly as textMarshalerOf finds MarshalText.
|
2026-09-20 22:15:38 +02:00
|
|
|
// The lookup consults the per-type flag cache, so an interface value is built
|
|
|
|
|
// only where the assertion can succeed; an interface-typed value is asked
|
|
|
|
|
// dynamically, because what it holds may implement the interface when the
|
|
|
|
|
// interface type itself does not.
|
2026-09-19 19:36:24 +02:00
|
|
|
func marshalerOf(v reflect.Value) (Marshaler, bool) {
|
|
|
|
|
if !v.CanInterface() {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
if v.Kind() == reflect.Interface {
|
|
|
|
|
m, ok := v.Interface().(Marshaler)
|
|
|
|
|
return m, ok
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
f := encTypeFlags(v.Type())
|
|
|
|
|
if f&encFlagMarshaler != 0 {
|
|
|
|
|
m, ok := v.Interface().(Marshaler)
|
|
|
|
|
return m, ok
|
|
|
|
|
}
|
|
|
|
|
if f&encFlagAddrMarshaler != 0 && v.CanAddr() {
|
|
|
|
|
m, ok := v.Addr().Interface().(Marshaler)
|
|
|
|
|
return m, ok
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resolveElement looks through pointers and runs MarshalTOML, so an array
|
|
|
|
|
// element is classified by what its method produces. path names the element,
|
|
|
|
|
// for the errors the method can raise.
|
2026-09-20 22:15:38 +02:00
|
|
|
func resolveElement(v reflect.Value, path encPath) (reflect.Value, error) {
|
2026-09-19 19:36:24 +02:00
|
|
|
ev := followPtr(v)
|
|
|
|
|
if !ev.IsValid() {
|
2026-09-22 00:34:48 +02:00
|
|
|
return ev, &EncodeError{Path: path.segments(), Err: errors.New("nil element")}
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
m, ok := marshalerOf(ev)
|
|
|
|
|
if !ok {
|
|
|
|
|
return ev, nil
|
|
|
|
|
}
|
|
|
|
|
mv, err := m.MarshalTOML()
|
|
|
|
|
if err != nil {
|
2026-09-22 00:34:48 +02:00
|
|
|
return reflect.Value{}, &EncodeError{Path: path.segments(), Err: err}
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
if mv == nil {
|
2026-09-22 00:34:48 +02:00
|
|
|
return reflect.Value{}, &EncodeError{Path: path.segments(), Err: errNilMarshalTOML}
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
ev = followPtr(reflect.ValueOf(mv))
|
|
|
|
|
if !ev.IsValid() {
|
2026-09-22 00:34:48 +02:00
|
|
|
return ev, &EncodeError{Path: path.segments(), Err: errors.New("nil element")}
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
return ev, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
// normaliseValue converts a reflect.Value into one of the canonical scalar or
|
|
|
|
|
// nested-array representations the emitter understands. Slices and arrays are
|
|
|
|
|
// recursively normalised so that nested arrays (e.g. [][]int) work.
|
|
|
|
|
func normaliseValue(v reflect.Value) (any, error) {
|
2026-09-22 00:21:46 +02:00
|
|
|
return normaliseValueAt(v, 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func normaliseValueAt(v reflect.Value, depth int) (any, error) {
|
|
|
|
|
if atDepthLimit(depth) {
|
|
|
|
|
return nil, errDepthLimit()
|
|
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
if !v.IsValid() {
|
|
|
|
|
return nil, fmt.Errorf("cannot encode nil value")
|
|
|
|
|
}
|
2026-09-17 19:49:50 +02:00
|
|
|
// Map and slice elements arrive wrapped in interface{}; look through them.
|
|
|
|
|
for v.Kind() == reflect.Interface && !v.IsNil() {
|
|
|
|
|
v = v.Elem()
|
|
|
|
|
}
|
|
|
|
|
if v.Kind() == reflect.Interface {
|
|
|
|
|
return nil, fmt.Errorf("cannot encode nil value")
|
|
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
if m, ok := marshalerOf(v); ok {
|
|
|
|
|
mv, err := m.MarshalTOML()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
if mv == nil {
|
|
|
|
|
return nil, errNilMarshalTOML
|
|
|
|
|
}
|
|
|
|
|
// The result is normalised like any other value, so a method may return
|
|
|
|
|
// a duration, a defined type or another Marshaler. A result of the
|
|
|
|
|
// receiver's own type is written as it is, because recursing into it
|
|
|
|
|
// would never end.
|
|
|
|
|
if rv := reflect.ValueOf(mv); rv.Type() != v.Type() {
|
2026-09-22 00:21:46 +02:00
|
|
|
return normaliseValueAt(rv, depth+1)
|
2026-09-19 19:36:24 +02:00
|
|
|
}
|
|
|
|
|
return mv, nil
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-17 19:49:50 +02:00
|
|
|
// The datetime structs are TOML scalars; the emitter renders each of them.
|
2026-09-19 19:36:24 +02:00
|
|
|
if isScalarStruct(v.Type()) {
|
2026-09-17 19:49:50 +02:00
|
|
|
return v.Interface(), nil
|
|
|
|
|
}
|
2026-09-19 02:41:09 +02:00
|
|
|
// TOML has no duration type, so a duration goes out in its canonical Go
|
|
|
|
|
// form, the shape it comes back in.
|
|
|
|
|
if v.Type() == durationType {
|
|
|
|
|
return time.Duration(v.Int()).String(), nil
|
|
|
|
|
}
|
2026-09-21 23:49:39 +02:00
|
|
|
// A Number goes out as the literal it carries, the one string-kind value
|
|
|
|
|
// that is not written quoted.
|
|
|
|
|
if v.Type() == numberType {
|
|
|
|
|
return Number(v.String()), nil
|
|
|
|
|
}
|
2026-09-22 00:15:17 +02:00
|
|
|
// An OrderedMap in a value array has no header form, so it renders as an
|
|
|
|
|
// ordinary inline table, whose keys come out sorted.
|
|
|
|
|
if v.Type() == orderedMapType {
|
|
|
|
|
om := v.Interface().(OrderedMap)
|
|
|
|
|
out := make(map[string]any, om.Len())
|
|
|
|
|
for _, k := range om.Keys() {
|
2026-09-22 00:21:46 +02:00
|
|
|
val, err := normaliseValueAt(reflect.ValueOf(om.values[k]), depth+1)
|
2026-09-22 00:15:17 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("[%s]: %w", k, err)
|
|
|
|
|
}
|
|
|
|
|
out[k] = val
|
|
|
|
|
}
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
2026-09-19 02:41:09 +02:00
|
|
|
// A type that renders itself as text becomes a TOML string, scalar kinds
|
|
|
|
|
// and structs alike.
|
|
|
|
|
s, isText, err := textValue(v)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if isText {
|
|
|
|
|
return s, nil
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.String:
|
|
|
|
|
return v.String(), nil
|
|
|
|
|
case reflect.Bool:
|
|
|
|
|
return v.Bool(), nil
|
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
|
|
|
return v.Int(), nil
|
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
|
u := v.Uint()
|
|
|
|
|
if u > math.MaxInt64 {
|
|
|
|
|
return nil, fmt.Errorf("unsigned value %d overflows int64", u)
|
|
|
|
|
}
|
|
|
|
|
return int64(u), nil
|
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
|
|
|
return v.Float(), nil
|
2026-09-17 19:49:50 +02:00
|
|
|
case reflect.Map:
|
|
|
|
|
// A table nested in a value array has no header form, so it renders
|
|
|
|
|
// inline; the keys normalise to strings for the emitter.
|
|
|
|
|
if v.Type().Key().Kind() != reflect.String {
|
|
|
|
|
return nil, fmt.Errorf("map key must be string, got %s", v.Type().Key())
|
|
|
|
|
}
|
|
|
|
|
out := make(map[string]any, v.Len())
|
|
|
|
|
for _, k := range v.MapKeys() {
|
2026-09-22 00:21:46 +02:00
|
|
|
val, err := normaliseValueAt(v.MapIndex(k), depth+1)
|
2026-09-17 19:49:50 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("[%s]: %w", k.String(), err)
|
|
|
|
|
}
|
|
|
|
|
out[k.String()] = val
|
|
|
|
|
}
|
|
|
|
|
return out, nil
|
2026-08-19 09:47:00 +02:00
|
|
|
case reflect.Slice, reflect.Array:
|
|
|
|
|
items := make([]any, v.Len())
|
|
|
|
|
for i := range v.Len() {
|
2026-09-22 00:21:46 +02:00
|
|
|
val, err := normaliseValueAt(v.Index(i), depth+1)
|
2026-08-19 09:47:00 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("[%d]: %w", i, err)
|
|
|
|
|
}
|
|
|
|
|
items[i] = val
|
|
|
|
|
}
|
|
|
|
|
return items, nil
|
|
|
|
|
}
|
|
|
|
|
if !v.IsValid() {
|
|
|
|
|
return nil, fmt.Errorf("invalid value")
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("cannot encode %s", v.Type())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// followPtr unwraps pointer and interface layers. Returns a zero Value if a
|
|
|
|
|
// nil pointer or nil interface is encountered.
|
|
|
|
|
func followPtr(v reflect.Value) reflect.Value {
|
|
|
|
|
for {
|
|
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.Pointer, reflect.Interface:
|
|
|
|
|
if v.IsNil() {
|
|
|
|
|
return reflect.Value{}
|
|
|
|
|
}
|
|
|
|
|
v = v.Elem()
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isScalarStruct reports whether t is a struct type that the encoder treats
|
2026-09-19 19:36:24 +02:00
|
|
|
// as a TOML scalar: time.Time and the four date-time wrappers.
|
2026-08-19 09:47:00 +02:00
|
|
|
func isScalarStruct(t reflect.Type) bool {
|
2026-09-19 19:36:24 +02:00
|
|
|
return t == timeGoType || t == offsetDateTimeType || isLocalDateType(t)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isLocalDateType(t reflect.Type) bool {
|
|
|
|
|
return t == localDateTimeType || t == localDateType || t == localTimeType
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 19:36:24 +02:00
|
|
|
// isDateTimeType reports whether t is one of the date-time types, which the
|
|
|
|
|
// encoder emits as bare atoms. Pointers are looked through. The types carry
|
|
|
|
|
// time.Time's text methods through an embedded field, and the atom form takes
|
|
|
|
|
// precedence over them.
|
2026-09-19 02:41:09 +02:00
|
|
|
func isDateTimeType(t reflect.Type) bool {
|
|
|
|
|
for t.Kind() == reflect.Pointer {
|
|
|
|
|
t = t.Elem()
|
|
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
return isScalarStruct(t)
|
2026-09-19 02:41:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isTextMarshalerType reports whether t or *t implements
|
|
|
|
|
// encoding.TextMarshaler. An array of such values stays a value array, because
|
|
|
|
|
// each element's TOML form is a string.
|
|
|
|
|
func isTextMarshalerType(t reflect.Type) bool {
|
|
|
|
|
if isDateTimeType(t) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return t.Implements(textMarshalerType) || reflect.PointerTo(t).Implements(textMarshalerType)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// textValue returns the string a value renders itself as through
|
|
|
|
|
// encoding.TextMarshaler. The date-time types are excluded, because their
|
|
|
|
|
// embedded time.Time would answer with an RFC 3339 string where the TOML form
|
|
|
|
|
// is a bare timestamp. A nil pointer offers no text and is left to the ordinary
|
|
|
|
|
// nil handling, which omits the field.
|
|
|
|
|
func textValue(v reflect.Value) (string, bool, error) {
|
|
|
|
|
for v.Kind() == reflect.Interface && !v.IsNil() {
|
|
|
|
|
v = v.Elem()
|
|
|
|
|
}
|
|
|
|
|
if !v.IsValid() || isDateTimeType(v.Type()) {
|
|
|
|
|
return "", false, nil
|
|
|
|
|
}
|
|
|
|
|
if v.Kind() == reflect.Pointer && v.IsNil() {
|
|
|
|
|
return "", false, nil
|
|
|
|
|
}
|
|
|
|
|
m, ok := textMarshalerOf(v)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", false, nil
|
|
|
|
|
}
|
|
|
|
|
b, err := m.MarshalText()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", true, err
|
|
|
|
|
}
|
|
|
|
|
return string(b), true, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 22:15:38 +02:00
|
|
|
// textMarshalerOf finds the encoding.TextMarshaler for v through the same
|
|
|
|
|
// flag cache: on the value itself, or on its address, so a pointer-receiver
|
|
|
|
|
// MarshalText is found on an addressable struct field.
|
2026-09-19 02:41:09 +02:00
|
|
|
func textMarshalerOf(v reflect.Value) (encoding.TextMarshaler, bool) {
|
|
|
|
|
if !v.CanInterface() {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
if v.Kind() == reflect.Interface {
|
|
|
|
|
m, ok := v.Interface().(encoding.TextMarshaler)
|
|
|
|
|
return m, ok
|
2026-09-19 02:41:09 +02:00
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
f := encTypeFlags(v.Type())
|
|
|
|
|
if f&encFlagTextMarshaler != 0 {
|
|
|
|
|
m, ok := v.Interface().(encoding.TextMarshaler)
|
|
|
|
|
return m, ok
|
|
|
|
|
}
|
|
|
|
|
if f&encFlagAddrTextMarshaler != 0 && v.CanAddr() {
|
|
|
|
|
m, ok := v.Addr().Interface().(encoding.TextMarshaler)
|
|
|
|
|
return m, ok
|
2026-09-19 02:41:09 +02:00
|
|
|
}
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
func isTableElementType(t reflect.Type) bool {
|
|
|
|
|
switch t.Kind() {
|
|
|
|
|
case reflect.Struct:
|
2026-09-19 02:41:09 +02:00
|
|
|
return !isScalarStruct(t) && !isTextMarshalerType(t)
|
2026-08-19 09:47:00 +02:00
|
|
|
case reflect.Map:
|
|
|
|
|
return t.Key().Kind() == reflect.String
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isTableElementValue(v reflect.Value) bool {
|
|
|
|
|
v = followPtr(v)
|
|
|
|
|
if !v.IsValid() {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return isTableElementType(v.Type())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- emission ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// writeBlankLine writes a single newline before a table or array-of-tables
|
|
|
|
|
// header so the output has a blank line between sections, unless the buffer
|
|
|
|
|
// is empty (i.e. this is the very first header).
|
|
|
|
|
func (e *encoder) writeBlankLine() {
|
|
|
|
|
if e.buf.Len() == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteByte('\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error {
|
2026-09-22 01:09:00 +02:00
|
|
|
if e.opts.layout == LayoutKindGrouped {
|
2026-09-20 22:15:38 +02:00
|
|
|
// Scalars first, then inline sub-tables as value lines, then the
|
|
|
|
|
// remaining tables as headers, then arrays of tables. Each pass walks
|
|
|
|
|
// the entries in place; grouping copies of them cost the encoder a
|
|
|
|
|
// third of its allocations for nothing.
|
|
|
|
|
for i := range doc.entries {
|
|
|
|
|
kv := &doc.entries[i]
|
|
|
|
|
if kv.kind != entryScalar {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-22 00:44:23 +02:00
|
|
|
if err := e.writeKV(kv); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 12:18:30 +02:00
|
|
|
// An inlined sub-table is a value line, so it has to precede every
|
|
|
|
|
// header of this document: a line written after a [header] would be
|
|
|
|
|
// read back as part of that table.
|
2026-09-20 22:15:38 +02:00
|
|
|
for i := range doc.entries {
|
|
|
|
|
t := &doc.entries[i]
|
|
|
|
|
if t.kind != entryTable {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
inlined, err := e.writeInlineSubTableIfSmall(t)
|
2026-09-19 12:18:30 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
t.emitted = inlined
|
2026-09-19 12:18:30 +02:00
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
for i := range doc.entries {
|
|
|
|
|
t := &doc.entries[i]
|
|
|
|
|
if t.kind != entryTable || t.emitted {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
path := append(append([]string{}, prefix...), t.key)
|
|
|
|
|
e.writeBlankLine()
|
2026-09-22 00:44:23 +02:00
|
|
|
e.writeComments(t.comments)
|
2026-08-19 09:47:00 +02:00
|
|
|
e.buf.WriteByte('[')
|
2026-09-17 23:06:59 +02:00
|
|
|
if err := e.writeKeyPath(path); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
e.buf.WriteString("]\n")
|
|
|
|
|
if err := e.emitDoc(t.doc, path); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
for i := range doc.entries {
|
|
|
|
|
a := &doc.entries[i]
|
|
|
|
|
if a.kind != entryArray {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
path := append(append([]string{}, prefix...), a.key)
|
2026-09-22 00:44:23 +02:00
|
|
|
for j, sub := range a.docs {
|
2026-08-19 09:47:00 +02:00
|
|
|
e.writeBlankLine()
|
2026-09-22 00:44:23 +02:00
|
|
|
if j == 0 {
|
|
|
|
|
e.writeComments(a.comments)
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
e.buf.WriteString("[[")
|
2026-09-17 23:06:59 +02:00
|
|
|
if err := e.writeKeyPath(path); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
e.buf.WriteString("]]\n")
|
|
|
|
|
if err := e.emitDoc(sub, path); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Preserve declaration order. Scalars and table/array headers may now
|
|
|
|
|
// interleave, which means each table/array header must include only its
|
|
|
|
|
// own section content; the emitter still writes sub-documents as separate
|
|
|
|
|
// nested blocks, so a "" sub-keyed scalar following a header for the same
|
|
|
|
|
// section is impossible in practice (struct fields are visited in order).
|
|
|
|
|
for _, ent := range doc.entries {
|
|
|
|
|
switch ent.kind {
|
|
|
|
|
case entryScalar:
|
2026-09-22 00:44:23 +02:00
|
|
|
if err := e.writeKV(&ent); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
case entryTable:
|
2026-09-22 00:28:52 +02:00
|
|
|
inlined, err := e.writeInlineSubTableIfSmall(&ent)
|
2026-09-19 12:18:30 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if inlined {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
path := append(append([]string{}, prefix...), ent.key)
|
|
|
|
|
e.writeBlankLine()
|
2026-09-22 00:44:23 +02:00
|
|
|
e.writeComments(ent.comments)
|
2026-08-19 09:47:00 +02:00
|
|
|
e.buf.WriteByte('[')
|
2026-09-17 23:06:59 +02:00
|
|
|
if err := e.writeKeyPath(path); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
e.buf.WriteString("]\n")
|
|
|
|
|
if err := e.emitDoc(ent.doc, path); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
case entryArray:
|
|
|
|
|
path := append(append([]string{}, prefix...), ent.key)
|
2026-09-22 00:44:23 +02:00
|
|
|
for j, sub := range ent.docs {
|
2026-08-19 09:47:00 +02:00
|
|
|
e.writeBlankLine()
|
2026-09-22 00:44:23 +02:00
|
|
|
if j == 0 {
|
|
|
|
|
e.writeComments(ent.comments)
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
e.buf.WriteString("[[")
|
2026-09-17 23:06:59 +02:00
|
|
|
if err := e.writeKeyPath(path); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
e.buf.WriteString("]]\n")
|
|
|
|
|
if err := e.emitDoc(sub, path); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:44:23 +02:00
|
|
|
// writeComments writes comment lines above an entry, each prefixed with the
|
|
|
|
|
// "# " marker the parser strips on the way in.
|
|
|
|
|
func (e *encoder) writeComments(lines []string) {
|
|
|
|
|
for _, line := range lines {
|
|
|
|
|
e.buf.WriteString("# ")
|
|
|
|
|
e.buf.WriteString(line)
|
|
|
|
|
e.buf.WriteByte('\n')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *encoder) writeKV(ent *entry) error {
|
|
|
|
|
e.writeComments(ent.comments)
|
|
|
|
|
if err := e.writeKey(ent.key); err != nil {
|
2026-09-17 23:06:59 +02:00
|
|
|
return err
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
e.buf.WriteString(" = ")
|
2026-09-22 01:46:26 +02:00
|
|
|
if err := e.writeEntryValue(ent); err != nil {
|
2026-08-19 09:47:00 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteByte('\n')
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 01:46:26 +02:00
|
|
|
// writeEntryValue writes a scalar entry's value: from the reflect cell the
|
|
|
|
|
// walk kept when it can be written unboxed, from the boxed value otherwise.
|
|
|
|
|
func (e *encoder) writeEntryValue(ent *entry) error {
|
|
|
|
|
if ent.rv.IsValid() {
|
|
|
|
|
switch ent.rv.Kind() {
|
|
|
|
|
case reflect.Slice, reflect.Array:
|
|
|
|
|
return e.writeArrayValue(ent.rv, 0)
|
|
|
|
|
}
|
|
|
|
|
return e.writeScalarValue(ent.rv)
|
|
|
|
|
}
|
|
|
|
|
return e.writeValue(ent.val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeScalarValue writes a scalar from its reflect value, boxing only the
|
|
|
|
|
// kinds the boxed writer handles specially.
|
|
|
|
|
func (e *encoder) writeScalarValue(v reflect.Value) error {
|
|
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.String:
|
|
|
|
|
return e.writeStringVal(v.String())
|
|
|
|
|
case reflect.Bool:
|
|
|
|
|
e.buf.WriteString(strconv.FormatBool(v.Bool()))
|
|
|
|
|
return nil
|
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
|
|
|
e.buf.WriteString(strconv.FormatInt(v.Int(), 10))
|
|
|
|
|
return nil
|
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
|
u := v.Uint()
|
|
|
|
|
if u > math.MaxInt64 {
|
|
|
|
|
return fmt.Errorf("unsigned value %d overflows int64", u)
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(strconv.FormatUint(u, 10))
|
|
|
|
|
return nil
|
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
|
|
|
return e.writeFloat(v.Float())
|
|
|
|
|
}
|
|
|
|
|
return e.writeValue(v.Interface())
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 23:06:59 +02:00
|
|
|
func (e *encoder) writeKeyPath(path []string) error {
|
2026-08-19 09:47:00 +02:00
|
|
|
for i, p := range path {
|
|
|
|
|
if i > 0 {
|
2026-09-17 23:06:59 +02:00
|
|
|
e.buf.WriteByte('.')
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-17 23:06:59 +02:00
|
|
|
if err := e.writeKey(p); err != nil {
|
|
|
|
|
return err
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-17 23:06:59 +02:00
|
|
|
return nil
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-17 23:06:59 +02:00
|
|
|
// writeKey writes one key, bare when it qualifies and quoted otherwise. A key
|
|
|
|
|
// that is not valid UTF-8 is an error; writing it anyway would emit corrupt
|
|
|
|
|
// TOML, because the quoted form has no representation for it.
|
|
|
|
|
func (e *encoder) writeKey(key string) error {
|
2026-08-19 09:47:00 +02:00
|
|
|
if isBareKey(key) {
|
|
|
|
|
e.buf.WriteString(key)
|
2026-09-17 23:06:59 +02:00
|
|
|
return nil
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-17 23:06:59 +02:00
|
|
|
if !utf8.ValidString(key) {
|
|
|
|
|
return fmt.Errorf("interpres: key %q is not valid UTF-8", key)
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
return writeQuotedString(e.buf, key)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeQuotedString writes s as a TOML basic string (double-quoted) to buf.
|
2026-09-20 22:15:38 +02:00
|
|
|
// Returns an error only if s is not valid UTF-8.
|
2026-08-19 09:47:00 +02:00
|
|
|
func writeQuotedString(buf *bytes.Buffer, s string) error {
|
|
|
|
|
if !utf8.ValidString(s) {
|
|
|
|
|
return fmt.Errorf("interpres: string is not valid UTF-8")
|
|
|
|
|
}
|
|
|
|
|
buf.WriteByte('"')
|
|
|
|
|
for i := 0; i < len(s); {
|
2026-09-20 22:15:38 +02:00
|
|
|
// Write the run of plain characters in one go; the scan stops at a
|
|
|
|
|
// byte that needs an escape, a control character, or a multi-byte rune.
|
|
|
|
|
j := i
|
|
|
|
|
for j < len(s) {
|
|
|
|
|
c := s[j]
|
|
|
|
|
if c < 0x20 || c == 0x7f || c == '"' || c == '\\' || c >= 0x80 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
j++
|
|
|
|
|
}
|
|
|
|
|
if j > i {
|
|
|
|
|
buf.WriteString(s[i:j])
|
|
|
|
|
i = j
|
|
|
|
|
}
|
|
|
|
|
if i >= len(s) {
|
|
|
|
|
break
|
|
|
|
|
}
|
2026-08-19 09:47:00 +02:00
|
|
|
r, size := utf8.DecodeRuneInString(s[i:])
|
|
|
|
|
if r == utf8.RuneError && size == 1 {
|
2026-09-20 22:15:38 +02:00
|
|
|
buf.WriteString("\\ufffd")
|
2026-08-19 09:47:00 +02:00
|
|
|
i++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
i += size
|
|
|
|
|
writeEscapedRune(buf, r)
|
|
|
|
|
}
|
|
|
|
|
buf.WriteByte('"')
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeEscapedRune writes a single rune to buf, escaping it as required by
|
|
|
|
|
// TOML basic-string rules.
|
|
|
|
|
func writeEscapedRune(buf *bytes.Buffer, r rune) {
|
|
|
|
|
switch r {
|
|
|
|
|
case '\\':
|
|
|
|
|
buf.WriteString(`\\`)
|
|
|
|
|
case '"':
|
|
|
|
|
buf.WriteString(`\"`)
|
|
|
|
|
case '\b':
|
|
|
|
|
buf.WriteString(`\b`)
|
|
|
|
|
case '\t':
|
|
|
|
|
buf.WriteString(`\t`)
|
|
|
|
|
case '\n':
|
|
|
|
|
buf.WriteString(`\n`)
|
|
|
|
|
case '\f':
|
|
|
|
|
buf.WriteString(`\f`)
|
|
|
|
|
case '\r':
|
|
|
|
|
buf.WriteString(`\r`)
|
|
|
|
|
default:
|
|
|
|
|
if r < 0x20 || r == 0x7f {
|
2026-09-20 22:15:38 +02:00
|
|
|
fmt.Fprintf(buf, "\\u%04X", r)
|
2026-08-19 09:47:00 +02:00
|
|
|
} else {
|
|
|
|
|
buf.WriteRune(r)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isBareKey(s string) bool {
|
|
|
|
|
if s == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for i := range len(s) {
|
|
|
|
|
c := s[i]
|
|
|
|
|
if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-') {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *encoder) writeValue(val any) error {
|
|
|
|
|
switch v := val.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
return e.writeStringVal(v)
|
|
|
|
|
case bool:
|
|
|
|
|
e.buf.WriteString(strconv.FormatBool(v))
|
|
|
|
|
return nil
|
|
|
|
|
case int64:
|
|
|
|
|
e.buf.WriteString(strconv.FormatInt(v, 10))
|
|
|
|
|
return nil
|
2026-09-21 23:49:39 +02:00
|
|
|
case Number:
|
|
|
|
|
if _, err := decodeNumber(string(v)); err != nil {
|
|
|
|
|
return fmt.Errorf("interpres: %w", err)
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(string(v))
|
|
|
|
|
return nil
|
2026-08-19 09:47:00 +02:00
|
|
|
case float64:
|
|
|
|
|
return e.writeFloat(v)
|
|
|
|
|
case time.Time:
|
2026-09-19 12:18:18 +02:00
|
|
|
e.buf.WriteString(offsetString(v))
|
2026-08-19 09:47:00 +02:00
|
|
|
return nil
|
2026-09-19 19:36:24 +02:00
|
|
|
case OffsetDateTime:
|
|
|
|
|
e.buf.WriteString(v.String())
|
|
|
|
|
return nil
|
2026-08-19 09:47:00 +02:00
|
|
|
case LocalDateTime:
|
|
|
|
|
e.buf.WriteString(v.String())
|
|
|
|
|
return nil
|
|
|
|
|
case LocalDate:
|
|
|
|
|
e.buf.WriteString(v.String())
|
|
|
|
|
return nil
|
|
|
|
|
case LocalTime:
|
|
|
|
|
e.buf.WriteString(v.String())
|
|
|
|
|
return nil
|
|
|
|
|
case []any:
|
|
|
|
|
e.buf.WriteByte('[')
|
|
|
|
|
for i, item := range v {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
e.buf.WriteString(", ")
|
|
|
|
|
}
|
|
|
|
|
if err := e.writeValue(item); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteByte(']')
|
|
|
|
|
return nil
|
2026-09-17 19:49:50 +02:00
|
|
|
case map[string]any:
|
2026-09-19 12:18:18 +02:00
|
|
|
return e.writeInlineMap(v)
|
2026-08-19 09:47:00 +02:00
|
|
|
case nil:
|
|
|
|
|
return fmt.Errorf("interpres: cannot encode nil value")
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("interpres: cannot encode %T", val)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 12:18:18 +02:00
|
|
|
// writeInlineMap renders m as a TOML inline table, on one line when it fits
|
|
|
|
|
// there and across lines when it does not.
|
|
|
|
|
func (e *encoder) writeInlineMap(m map[string]any) error {
|
2026-09-20 22:15:38 +02:00
|
|
|
// An encoder that is itself measuring renders single-line by contract, so
|
|
|
|
|
// a nested inline table needs no measuring pass of its own.
|
|
|
|
|
if e.limit >= noInlineBreak {
|
|
|
|
|
return e.writeInlineMapFlat(m)
|
|
|
|
|
}
|
2026-09-19 12:18:18 +02:00
|
|
|
flat := e.flat()
|
2026-09-20 22:15:38 +02:00
|
|
|
err := flat.writeInlineMapFlat(m)
|
|
|
|
|
if err != nil {
|
|
|
|
|
flat.release()
|
2026-09-19 12:18:18 +02:00
|
|
|
return err
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
fits := e.column()+flat.buf.Len() <= e.limit
|
|
|
|
|
if fits {
|
2026-09-19 12:18:18 +02:00
|
|
|
e.buf.Write(flat.buf.Bytes())
|
2026-09-20 22:15:38 +02:00
|
|
|
}
|
|
|
|
|
flat.release()
|
|
|
|
|
if fits {
|
2026-09-19 12:18:18 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return e.writeInlineMapMultiline(m)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeInlineMapFlat renders m as a single-line inline table with sorted keys,
|
|
|
|
|
// the order buildMapDoc uses for header tables. It backs the table elements of
|
|
|
|
|
// a value array, where the [[header]] form is not available.
|
|
|
|
|
func (e *encoder) writeInlineMapFlat(m map[string]any) error {
|
2026-09-17 19:49:50 +02:00
|
|
|
keys := slices.Sorted(maps.Keys(m))
|
|
|
|
|
e.buf.WriteByte('{')
|
|
|
|
|
for i, k := range keys {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
e.buf.WriteString(", ")
|
|
|
|
|
}
|
2026-09-17 23:06:59 +02:00
|
|
|
if err := e.writeKey(k); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-09-17 19:49:50 +02:00
|
|
|
e.buf.WriteString(" = ")
|
|
|
|
|
if err := e.writeValue(m[k]); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteByte('}')
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 12:18:18 +02:00
|
|
|
// writeInlineMapMultiline renders m with one entry per line and a trailing
|
|
|
|
|
// comma, the form TOML 1.1 allows for an inline table too long for one line.
|
|
|
|
|
func (e *encoder) writeInlineMapMultiline(m map[string]any) error {
|
|
|
|
|
keys := slices.Sorted(maps.Keys(m))
|
|
|
|
|
e.buf.WriteString("{\n")
|
|
|
|
|
e.inlineDepth++
|
|
|
|
|
for _, k := range keys {
|
|
|
|
|
e.writeInlineIndent()
|
|
|
|
|
if err := e.writeKey(k); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(" = ")
|
|
|
|
|
if err := e.writeValue(m[k]); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(",\n")
|
|
|
|
|
}
|
|
|
|
|
e.inlineDepth--
|
|
|
|
|
e.writeInlineIndent()
|
|
|
|
|
e.buf.WriteByte('}')
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeInlineIndent writes one tab per inline-table nesting level.
|
|
|
|
|
func (e *encoder) writeInlineIndent() {
|
|
|
|
|
for range e.inlineDepth {
|
|
|
|
|
e.buf.WriteByte('\t')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 12:18:30 +02:00
|
|
|
// errInlineArrayOfTables reports an attempt to render an array of tables
|
|
|
|
|
// inline, which has no form that keeps the value's type.
|
|
|
|
|
var errInlineArrayOfTables = errors.New("interpres: an array of tables has no inline form")
|
|
|
|
|
|
|
|
|
|
// inlinableDoc reports whether doc can be written as an inline table without
|
|
|
|
|
// changing the type of any value: scalars, value arrays and further sub-tables
|
|
|
|
|
// are fine, while an array of tables is not, because its inline form would
|
|
|
|
|
// re-parse as a value array.
|
|
|
|
|
func inlinableDoc(doc *tomlDoc) bool {
|
|
|
|
|
for _, ent := range doc.entries {
|
|
|
|
|
switch ent.kind {
|
|
|
|
|
case entryArray:
|
|
|
|
|
return false
|
|
|
|
|
case entryTable:
|
|
|
|
|
if !inlinableDoc(ent.doc) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeInlineDocEntry writes one "key = value" binding of an inline table,
|
|
|
|
|
// without the separator that follows it.
|
|
|
|
|
func (e *encoder) writeInlineDocEntry(ent entry) error {
|
|
|
|
|
if err := e.writeKey(ent.key); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(" = ")
|
|
|
|
|
switch ent.kind {
|
|
|
|
|
case entryTable:
|
|
|
|
|
return e.writeInlineDoc(ent.doc)
|
|
|
|
|
case entryArray:
|
|
|
|
|
return errInlineArrayOfTables
|
|
|
|
|
default:
|
2026-09-22 01:46:26 +02:00
|
|
|
return e.writeEntryValue(&ent)
|
2026-09-19 12:18:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeInlineDoc renders doc as a single-line inline table in entry order, the
|
|
|
|
|
// order the fields were declared in.
|
|
|
|
|
func (e *encoder) writeInlineDoc(doc *tomlDoc) error {
|
|
|
|
|
e.buf.WriteByte('{')
|
|
|
|
|
for i, ent := range doc.entries {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
e.buf.WriteString(", ")
|
|
|
|
|
}
|
|
|
|
|
if err := e.writeInlineDocEntry(ent); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteByte('}')
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeInlineDocMultiline renders doc with one entry per line and a trailing
|
|
|
|
|
// comma, the form TOML 1.1 allows for an inline table too long for one line.
|
|
|
|
|
func (e *encoder) writeInlineDocMultiline(doc *tomlDoc) error {
|
|
|
|
|
e.buf.WriteString("{\n")
|
|
|
|
|
e.inlineDepth++
|
|
|
|
|
for _, ent := range doc.entries {
|
|
|
|
|
e.writeInlineIndent()
|
|
|
|
|
if err := e.writeInlineDocEntry(ent); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(",\n")
|
|
|
|
|
}
|
|
|
|
|
e.inlineDepth--
|
|
|
|
|
e.writeInlineIndent()
|
|
|
|
|
e.buf.WriteByte('}')
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeInlineSubTableIfSmall writes "key = {…}" for a sub-table whose
|
|
|
|
|
// single-line rendering fits the compact threshold, and reports whether it did
|
2026-09-22 00:28:52 +02:00
|
|
|
// so. An entry the `,inline` tag option marks is written regardless of the
|
|
|
|
|
// threshold. An array of tables is never inlined, because its inline form
|
|
|
|
|
// would re-parse as a value array and change the value's Go type; a forced
|
|
|
|
|
// inline of one is an error rather than a silent form change.
|
|
|
|
|
func (e *encoder) writeInlineSubTableIfSmall(t *entry) (bool, error) {
|
|
|
|
|
if !t.inline && (e.opts.inlineTablesAt <= 0 || !inlinableDoc(t.doc)) {
|
2026-09-19 12:18:30 +02:00
|
|
|
return false, nil
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
if !inlinableDoc(t.doc) {
|
|
|
|
|
return false, fmt.Errorf("interpres: field %q holds an array of tables and has no inline form", t.key)
|
|
|
|
|
}
|
2026-09-19 12:18:30 +02:00
|
|
|
flat := e.flat()
|
2026-09-22 00:28:52 +02:00
|
|
|
if err := flat.writeInlineDoc(t.doc); err != nil {
|
2026-09-20 22:15:38 +02:00
|
|
|
flat.release()
|
2026-09-19 12:18:30 +02:00
|
|
|
return false, err
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
if !t.inline && flat.buf.Len() > e.opts.inlineTablesAt {
|
2026-09-20 22:15:38 +02:00
|
|
|
flat.release()
|
2026-09-19 12:18:30 +02:00
|
|
|
return false, nil
|
|
|
|
|
}
|
2026-09-22 00:28:52 +02:00
|
|
|
if err := e.writeKey(t.key); err != nil {
|
2026-09-20 22:15:38 +02:00
|
|
|
flat.release()
|
2026-09-19 12:18:30 +02:00
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(" = ")
|
|
|
|
|
if e.column()+flat.buf.Len() <= e.limit {
|
|
|
|
|
e.buf.Write(flat.buf.Bytes())
|
2026-09-22 00:28:52 +02:00
|
|
|
} else if err := e.writeInlineDocMultiline(t.doc); err != nil {
|
2026-09-20 22:15:38 +02:00
|
|
|
flat.release()
|
2026-09-19 12:18:30 +02:00
|
|
|
return false, err
|
|
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
flat.release()
|
2026-09-19 12:18:30 +02:00
|
|
|
e.buf.WriteByte('\n')
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
func (e *encoder) writeStringVal(s string) error {
|
2026-09-17 23:06:18 +02:00
|
|
|
if e.opts.literalMultilineAt > 0 && strings.ContainsRune(s, '\n') &&
|
|
|
|
|
len(s) >= e.opts.literalMultilineAt && canBeLiteralMultiline(s) {
|
2026-09-20 22:15:38 +02:00
|
|
|
return writeLiteralMultilineString(e.buf, s)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-20 22:15:38 +02:00
|
|
|
return writeQuotedString(e.buf, s)
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-17 23:06:18 +02:00
|
|
|
// canBeLiteralMultiline reports whether s can be carried verbatim by the
|
|
|
|
|
// literal ”'...”' form: the form has no escapes, so a run of three single
|
|
|
|
|
// quotes would close the delimiter early, and control characters beyond tab,
|
|
|
|
|
// and a carriage return outside a CRLF pair, have no representation at all.
|
|
|
|
|
// Anything else falls back to the escaped basic string.
|
|
|
|
|
func canBeLiteralMultiline(s string) bool {
|
|
|
|
|
if strings.Contains(s, "'''") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for i := 0; i < len(s); {
|
|
|
|
|
r, size := utf8.DecodeRuneInString(s[i:])
|
|
|
|
|
switch {
|
|
|
|
|
case r == '\t' || r == '\n':
|
|
|
|
|
case r == '\r':
|
|
|
|
|
if !strings.HasPrefix(s[i+size:], "\n") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
if r < 0x20 || r == 0x7f {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
i += size
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 09:47:00 +02:00
|
|
|
// writeLiteralMultilineString writes s as a TOML literal multi-line string,
|
|
|
|
|
// surrounded by triple single quotes. The opening delimiter is followed by a
|
|
|
|
|
// newline that the reader trims, so we always include one. The closing
|
|
|
|
|
// delimiter sits on its own line; if the value does not end in a newline, one
|
|
|
|
|
// is inserted before the closing delimiter.
|
|
|
|
|
func writeLiteralMultilineString(buf *bytes.Buffer, s string) error {
|
|
|
|
|
if !utf8.ValidString(s) {
|
|
|
|
|
return fmt.Errorf("interpres: string is not valid UTF-8")
|
|
|
|
|
}
|
|
|
|
|
buf.WriteString("'''\n")
|
|
|
|
|
buf.WriteString(s)
|
|
|
|
|
if !strings.HasSuffix(s, "\n") {
|
|
|
|
|
buf.WriteByte('\n')
|
|
|
|
|
}
|
|
|
|
|
buf.WriteString("'''")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *encoder) writeFloat(v float64) error {
|
|
|
|
|
switch {
|
|
|
|
|
case math.IsNaN(v):
|
|
|
|
|
e.buf.WriteString("nan")
|
|
|
|
|
case math.IsInf(v, 1):
|
|
|
|
|
e.buf.WriteString("inf")
|
|
|
|
|
case math.IsInf(v, -1):
|
|
|
|
|
e.buf.WriteString("-inf")
|
|
|
|
|
case v == 0:
|
2026-09-17 23:05:41 +02:00
|
|
|
// Normalise negative zero to positive zero, the contract the output
|
|
|
|
|
// rules in the documentation state.
|
2026-08-19 09:47:00 +02:00
|
|
|
e.buf.WriteString("0.0")
|
|
|
|
|
default:
|
|
|
|
|
s := strconv.FormatFloat(v, 'g', -1, 64)
|
|
|
|
|
// TOML forbids leading zeros in the exponent digits.
|
|
|
|
|
if idx := strings.LastIndexAny(s, "eE"); idx >= 0 {
|
|
|
|
|
mant := s[:idx]
|
|
|
|
|
exp := s[idx+1:] // e.g. "+06", "-05"
|
|
|
|
|
sign := ""
|
|
|
|
|
if len(exp) > 0 && (exp[0] == '+' || exp[0] == '-') {
|
|
|
|
|
sign = string(exp[0])
|
|
|
|
|
exp = exp[1:]
|
|
|
|
|
}
|
|
|
|
|
exp = strings.TrimLeft(exp, "0")
|
|
|
|
|
if exp == "" {
|
|
|
|
|
exp = "0"
|
|
|
|
|
}
|
|
|
|
|
s = mant + "e" + sign + exp
|
|
|
|
|
}
|
|
|
|
|
if !strings.ContainsAny(s, ".eE") {
|
|
|
|
|
s += ".0"
|
|
|
|
|
}
|
|
|
|
|
e.buf.WriteString(s)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|