003aa0993b
When marshaling a map with nil pointer values, the keys were being
silently dropped, breaking round-trip fidelity. For example:
map[string]*struct{}{"foo": nil}
Would produce an empty TOML document instead of "[foo]".
This change converts nil pointer values in maps to their zero values
(consistent with how nil pointers in slices are handled), allowing the
keys to be preserved as empty tables.
Nil interface values (map[string]any{"foo": nil}) are still skipped
since there's no type information to derive a zero value.
Fixes #975
Also, pin golangci-lint version to v2.8.0 in CI and document in AGENTS.md
- Explicitly set golangci-lint version in lint.yml to ensure consistent
behavior across CI runs
- Update AGENTS.md with instructions to use the same linter version locally
---------
Co-authored-by: Claude <noreply@anthropic.com>
1200 lines
26 KiB
Go
1200 lines
26 KiB
Go
package toml
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/pelletier/go-toml/v2/internal/characters"
|
|
)
|
|
|
|
// Marshal serializes a Go value as a TOML document.
|
|
//
|
|
// It is a shortcut for Encoder.Encode() with the default options.
|
|
func Marshal(v interface{}) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
enc := NewEncoder(&buf)
|
|
|
|
err := enc.Encode(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// Encoder writes a TOML document to an output stream.
|
|
type Encoder struct {
|
|
// output
|
|
w io.Writer
|
|
|
|
// global settings
|
|
tablesInline bool
|
|
arraysMultiline bool
|
|
indentSymbol string
|
|
indentTables bool
|
|
marshalJSONNumbers bool
|
|
}
|
|
|
|
// NewEncoder returns a new Encoder that writes to w.
|
|
func NewEncoder(w io.Writer) *Encoder {
|
|
return &Encoder{
|
|
w: w,
|
|
indentSymbol: " ",
|
|
}
|
|
}
|
|
|
|
// SetTablesInline forces the encoder to emit all tables inline.
|
|
//
|
|
// This behavior can be controlled on an individual struct field basis with the
|
|
// inline tag:
|
|
//
|
|
// MyField `toml:",inline"`
|
|
func (enc *Encoder) SetTablesInline(inline bool) *Encoder {
|
|
enc.tablesInline = inline
|
|
return enc
|
|
}
|
|
|
|
// SetArraysMultiline forces the encoder to emit all arrays with one element per
|
|
// line.
|
|
//
|
|
// This behavior can be controlled on an individual struct field basis with the multiline tag:
|
|
//
|
|
// MyField `multiline:"true"`
|
|
func (enc *Encoder) SetArraysMultiline(multiline bool) *Encoder {
|
|
enc.arraysMultiline = multiline
|
|
return enc
|
|
}
|
|
|
|
// SetIndentSymbol defines the string that should be used for indentation. The
|
|
// provided string is repeated for each indentation level. Defaults to two
|
|
// spaces.
|
|
func (enc *Encoder) SetIndentSymbol(s string) *Encoder {
|
|
enc.indentSymbol = s
|
|
return enc
|
|
}
|
|
|
|
// SetIndentTables forces the encoder to intent tables and array tables.
|
|
func (enc *Encoder) SetIndentTables(indent bool) *Encoder {
|
|
enc.indentTables = indent
|
|
return enc
|
|
}
|
|
|
|
// SetMarshalJSONNumbers forces the encoder to serialize `json.Number` as a
|
|
// float or integer instead of relying on TextMarshaler to emit a string.
|
|
//
|
|
// *Unstable:* This method does not follow the compatibility guarantees of
|
|
// semver. It can be changed or removed without a new major version being
|
|
// issued.
|
|
func (enc *Encoder) SetMarshalJSONNumbers(indent bool) *Encoder {
|
|
enc.marshalJSONNumbers = indent
|
|
return enc
|
|
}
|
|
|
|
// Encode writes a TOML representation of v to the stream.
|
|
//
|
|
// If v cannot be represented to TOML it returns an error.
|
|
//
|
|
// # Encoding rules
|
|
//
|
|
// A top level slice containing only maps or structs is encoded as [[table
|
|
// array]].
|
|
//
|
|
// All slices not matching rule 1 are encoded as [array]. As a result, any map
|
|
// or struct they contain is encoded as an {inline table}.
|
|
//
|
|
// Nil interfaces and nil pointers are not supported.
|
|
//
|
|
// Keys in key-values always have one part.
|
|
//
|
|
// Intermediate tables are always printed.
|
|
//
|
|
// By default, strings are encoded as literal string, unless they contain either
|
|
// a newline character or a single quote. In that case they are emitted as
|
|
// quoted strings.
|
|
//
|
|
// Unsigned integers larger than math.MaxInt64 cannot be encoded. Doing so
|
|
// results in an error. This rule exists because the TOML specification only
|
|
// requires parsers to support at least the 64 bits integer range. Allowing
|
|
// larger numbers would create non-standard TOML documents, which may not be
|
|
// readable (at best) by other implementations. To encode such numbers, a
|
|
// solution is a custom type that implements encoding.TextMarshaler.
|
|
//
|
|
// When encoding structs, fields are encoded in order of definition, with their
|
|
// exact name.
|
|
//
|
|
// Tables and array tables are separated by empty lines. However, consecutive
|
|
// subtables definitions are not. For example:
|
|
//
|
|
// [top1]
|
|
//
|
|
// [top2]
|
|
// [top2.child1]
|
|
//
|
|
// [[array]]
|
|
//
|
|
// [[array]]
|
|
// [array.child2]
|
|
//
|
|
// # Struct tags
|
|
//
|
|
// The encoding of each public struct field can be customized by the format
|
|
// string in the "toml" key of the struct field's tag. This follows
|
|
// encoding/json's convention. The format string starts with the name of the
|
|
// field, optionally followed by a comma-separated list of options. The name may
|
|
// be empty in order to provide options without overriding the default name.
|
|
//
|
|
// The "multiline" option emits strings as quoted multi-line TOML strings. It
|
|
// has no effect on fields that would not be encoded as strings.
|
|
//
|
|
// The "inline" option turns fields that would be emitted as tables into inline
|
|
// tables instead. It has no effect on other fields.
|
|
//
|
|
// The "omitempty" option prevents empty values or groups from being emitted.
|
|
//
|
|
// The "omitzero" option prevents zero values or groups from being emitted.
|
|
//
|
|
// The "commented" option prefixes the value and all its children with a comment
|
|
// symbol.
|
|
//
|
|
// In addition to the "toml" tag struct tag, a "comment" tag can be used to emit
|
|
// a TOML comment before the value being annotated. Comments are ignored inside
|
|
// inline tables. For array tables, the comment is only present before the first
|
|
// element of the array.
|
|
func (enc *Encoder) Encode(v interface{}) error {
|
|
var (
|
|
b []byte
|
|
ctx encoderCtx
|
|
)
|
|
|
|
ctx.inline = enc.tablesInline
|
|
|
|
if v == nil {
|
|
return errors.New("toml: cannot encode a nil interface")
|
|
}
|
|
|
|
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = enc.w.Write(b)
|
|
if err != nil {
|
|
return fmt.Errorf("toml: cannot write: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type valueOptions struct {
|
|
multiline bool
|
|
omitempty bool
|
|
omitzero bool
|
|
commented bool
|
|
comment string
|
|
}
|
|
|
|
type encoderCtx struct {
|
|
// Current top-level key.
|
|
parentKey []string
|
|
|
|
// Key that should be used for a KV.
|
|
key string
|
|
// Extra flag to account for the empty string
|
|
hasKey bool
|
|
|
|
// Set to true to indicate that the encoder is inside a KV, so that all
|
|
// tables need to be inlined.
|
|
insideKv bool
|
|
|
|
// Set to true to skip the first table header in an array table.
|
|
skipTableHeader bool
|
|
|
|
// Should the next table be encoded as inline
|
|
inline bool
|
|
|
|
// Indentation level
|
|
indent int
|
|
|
|
// Prefix the current value with a comment.
|
|
commented bool
|
|
|
|
// Options coming from struct tags
|
|
options valueOptions
|
|
}
|
|
|
|
func (ctx *encoderCtx) shiftKey() {
|
|
if ctx.hasKey {
|
|
ctx.parentKey = append(ctx.parentKey, ctx.key)
|
|
ctx.clearKey()
|
|
}
|
|
}
|
|
|
|
func (ctx *encoderCtx) setKey(k string) {
|
|
ctx.key = k
|
|
ctx.hasKey = true
|
|
}
|
|
|
|
func (ctx *encoderCtx) clearKey() {
|
|
ctx.key = ""
|
|
ctx.hasKey = false
|
|
}
|
|
|
|
func (ctx *encoderCtx) isRoot() bool {
|
|
return len(ctx.parentKey) == 0 && !ctx.hasKey
|
|
}
|
|
|
|
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
i := v.Interface()
|
|
|
|
switch x := i.(type) {
|
|
case time.Time:
|
|
if x.Nanosecond() > 0 {
|
|
return x.AppendFormat(b, time.RFC3339Nano), nil
|
|
}
|
|
return x.AppendFormat(b, time.RFC3339), nil
|
|
case LocalTime:
|
|
return append(b, x.String()...), nil
|
|
case LocalDate:
|
|
return append(b, x.String()...), nil
|
|
case LocalDateTime:
|
|
return append(b, x.String()...), nil
|
|
case json.Number:
|
|
if enc.marshalJSONNumbers {
|
|
if x == "" { /// Useful zero value.
|
|
return append(b, "0"...), nil
|
|
} else if v, err := x.Int64(); err == nil {
|
|
return enc.encode(b, ctx, reflect.ValueOf(v))
|
|
} else if f, err := x.Float64(); err == nil {
|
|
return enc.encode(b, ctx, reflect.ValueOf(f))
|
|
}
|
|
return nil, fmt.Errorf("toml: unable to convert %q to int64 or float64", x)
|
|
}
|
|
}
|
|
|
|
hasTextMarshaler := v.Type().Implements(textMarshalerType)
|
|
if hasTextMarshaler || (v.CanAddr() && reflect.PointerTo(v.Type()).Implements(textMarshalerType)) {
|
|
if !hasTextMarshaler {
|
|
v = v.Addr()
|
|
}
|
|
|
|
if ctx.isRoot() {
|
|
return nil, fmt.Errorf("toml: type %s implementing the TextMarshaler interface cannot be a root element", v.Type())
|
|
}
|
|
|
|
text, err := v.Interface().(encoding.TextMarshaler).MarshalText()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b = enc.encodeString(b, string(text), ctx.options)
|
|
|
|
return b, nil
|
|
}
|
|
|
|
switch v.Kind() {
|
|
// containers
|
|
case reflect.Map:
|
|
return enc.encodeMap(b, ctx, v)
|
|
case reflect.Struct:
|
|
return enc.encodeStruct(b, ctx, v)
|
|
case reflect.Slice, reflect.Array:
|
|
return enc.encodeSlice(b, ctx, v)
|
|
case reflect.Interface:
|
|
if v.IsNil() {
|
|
return nil, errors.New("toml: encoding a nil interface is not supported")
|
|
}
|
|
|
|
return enc.encode(b, ctx, v.Elem())
|
|
case reflect.Ptr:
|
|
if v.IsNil() {
|
|
return enc.encode(b, ctx, reflect.Zero(v.Type().Elem()))
|
|
}
|
|
|
|
return enc.encode(b, ctx, v.Elem())
|
|
|
|
// values
|
|
case reflect.String:
|
|
b = enc.encodeString(b, v.String(), ctx.options)
|
|
case reflect.Float32:
|
|
f := v.Float()
|
|
|
|
switch {
|
|
case math.IsNaN(f):
|
|
b = append(b, "nan"...)
|
|
case f > math.MaxFloat32:
|
|
b = append(b, "inf"...)
|
|
case f < -math.MaxFloat32:
|
|
b = append(b, "-inf"...)
|
|
case math.Trunc(f) == f:
|
|
b = strconv.AppendFloat(b, f, 'f', 1, 32)
|
|
default:
|
|
b = strconv.AppendFloat(b, f, 'f', -1, 32)
|
|
}
|
|
case reflect.Float64:
|
|
f := v.Float()
|
|
switch {
|
|
case math.IsNaN(f):
|
|
b = append(b, "nan"...)
|
|
case f > math.MaxFloat64:
|
|
b = append(b, "inf"...)
|
|
case f < -math.MaxFloat64:
|
|
b = append(b, "-inf"...)
|
|
case math.Trunc(f) == f:
|
|
b = strconv.AppendFloat(b, f, 'f', 1, 64)
|
|
default:
|
|
b = strconv.AppendFloat(b, f, 'f', -1, 64)
|
|
}
|
|
case reflect.Bool:
|
|
if v.Bool() {
|
|
b = append(b, "true"...)
|
|
} else {
|
|
b = append(b, "false"...)
|
|
}
|
|
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
|
|
x := v.Uint()
|
|
if x > uint64(math.MaxInt64) {
|
|
return nil, fmt.Errorf("toml: not encoding uint (%d) greater than max int64 (%d)", x, int64(math.MaxInt64))
|
|
}
|
|
b = strconv.AppendUint(b, x, 10)
|
|
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
|
|
b = strconv.AppendInt(b, v.Int(), 10)
|
|
default:
|
|
return nil, fmt.Errorf("toml: cannot encode value of type %s", v.Kind())
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func isNil(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.Ptr, reflect.Interface, reflect.Map:
|
|
return v.IsNil()
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func shouldOmitEmpty(options valueOptions, v reflect.Value) bool {
|
|
return options.omitempty && isEmptyValue(v)
|
|
}
|
|
|
|
func shouldOmitZero(options valueOptions, v reflect.Value) bool {
|
|
if !options.omitzero {
|
|
return false
|
|
}
|
|
|
|
// Check if the type implements isZeroer interface (has a custom IsZero method).
|
|
if v.Type().Implements(isZeroerType) {
|
|
return v.Interface().(isZeroer).IsZero()
|
|
}
|
|
|
|
// Check if pointer type implements isZeroer.
|
|
if reflect.PointerTo(v.Type()).Implements(isZeroerType) {
|
|
if v.CanAddr() {
|
|
return v.Addr().Interface().(isZeroer).IsZero()
|
|
}
|
|
// Create a temporary addressable copy to call the pointer receiver method.
|
|
pv := reflect.New(v.Type())
|
|
pv.Elem().Set(v)
|
|
return pv.Interface().(isZeroer).IsZero()
|
|
}
|
|
|
|
// Fall back to reflect's IsZero for types without custom IsZero method.
|
|
return v.IsZero()
|
|
}
|
|
|
|
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
|
|
var err error
|
|
|
|
if !ctx.inline {
|
|
b = enc.encodeComment(ctx.indent, options.comment, b)
|
|
b = enc.commented(ctx.commented, b)
|
|
b = enc.indent(ctx.indent, b)
|
|
}
|
|
|
|
b = enc.encodeKey(b, ctx.key)
|
|
b = append(b, " = "...)
|
|
|
|
// create a copy of the context because the value of a KV shouldn't
|
|
// modify the global context.
|
|
subctx := ctx
|
|
subctx.insideKv = true
|
|
subctx.shiftKey()
|
|
subctx.options = options
|
|
|
|
b, err = enc.encode(b, subctx, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (enc *Encoder) commented(commented bool, b []byte) []byte {
|
|
if commented {
|
|
return append(b, "# "...)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func isEmptyValue(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.Struct:
|
|
return isEmptyStruct(v)
|
|
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
|
return v.Len() == 0
|
|
case reflect.Bool:
|
|
return !v.Bool()
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return v.Int() == 0
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
return v.Uint() == 0
|
|
case reflect.Float32, reflect.Float64:
|
|
return v.Float() == 0
|
|
case reflect.Interface, reflect.Ptr:
|
|
return v.IsNil()
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isEmptyStruct(v reflect.Value) bool {
|
|
// TODO: merge with walkStruct and cache.
|
|
typ := v.Type()
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
fieldType := typ.Field(i)
|
|
|
|
// only consider exported fields
|
|
if fieldType.PkgPath != "" {
|
|
continue
|
|
}
|
|
|
|
tag := fieldType.Tag.Get("toml")
|
|
|
|
// special field name to skip field
|
|
if tag == "-" {
|
|
continue
|
|
}
|
|
|
|
f := v.Field(i)
|
|
|
|
if !isEmptyValue(f) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
const literalQuote = '\''
|
|
|
|
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
|
|
if needsQuoting(v) {
|
|
return enc.encodeQuotedString(options.multiline, b, v)
|
|
}
|
|
|
|
return enc.encodeLiteralString(b, v)
|
|
}
|
|
|
|
func needsQuoting(v string) bool {
|
|
// TODO: vectorize
|
|
for _, b := range []byte(v) {
|
|
if b == '\'' || b == '\r' || b == '\n' || characters.InvalidASCII(b) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// caller should have checked that the string does not contain new lines or ' .
|
|
func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
|
|
b = append(b, literalQuote)
|
|
b = append(b, v...)
|
|
b = append(b, literalQuote)
|
|
|
|
return b
|
|
}
|
|
|
|
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
|
|
stringQuote := `"`
|
|
|
|
if multiline {
|
|
stringQuote = `"""`
|
|
}
|
|
|
|
b = append(b, stringQuote...)
|
|
if multiline {
|
|
b = append(b, '\n')
|
|
}
|
|
|
|
const (
|
|
hextable = "0123456789ABCDEF"
|
|
// U+0000 to U+0008, U+000A to U+001F, U+007F
|
|
nul = 0x0
|
|
bs = 0x8
|
|
lf = 0xa
|
|
us = 0x1f
|
|
del = 0x7f
|
|
)
|
|
|
|
bv := []byte(v)
|
|
for i := 0; i < len(bv); i++ {
|
|
r := bv[i]
|
|
switch r {
|
|
case '\\':
|
|
b = append(b, `\\`...)
|
|
case '"':
|
|
if multiline {
|
|
// Quotation marks do not need to be quoted in multiline strings unless
|
|
// it contains 3 consecutive. If 3+ quotes appear, quote all of them
|
|
// because it's visually better
|
|
if i+2 > len(bv) || bv[i+1] != '"' || bv[i+2] != '"' {
|
|
b = append(b, r)
|
|
} else {
|
|
b = append(b, `\"\"\"`...)
|
|
i += 2
|
|
}
|
|
} else {
|
|
b = append(b, `\"`...)
|
|
}
|
|
case '\b':
|
|
b = append(b, `\b`...)
|
|
case '\f':
|
|
b = append(b, `\f`...)
|
|
case '\n':
|
|
if multiline {
|
|
b = append(b, r)
|
|
} else {
|
|
b = append(b, `\n`...)
|
|
}
|
|
case '\r':
|
|
b = append(b, `\r`...)
|
|
case '\t':
|
|
b = append(b, `\t`...)
|
|
default:
|
|
switch {
|
|
case r >= nul && r <= bs, r >= lf && r <= us, r == del:
|
|
b = append(b, `\u00`...)
|
|
b = append(b, hextable[r>>4])
|
|
b = append(b, hextable[r&0x0f])
|
|
default:
|
|
b = append(b, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
b = append(b, stringQuote...)
|
|
|
|
return b
|
|
}
|
|
|
|
// caller should have checked that the string is in A-Z / a-z / 0-9 / - / _ .
|
|
func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
|
|
return append(b, v...)
|
|
}
|
|
|
|
func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) []byte {
|
|
if len(ctx.parentKey) == 0 {
|
|
return b
|
|
}
|
|
|
|
b = enc.encodeComment(ctx.indent, ctx.options.comment, b)
|
|
|
|
b = enc.commented(ctx.commented, b)
|
|
|
|
b = enc.indent(ctx.indent, b)
|
|
|
|
b = append(b, '[')
|
|
|
|
b = enc.encodeKey(b, ctx.parentKey[0])
|
|
|
|
for _, k := range ctx.parentKey[1:] {
|
|
b = append(b, '.')
|
|
b = enc.encodeKey(b, k)
|
|
}
|
|
|
|
b = append(b, "]\n"...)
|
|
|
|
return b
|
|
}
|
|
|
|
func (enc *Encoder) encodeKey(b []byte, k string) []byte {
|
|
needsQuotation := false
|
|
cannotUseLiteral := false
|
|
|
|
if len(k) == 0 {
|
|
return append(b, "''"...)
|
|
}
|
|
|
|
for _, c := range k {
|
|
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
|
|
continue
|
|
}
|
|
|
|
if c == literalQuote {
|
|
cannotUseLiteral = true
|
|
}
|
|
|
|
needsQuotation = true
|
|
}
|
|
|
|
if needsQuotation && needsQuoting(k) {
|
|
cannotUseLiteral = true
|
|
}
|
|
|
|
switch {
|
|
case cannotUseLiteral:
|
|
return enc.encodeQuotedString(false, b, k)
|
|
case needsQuotation:
|
|
return enc.encodeLiteralString(b, k)
|
|
default:
|
|
return enc.encodeUnquotedKey(b, k)
|
|
}
|
|
}
|
|
|
|
func (enc *Encoder) keyToString(k reflect.Value) (string, error) {
|
|
keyType := k.Type()
|
|
if keyType.Implements(textMarshalerType) {
|
|
keyB, err := k.Interface().(encoding.TextMarshaler).MarshalText()
|
|
if err != nil {
|
|
return "", fmt.Errorf("toml: error marshalling key %v from text: %w", k, err)
|
|
}
|
|
return string(keyB), nil
|
|
}
|
|
|
|
switch keyType.Kind() {
|
|
case reflect.String:
|
|
return k.String(), nil
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return strconv.FormatInt(k.Int(), 10), nil
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return strconv.FormatUint(k.Uint(), 10), nil
|
|
|
|
case reflect.Float32:
|
|
return strconv.FormatFloat(k.Float(), 'f', -1, 32), nil
|
|
|
|
case reflect.Float64:
|
|
return strconv.FormatFloat(k.Float(), 'f', -1, 64), nil
|
|
|
|
default:
|
|
return "", fmt.Errorf("toml: type %s is not supported as a map key", keyType.Kind())
|
|
}
|
|
}
|
|
|
|
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
var (
|
|
t table
|
|
emptyValueOptions valueOptions
|
|
)
|
|
|
|
iter := v.MapRange()
|
|
for iter.Next() {
|
|
v := iter.Value()
|
|
|
|
if isNil(v) {
|
|
// For nil pointers, convert to zero value of the element type.
|
|
// This allows round-trip marshaling of maps with nil pointer values.
|
|
// For nil interfaces and nil maps, skip since we can't derive a type.
|
|
if v.Kind() == reflect.Ptr {
|
|
v = reflect.Zero(v.Type().Elem())
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
k, err := enc.keyToString(iter.Key())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if willConvertToTableOrArrayTable(ctx, v) {
|
|
t.pushTable(k, v, emptyValueOptions)
|
|
} else {
|
|
t.pushKV(k, v, emptyValueOptions)
|
|
}
|
|
}
|
|
|
|
sortEntriesByKey(t.kvs)
|
|
sortEntriesByKey(t.tables)
|
|
|
|
return enc.encodeTable(b, ctx, t)
|
|
}
|
|
|
|
func sortEntriesByKey(e []entry) {
|
|
slices.SortFunc(e, func(a, b entry) int {
|
|
return strings.Compare(a.Key, b.Key)
|
|
})
|
|
}
|
|
|
|
type entry struct {
|
|
Key string
|
|
Value reflect.Value
|
|
Options valueOptions
|
|
}
|
|
|
|
type table struct {
|
|
kvs []entry
|
|
tables []entry
|
|
}
|
|
|
|
func (t *table) pushKV(k string, v reflect.Value, options valueOptions) {
|
|
for _, e := range t.kvs {
|
|
if e.Key == k {
|
|
return
|
|
}
|
|
}
|
|
|
|
t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options})
|
|
}
|
|
|
|
func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
|
|
for _, e := range t.tables {
|
|
if e.Key == k {
|
|
return
|
|
}
|
|
}
|
|
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
|
|
}
|
|
|
|
func walkStruct(ctx encoderCtx, t *table, v reflect.Value) {
|
|
// TODO: cache this
|
|
typ := v.Type()
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
fieldType := typ.Field(i)
|
|
|
|
// only consider exported fields
|
|
if fieldType.PkgPath != "" {
|
|
continue
|
|
}
|
|
|
|
tag := fieldType.Tag.Get("toml")
|
|
|
|
// special field name to skip field
|
|
if tag == "-" {
|
|
continue
|
|
}
|
|
|
|
k, opts := parseTag(tag)
|
|
if !isValidName(k) {
|
|
k = ""
|
|
}
|
|
|
|
f := v.Field(i)
|
|
|
|
if k == "" {
|
|
if fieldType.Anonymous {
|
|
if fieldType.Type.Kind() == reflect.Struct {
|
|
walkStruct(ctx, t, f)
|
|
} else if fieldType.Type.Kind() == reflect.Ptr && !f.IsNil() && f.Elem().Kind() == reflect.Struct {
|
|
walkStruct(ctx, t, f.Elem())
|
|
}
|
|
continue
|
|
}
|
|
k = fieldType.Name
|
|
}
|
|
|
|
if isNil(f) {
|
|
continue
|
|
}
|
|
|
|
options := valueOptions{
|
|
multiline: opts.multiline,
|
|
omitempty: opts.omitempty,
|
|
omitzero: opts.omitzero,
|
|
commented: opts.commented,
|
|
comment: fieldType.Tag.Get("comment"),
|
|
}
|
|
|
|
if opts.inline || !willConvertToTableOrArrayTable(ctx, f) {
|
|
t.pushKV(k, f, options)
|
|
} else {
|
|
t.pushTable(k, f, options)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
var t table
|
|
|
|
walkStruct(ctx, &t, v)
|
|
|
|
return enc.encodeTable(b, ctx, t)
|
|
}
|
|
|
|
func (enc *Encoder) encodeComment(indent int, comment string, b []byte) []byte {
|
|
for len(comment) > 0 {
|
|
var line string
|
|
idx := strings.IndexByte(comment, '\n')
|
|
if idx >= 0 {
|
|
line = comment[:idx]
|
|
comment = comment[idx+1:]
|
|
} else {
|
|
line = comment
|
|
comment = ""
|
|
}
|
|
b = enc.indent(indent, b)
|
|
b = append(b, "# "...)
|
|
b = append(b, line...)
|
|
b = append(b, '\n')
|
|
}
|
|
return b
|
|
}
|
|
|
|
func isValidName(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
for _, c := range s {
|
|
switch {
|
|
case strings.ContainsRune("!#$%&()*+-./:;<=>?@[]^_{|}~ ", c):
|
|
// Backslash and quote chars are reserved, but
|
|
// otherwise any punctuation chars are allowed
|
|
// in a tag name.
|
|
case !unicode.IsLetter(c) && !unicode.IsDigit(c):
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
type tagOptions struct {
|
|
multiline bool
|
|
inline bool
|
|
omitempty bool
|
|
omitzero bool
|
|
commented bool
|
|
}
|
|
|
|
func parseTag(tag string) (string, tagOptions) {
|
|
opts := tagOptions{}
|
|
|
|
idx := strings.Index(tag, ",")
|
|
if idx == -1 {
|
|
return tag, opts
|
|
}
|
|
|
|
raw := tag[idx+1:]
|
|
tag = tag[:idx]
|
|
for raw != "" {
|
|
var o string
|
|
i := strings.Index(raw, ",")
|
|
if i >= 0 {
|
|
o, raw = raw[:i], raw[i+1:]
|
|
} else {
|
|
o, raw = raw, ""
|
|
}
|
|
switch o {
|
|
case "multiline":
|
|
opts.multiline = true
|
|
case "inline":
|
|
opts.inline = true
|
|
case "omitempty":
|
|
opts.omitempty = true
|
|
case "omitzero":
|
|
opts.omitzero = true
|
|
case "commented":
|
|
opts.commented = true
|
|
}
|
|
}
|
|
|
|
return tag, opts
|
|
}
|
|
|
|
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
|
|
var err error
|
|
|
|
ctx.shiftKey()
|
|
|
|
if ctx.insideKv || (ctx.inline && !ctx.isRoot()) {
|
|
return enc.encodeTableInline(b, ctx, t)
|
|
}
|
|
|
|
if !ctx.skipTableHeader {
|
|
b = enc.encodeTableHeader(ctx, b)
|
|
|
|
if enc.indentTables && len(ctx.parentKey) > 0 {
|
|
ctx.indent++
|
|
}
|
|
}
|
|
ctx.skipTableHeader = false
|
|
|
|
hasNonEmptyKV := false
|
|
for _, kv := range t.kvs {
|
|
if shouldOmitEmpty(kv.Options, kv.Value) {
|
|
continue
|
|
}
|
|
if shouldOmitZero(kv.Options, kv.Value) {
|
|
continue
|
|
}
|
|
hasNonEmptyKV = true
|
|
|
|
ctx.setKey(kv.Key)
|
|
ctx2 := ctx
|
|
ctx2.commented = kv.Options.commented || ctx2.commented
|
|
|
|
b, err = enc.encodeKv(b, ctx2, kv.Options, kv.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b = append(b, '\n')
|
|
}
|
|
|
|
first := true
|
|
for _, table := range t.tables {
|
|
if shouldOmitEmpty(table.Options, table.Value) {
|
|
continue
|
|
}
|
|
if shouldOmitZero(table.Options, table.Value) {
|
|
continue
|
|
}
|
|
if first {
|
|
first = false
|
|
if hasNonEmptyKV {
|
|
b = append(b, '\n')
|
|
}
|
|
} else {
|
|
b = append(b, "\n"...)
|
|
}
|
|
|
|
ctx.setKey(table.Key)
|
|
|
|
ctx.options = table.Options
|
|
ctx2 := ctx
|
|
ctx2.commented = ctx2.commented || ctx.options.commented
|
|
|
|
b, err = enc.encode(b, ctx2, table.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte, error) {
|
|
var err error
|
|
|
|
b = append(b, '{')
|
|
|
|
first := true
|
|
for _, kv := range t.kvs {
|
|
if shouldOmitEmpty(kv.Options, kv.Value) {
|
|
continue
|
|
}
|
|
if shouldOmitZero(kv.Options, kv.Value) {
|
|
continue
|
|
}
|
|
|
|
if first {
|
|
first = false
|
|
} else {
|
|
b = append(b, `, `...)
|
|
}
|
|
|
|
ctx.setKey(kv.Key)
|
|
|
|
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if len(t.tables) > 0 {
|
|
panic("inline table cannot contain nested tables, only key-values")
|
|
}
|
|
|
|
b = append(b, "}"...)
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
|
|
if !v.IsValid() {
|
|
return false
|
|
}
|
|
t := v.Type()
|
|
if t == timeType || t.Implements(textMarshalerType) {
|
|
return false
|
|
}
|
|
if v.Kind() != reflect.Ptr && v.CanAddr() && reflect.PointerTo(t).Implements(textMarshalerType) {
|
|
return false
|
|
}
|
|
|
|
switch t.Kind() {
|
|
case reflect.Map, reflect.Struct:
|
|
return !ctx.inline
|
|
case reflect.Interface:
|
|
return willConvertToTable(ctx, v.Elem())
|
|
case reflect.Ptr:
|
|
if v.IsNil() {
|
|
return false
|
|
}
|
|
|
|
return willConvertToTable(ctx, v.Elem())
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool {
|
|
if ctx.insideKv {
|
|
return false
|
|
}
|
|
t := v.Type()
|
|
|
|
if t.Kind() == reflect.Interface {
|
|
return willConvertToTableOrArrayTable(ctx, v.Elem())
|
|
}
|
|
|
|
if t.Kind() == reflect.Slice || t.Kind() == reflect.Array {
|
|
if v.Len() == 0 {
|
|
// An empty slice should be a kv = [].
|
|
return false
|
|
}
|
|
|
|
for i := 0; i < v.Len(); i++ {
|
|
t := willConvertToTable(ctx, v.Index(i))
|
|
|
|
if !t {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return willConvertToTable(ctx, v)
|
|
}
|
|
|
|
func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
if v.Len() == 0 {
|
|
b = append(b, "[]"...)
|
|
|
|
return b, nil
|
|
}
|
|
|
|
if willConvertToTableOrArrayTable(ctx, v) {
|
|
return enc.encodeSliceAsArrayTable(b, ctx, v)
|
|
}
|
|
|
|
return enc.encodeSliceAsArray(b, ctx, v)
|
|
}
|
|
|
|
// caller should have checked that v is a slice that only contains values that
|
|
// encode into tables.
|
|
func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
ctx.shiftKey()
|
|
|
|
scratch := make([]byte, 0, 64)
|
|
|
|
scratch = enc.commented(ctx.commented, scratch)
|
|
|
|
if enc.indentTables {
|
|
scratch = enc.indent(ctx.indent, scratch)
|
|
}
|
|
|
|
scratch = append(scratch, "[["...)
|
|
|
|
for i, k := range ctx.parentKey {
|
|
if i > 0 {
|
|
scratch = append(scratch, '.')
|
|
}
|
|
|
|
scratch = enc.encodeKey(scratch, k)
|
|
}
|
|
|
|
scratch = append(scratch, "]]\n"...)
|
|
ctx.skipTableHeader = true
|
|
|
|
b = enc.encodeComment(ctx.indent, ctx.options.comment, b)
|
|
|
|
if enc.indentTables {
|
|
ctx.indent++
|
|
}
|
|
|
|
for i := 0; i < v.Len(); i++ {
|
|
if i != 0 {
|
|
b = append(b, "\n"...)
|
|
}
|
|
|
|
b = append(b, scratch...)
|
|
|
|
var err error
|
|
b, err = enc.encode(b, ctx, v.Index(i))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
multiline := ctx.options.multiline || enc.arraysMultiline
|
|
separator := ", "
|
|
|
|
b = append(b, '[')
|
|
|
|
subCtx := ctx
|
|
subCtx.options = valueOptions{}
|
|
|
|
if multiline {
|
|
separator = ",\n"
|
|
|
|
b = append(b, '\n')
|
|
|
|
subCtx.indent++
|
|
}
|
|
|
|
var err error
|
|
first := true
|
|
|
|
for i := 0; i < v.Len(); i++ {
|
|
if first {
|
|
first = false
|
|
} else {
|
|
b = append(b, separator...)
|
|
}
|
|
|
|
if multiline {
|
|
b = enc.indent(subCtx.indent, b)
|
|
}
|
|
|
|
b, err = enc.encode(b, subCtx, v.Index(i))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if multiline {
|
|
b = append(b, '\n')
|
|
b = enc.indent(ctx.indent, b)
|
|
}
|
|
|
|
b = append(b, ']')
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (enc *Encoder) indent(level int, b []byte) []byte {
|
|
for i := 0; i < level; i++ {
|
|
b = append(b, enc.indentSymbol...)
|
|
}
|
|
|
|
return b
|
|
}
|