// Copyright (c) 2026 Petr Balvín (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 } // 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 Encoder // 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 } 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 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 Encoder // 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}) } 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: 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) // 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 := true 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 } // Value array. Table elements normalise to map[string]any and the emitter // writes them as inline tables. items := make([]any, n) for i, ev := range elems { if i%ctxCheckInterval == 0 { if err := doc.checkCtx(); err != nil { return err } } 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 } // 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 } func isTableElementType(t reflect.Type) bool { 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 { 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 { 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 { 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 { 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 { a := &doc.entries[i] if a.kind != entryArray { continue } path := append(append([]string{}, prefix...), a.key) for j, sub := range a.docs { 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 _, ent := range doc.entries { 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.writeValue(ent.val); err != nil { return err } e.buf.WriteByte('\n') return nil } 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: e.buf.WriteString(offsetString(v)) return nil case OffsetDateTime: 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: 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 case map[string]any: return e.writeInlineMap(v) case nil: return fmt.Errorf("interpres: cannot encode nil value") default: return fmt.Errorf("interpres: cannot encode %T", val) } } // 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.writeValue(ent.val) } } // 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 }