Files
interpres/encode.go
T

1972 lines
56 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres
import (
"bytes"
"context"
"encoding"
"errors"
"fmt"
"maps"
"math"
"reflect"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"unicode/utf8"
)
var (
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]()
marshalerIfaceType = reflect.TypeFor[Marshaler]()
)
// 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
}
// 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
// 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
}
// encodeConfig carries the public emission options one Marshal call applies;
// the names and defaults are documented on the MarshalOption constructors.
type encodeConfig struct {
layout LayoutKind
omitEmptyArrays bool
literalMultilineAt int
inlineTablesAt int
emitFieldComments bool
}
// 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 {
buf *bytes.Buffer
ctx context.Context
opts encodeConfig
// inlineDepth is the nesting level inside inline tables, which decides
// their indentation.
inlineDepth int
// valueDepth is the nesting level of boxed containers the value writer
// walks, the bound a cyclic map or slice hits instead of the stack.
valueDepth int
// limit is the column at which an inline table is broken; only a
// measuring encoder raises it.
limit int
}
func newEncoder() *encoder {
e := &encoder{limit: inlineLimit}
e.buf = getEncoderBuf()
return e
}
// 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 {
f := &encoder{ctx: e.ctx, opts: e.opts, limit: noInlineBreak}
f.buf = getEncoderBuf()
return f
}
// 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)
}
}
// 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()
}
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
}
switch x := v.(type) {
case *Document:
if x == nil {
return fmt.Errorf("interpres: cannot marshal nil value")
}
return e.writeDocument(x)
case Document:
if x.root == nil {
return fmt.Errorf("interpres: cannot marshal nil value")
}
return e.writeDocument(&x)
case OrderedMap:
return e.encodeOrderedMap(&x)
case *OrderedMap:
if x == nil {
return fmt.Errorf("interpres: cannot marshal nil value")
}
return e.encodeOrderedMap(x)
}
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}
root := encPath{}
switch rv.Kind() {
case reflect.Struct:
if err := buildStructDoc(rv, doc, root); err != nil {
return err
}
case reflect.Map:
if err := buildMapDoc(rv, doc, root); err != nil {
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)
}
// 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:
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
if atDepthLimit(sub.depth) {
return &EncodeError{Path: path.key(key).segments(), Err: errDepthLimit()}
}
if err := buildOrderedDoc(&x, sub, path.key(key)); err != nil {
return err
}
doc.addTable(key, sub)
continue
case *OrderedMap:
if x == nil {
continue
}
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
if atDepthLimit(sub.depth) {
return &EncodeError{Path: path.key(key).segments(), Err: errDepthLimit()}
}
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
}
if err := addField(doc, key, rv, path, false); err != nil {
return err
}
}
return nil
}
// --- 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
// order they were added; emission walks that order directly, either as it is
// (Encoder with LayoutKindDeclaration) or in kind-grouped passes over the same
// slice (the default).
type entry struct {
kind entryKind
key string
val any // entryScalar, boxed
rv reflect.Value // entryScalar, the value the walk kept unboxed
doc *tomlDoc // entryTable
docs []*tomlDoc
// 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
// 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
// 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
}
// 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.
type tomlDoc struct {
entries []entry
ctx context.Context // inherited from encoder; nil-safe
opts encodeConfig // inherited from encoder; options drive emit-time behaviour
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)
}
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})
}
// 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})
}
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]
}
func (d *tomlDoc) addArray(key string, subs []*tomlDoc) {
d.entries = append(d.entries, entry{kind: entryArray, key: key, docs: subs})
}
// --- 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}
}
// String renders the path root first, in the notation Path.String uses.
func (p encPath) String() string {
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 {
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)
}
}
slices.Reverse(parts)
return parts
}
// --- reflection walk: struct ---------------------------------------------
func buildStructDoc(v reflect.Value, doc *tomlDoc, path encPath) error {
return walkStructDoc(v, doc, path, nil, cachedStructSchema(v.Type()), doc.depth)
}
// 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
// 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())
}
t := v.Type()
if cap(doc.entries) == 0 {
doc.entries = make([]entry, 0, t.NumField())
}
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
}
fpath := append(append([]int{}, prefix...), i)
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)
if !schema.ownsKey(name, fpath) {
continue
}
doc.addScalar(name, fv.Interface())
continue
}
if err := walkStructDoc(fv, doc, path, fpath, schema, depth+1); err != nil {
return err
}
continue
case reflect.Map:
if err := buildMapDoc(fv, doc, path); err != nil {
return err
}
continue
}
}
}
name := fieldName(f)
if name == "-" {
continue
}
if !schema.ownsKey(strings.ToLower(name), fpath) {
continue
}
if fieldOmitted(f, v.Field(i)) {
continue
}
before := len(doc.entries)
if err := addField(doc, name, v.Field(i), path, tagHasOption(f.Tag.Get("toml"), "inline")); err != nil {
return err
}
// 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")
}
}
}
return nil
}
// 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 }
// 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
}
// 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 ""
}
// 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
// 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.
func fieldOmitted(f reflect.StructField, v reflect.Value) bool {
tag, ok := f.Tag.Lookup("toml")
if !ok {
return false
}
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 {
return true
}
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
}
}
}
return false
}
func isZeroValue(v reflect.Value) bool {
if v.CanInterface() {
if z, ok := v.Interface().(isZeroer); ok {
return z.IsZero()
}
}
return v.IsZero()
}
// 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 ------------------------------------------------
func buildMapDoc(v reflect.Value, doc *tomlDoc, path encPath) error {
if atDepthLimit(doc.depth) {
return fmt.Errorf("interpres: %s: %w", path.String(), errDepthLimit())
}
if v.Type().Key().Kind() != reflect.String {
return fmt.Errorf("interpres: map key must be string, got %s", v.Type().Key())
}
if cap(doc.entries) == 0 {
doc.entries = make([]entry, 0, v.Len())
}
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
}
}
if err := addField(doc, k.String(), v.MapIndex(k), path, false); err != nil {
return err
}
}
return nil
}
// --- reflection walk: field dispatch -------------------------------------
// 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")
// 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 {
if m, ok := marshalerOf(v); ok {
mv, err := m.MarshalTOML()
if err != nil {
return &EncodeError{Path: path.key(name).segments(), Err: err}
}
if mv == nil {
return &EncodeError{Path: path.key(name).segments(), Err: errNilMarshalTOML}
}
v = reflect.ValueOf(mv)
}
// 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 {
return &EncodeError{Path: path.key(name).segments(), Err: err}
}
if isText {
doc.addScalar(name, s)
return nil
}
v = followPtr(v)
if !v.IsValid() {
return nil
}
if v.Kind() == reflect.Interface {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.Type() == orderedMapType {
om := v.Interface().(OrderedMap)
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
if atDepthLimit(sub.depth) {
return &EncodeError{Path: path.key(name).segments(), Err: errDepthLimit()}
}
if err := buildOrderedDoc(&om, sub, path.key(name)); err != nil {
return err
}
doc.addTable(name, sub).inline = forceInline
return nil
}
switch v.Kind() {
case reflect.Struct:
if isScalarStruct(v.Type()) {
doc.addScalar(name, v.Interface())
return nil
}
return addSubTable(doc, name, v, path, forceInline)
case reflect.Map:
return addSubTable(doc, name, v, path, forceInline)
case reflect.Slice, reflect.Array:
return addArrayValue(doc, name, v, path, forceInline)
default:
// 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
}
}
val, err := normaliseValue(v)
if err != nil {
return fmt.Errorf("interpres: %s: %w", path.key(name), err)
}
doc.addScalar(name, val)
return nil
}
}
func addSubTable(doc *tomlDoc, name string, v reflect.Value, path encPath, forceInline bool) error {
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
if atDepthLimit(sub.depth) {
return &EncodeError{Path: path.key(name).segments(), Err: errDepthLimit()}
}
switch v.Kind() {
case reflect.Struct:
if err := buildStructDoc(v, sub, path.key(name)); err != nil {
return err
}
case reflect.Map:
if err := buildMapDoc(v, sub, path.key(name)); err != nil {
return err
}
}
doc.addTable(name, sub).inline = forceInline
return nil
}
func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath, forceInline bool) error {
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
}
doc.addScalar(name, []any{})
return nil
}
// The element paths hang off this one; they render only when an error
// names them.
apath := path.key(name)
// 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
}
}
}
// 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
}
}
// 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.
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 {
allTables = false
}
}
// 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 {
return &EncodeError{Path: path.key(name).segments(), Err: errors.New("an array of tables has no inline form")}
}
if allTables {
subs := make([]*tomlDoc, n)
for i, ev := range elems {
if i%ctxCheckInterval == 0 {
if err := doc.checkCtx(); err != nil {
return err
}
}
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
if atDepthLimit(sub.depth) {
return &EncodeError{Path: apath.elem(i).segments(), Err: errDepthLimit()}
}
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:
if isScalarStruct(ev.Type()) {
return &EncodeError{Path: apath.elem(i).segments(), Err: errors.New("heterogeneous array contains scalar")}
}
if err := buildStructDoc(ev, sub, apath.elem(i)); err != nil {
return err
}
case ev.Kind() == reflect.Map:
if err := buildMapDoc(ev, sub, apath.elem(i)); err != nil {
return err
}
default:
return &EncodeError{Path: apath.elem(i).segments(), Err: errors.New("heterogeneous array, expected table")}
}
subs[i] = sub
}
doc.addArray(name, subs)
return nil
}
if directValueArray {
doc.addScalarReflect(name, v)
return nil
}
// Value array. Table elements normalise to map[string]any and the emitter
// writes them as inline tables.
items := make([]any, n)
for i := range n {
if i%ctxCheckInterval == 0 {
if err := doc.checkCtx(); err != nil {
return err
}
}
ev := v.Index(i)
if elems != nil {
ev = elems[i]
}
val, err := normaliseValue(ev)
if err != nil {
return &EncodeError{Path: apath.elem(i).segments(), Err: err}
}
items[i] = val
}
doc.addScalar(name, items)
return nil
}
// writeArrayValue writes a value array from its reflect value. Only the
// plain scalar kinds reach it: addArrayValue's direct path takes nothing but
// arrays whose elements are scalar kinds without methods, so one writer per
// element needs no boxing branch and no depth walk of its own; the slices
// and maps the boxed rules re-write travel the normaliseValue route.
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.writeScalarValue(v.Index(i)); err != nil {
return err
}
}
e.buf.WriteByte(']')
return nil
}
// 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.
// 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.
func marshalerOf(v reflect.Value) (Marshaler, bool) {
if !v.CanInterface() {
return nil, false
}
if v.Kind() == reflect.Interface {
m, ok := v.Interface().(Marshaler)
return m, ok
}
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
}
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.
func resolveElement(v reflect.Value, path encPath) (reflect.Value, error) {
ev := followPtr(v)
if !ev.IsValid() {
return ev, &EncodeError{Path: path.segments(), Err: errors.New("nil element")}
}
m, ok := marshalerOf(ev)
if !ok {
return ev, nil
}
mv, err := m.MarshalTOML()
if err != nil {
return reflect.Value{}, &EncodeError{Path: path.segments(), Err: err}
}
if mv == nil {
return reflect.Value{}, &EncodeError{Path: path.segments(), Err: errNilMarshalTOML}
}
ev = followPtr(reflect.ValueOf(mv))
if !ev.IsValid() {
return ev, &EncodeError{Path: path.segments(), Err: errors.New("nil element")}
}
return ev, nil
}
// 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) {
return normaliseValueAt(v, 0)
}
func normaliseValueAt(v reflect.Value, depth int) (any, error) {
if atDepthLimit(depth) {
return nil, errDepthLimit()
}
if !v.IsValid() {
return nil, fmt.Errorf("cannot encode nil value")
}
// 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")
}
if m, ok := marshalerOf(v); ok {
mv, err := m.MarshalTOML()
if err != nil {
return nil, err
}
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() {
return normaliseValueAt(rv, depth+1)
}
return mv, nil
}
// The datetime structs are TOML scalars; the emitter renders each of them.
if isScalarStruct(v.Type()) {
return v.Interface(), nil
}
// 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
}
// 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
}
// 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() {
val, err := normaliseValueAt(reflect.ValueOf(om.values[k]), depth+1)
if err != nil {
return nil, fmt.Errorf("[%s]: %w", k, err)
}
out[k] = val
}
return out, nil
}
// 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
}
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
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() {
val, err := normaliseValueAt(v.MapIndex(k), depth+1)
if err != nil {
return nil, fmt.Errorf("[%s]: %w", k.String(), err)
}
out[k.String()] = val
}
return out, nil
case reflect.Slice, reflect.Array:
items := make([]any, v.Len())
for i := range v.Len() {
val, err := normaliseValueAt(v.Index(i), depth+1)
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
// as a TOML scalar: time.Time and the four date-time wrappers.
func isScalarStruct(t reflect.Type) bool {
return t == timeGoType || t == offsetDateTimeType || isLocalDateType(t)
}
func isLocalDateType(t reflect.Type) bool {
return t == localDateTimeType || t == localDateType || t == localTimeType
}
// 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.
func isDateTimeType(t reflect.Type) bool {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
return isScalarStruct(t)
}
// 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
}
// 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.
func textMarshalerOf(v reflect.Value) (encoding.TextMarshaler, bool) {
if !v.CanInterface() {
return nil, false
}
if v.Kind() == reflect.Interface {
m, ok := v.Interface().(encoding.TextMarshaler)
return m, ok
}
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
}
return nil, false
}
// isTableElementType reports whether a slice of t is an array of tables. A
// pointer element is looked through, so []*T behaves as []T: the empty-slice
// decision and the header form agree on the same element type.
func isTableElementType(t reflect.Type) bool {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
switch t.Kind() {
case reflect.Struct:
return !isScalarStruct(t) && !isTextMarshalerType(t)
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 {
// The walk that built the document checked the context on its own
// cadence; the emission of a large document is long enough to need the
// same checks, or a cancellation that lands after the walk would wait a
// full document out.
if err := e.checkCtx(); err != nil {
return err
}
if e.opts.layout == LayoutKindGrouped {
// 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 {
if i%ctxCheckInterval == 0 {
if err := e.checkCtx(); err != nil {
return err
}
}
kv := &doc.entries[i]
if kv.kind != entryScalar {
continue
}
if err := e.writeKV(kv); err != nil {
return err
}
}
// 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.
for i := range doc.entries {
if i%ctxCheckInterval == 0 {
if err := e.checkCtx(); err != nil {
return err
}
}
t := &doc.entries[i]
if t.kind != entryTable {
continue
}
inlined, err := e.writeInlineSubTableIfSmall(t)
if err != nil {
return err
}
t.emitted = inlined
}
for i := range doc.entries {
if i%ctxCheckInterval == 0 {
if err := e.checkCtx(); err != nil {
return err
}
}
t := &doc.entries[i]
if t.kind != entryTable || t.emitted {
continue
}
path := append(append([]string{}, prefix...), t.key)
e.writeBlankLine()
e.writeComments(t.comments)
e.buf.WriteByte('[')
if err := e.writeKeyPath(path); err != nil {
return err
}
e.buf.WriteString("]\n")
if err := e.emitDoc(t.doc, path); err != nil {
return err
}
}
for i := range doc.entries {
if i%ctxCheckInterval == 0 {
if err := e.checkCtx(); err != nil {
return err
}
}
a := &doc.entries[i]
if a.kind != entryArray {
continue
}
path := append(append([]string{}, prefix...), a.key)
for j, sub := range a.docs {
if j%ctxCheckInterval == 0 {
if err := e.checkCtx(); err != nil {
return err
}
}
e.writeBlankLine()
if j == 0 {
e.writeComments(a.comments)
}
e.buf.WriteString("[[")
if err := e.writeKeyPath(path); err != nil {
return err
}
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 i, ent := range doc.entries {
if i%ctxCheckInterval == 0 {
if err := e.checkCtx(); err != nil {
return err
}
}
switch ent.kind {
case entryScalar:
if err := e.writeKV(&ent); err != nil {
return err
}
case entryTable:
inlined, err := e.writeInlineSubTableIfSmall(&ent)
if err != nil {
return err
}
if inlined {
continue
}
path := append(append([]string{}, prefix...), ent.key)
e.writeBlankLine()
e.writeComments(ent.comments)
e.buf.WriteByte('[')
if err := e.writeKeyPath(path); err != nil {
return err
}
e.buf.WriteString("]\n")
if err := e.emitDoc(ent.doc, path); err != nil {
return err
}
case entryArray:
path := append(append([]string{}, prefix...), ent.key)
for j, sub := range ent.docs {
e.writeBlankLine()
if j == 0 {
e.writeComments(ent.comments)
}
e.buf.WriteString("[[")
if err := e.writeKeyPath(path); err != nil {
return err
}
e.buf.WriteString("]]\n")
if err := e.emitDoc(sub, path); err != nil {
return err
}
}
}
}
return nil
}
// 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 {
return err
}
e.buf.WriteString(" = ")
if err := e.writeEntryValue(ent); err != nil {
return err
}
e.buf.WriteByte('\n')
return nil
}
// 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())
}
func (e *encoder) writeKeyPath(path []string) error {
for i, p := range path {
if i > 0 {
e.buf.WriteByte('.')
}
if err := e.writeKey(p); err != nil {
return err
}
}
return nil
}
// 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 {
if isBareKey(key) {
e.buf.WriteString(key)
return nil
}
if !utf8.ValidString(key) {
return fmt.Errorf("interpres: key %q is not valid UTF-8", key)
}
return writeQuotedString(e.buf, key)
}
// writeQuotedString writes s as a TOML basic string (double-quoted) to buf.
// Returns an error only if s is not valid UTF-8.
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); {
// 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
}
r, size := utf8.DecodeRuneInString(s[i:])
if r == utf8.RuneError && size == 1 {
buf.WriteString("\\ufffd")
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 {
fmt.Fprintf(buf, "\\u%04X", r)
} 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
case Number:
if _, err := decodeNumber(string(v)); err != nil {
return fmt.Errorf("interpres: %w", err)
}
e.buf.WriteString(string(v))
return nil
case float64:
return e.writeFloat(v)
case time.Time:
if err := wholeMinuteOffset(v); err != nil {
return err
}
e.buf.WriteString(offsetString(v))
return nil
case OffsetDateTime:
if err := wholeMinuteOffset(v.Time); err != nil {
return err
}
e.buf.WriteString(v.String())
return nil
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:
if err := e.enterValueDepth(); err != nil {
return err
}
err := e.writeValueArray(v)
e.valueDepth--
return err
case map[string]any:
if err := e.enterValueDepth(); err != nil {
return err
}
err := e.writeInlineMap(v)
e.valueDepth--
return err
case nil:
return fmt.Errorf("interpres: cannot encode nil value")
default:
return fmt.Errorf("interpres: cannot encode %T", val)
}
}
// enterValueDepth counts one level of boxed container nesting. The
// reflection walk has its own bound, but a tree built by hand and written
// through the boxed path carries no walk, so the writer bounds itself.
func (e *encoder) enterValueDepth() error {
e.valueDepth++
if e.valueDepth > maxEncodeDepth {
return errDepthLimit()
}
return nil
}
// writeValueArray writes a boxed value array, one writeValue per element.
func (e *encoder) writeValueArray(v []any) error {
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
}
// 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 {
// 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)
}
flat := e.flat()
err := flat.writeInlineMapFlat(m)
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.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 {
keys := slices.Sorted(maps.Keys(m))
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.writeValue(m[k]); err != nil {
return err
}
}
e.buf.WriteByte('}')
return nil
}
// 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')
}
}
// 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:
return e.writeEntryValue(&ent)
}
}
// 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
// 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)) {
return false, nil
}
if !inlinableDoc(t.doc) {
return false, fmt.Errorf("interpres: field %q holds an array of tables and has no inline form", t.key)
}
flat := e.flat()
if err := flat.writeInlineDoc(t.doc); err != nil {
flat.release()
return false, err
}
if !t.inline && flat.buf.Len() > e.opts.inlineTablesAt {
flat.release()
return false, nil
}
if err := e.writeKey(t.key); err != nil {
flat.release()
return false, err
}
e.buf.WriteString(" = ")
if e.column()+flat.buf.Len() <= e.limit {
e.buf.Write(flat.buf.Bytes())
} else if err := e.writeInlineDocMultiline(t.doc); err != nil {
flat.release()
return false, err
}
flat.release()
e.buf.WriteByte('\n')
return true, nil
}
func (e *encoder) writeStringVal(s string) error {
if e.opts.literalMultilineAt > 0 && strings.ContainsRune(s, '\n') &&
len(s) >= e.opts.literalMultilineAt && canBeLiteralMultiline(s) {
return writeLiteralMultilineString(e.buf, s)
}
return writeQuotedString(e.buf, s)
}
// 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
}
// 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:
// Normalise negative zero to positive zero, the contract the output
// rules in the documentation state.
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
}