2022-10-31 15:52:21 +00:00
|
|
|
package toml
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"encoding"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"math"
|
|
|
|
"reflect"
|
|
|
|
"sort"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/BurntSushi/toml/internal"
|
|
|
|
)
|
|
|
|
|
|
|
|
type tomlEncodeError struct{ error }
|
|
|
|
|
|
|
|
var (
|
|
|
|
errArrayNilElement = errors.New("toml: cannot encode array with nil element")
|
|
|
|
errNonString = errors.New("toml: cannot encode a map with non-string key type")
|
|
|
|
errNoKey = errors.New("toml: top-level values must be Go maps or structs")
|
|
|
|
errAnything = errors.New("") // used in testing
|
|
|
|
)
|
|
|
|
|
|
|
|
var dblQuotedReplacer = strings.NewReplacer(
|
|
|
|
"\"", "\\\"",
|
|
|
|
"\\", "\\\\",
|
|
|
|
"\x00", `\u0000`,
|
|
|
|
"\x01", `\u0001`,
|
|
|
|
"\x02", `\u0002`,
|
|
|
|
"\x03", `\u0003`,
|
|
|
|
"\x04", `\u0004`,
|
|
|
|
"\x05", `\u0005`,
|
|
|
|
"\x06", `\u0006`,
|
|
|
|
"\x07", `\u0007`,
|
|
|
|
"\b", `\b`,
|
|
|
|
"\t", `\t`,
|
|
|
|
"\n", `\n`,
|
|
|
|
"\x0b", `\u000b`,
|
|
|
|
"\f", `\f`,
|
|
|
|
"\r", `\r`,
|
|
|
|
"\x0e", `\u000e`,
|
|
|
|
"\x0f", `\u000f`,
|
|
|
|
"\x10", `\u0010`,
|
|
|
|
"\x11", `\u0011`,
|
|
|
|
"\x12", `\u0012`,
|
|
|
|
"\x13", `\u0013`,
|
|
|
|
"\x14", `\u0014`,
|
|
|
|
"\x15", `\u0015`,
|
|
|
|
"\x16", `\u0016`,
|
|
|
|
"\x17", `\u0017`,
|
|
|
|
"\x18", `\u0018`,
|
|
|
|
"\x19", `\u0019`,
|
|
|
|
"\x1a", `\u001a`,
|
|
|
|
"\x1b", `\u001b`,
|
|
|
|
"\x1c", `\u001c`,
|
|
|
|
"\x1d", `\u001d`,
|
|
|
|
"\x1e", `\u001e`,
|
|
|
|
"\x1f", `\u001f`,
|
|
|
|
"\x7f", `\u007f`,
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem()
|
|
|
|
marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
|
|
|
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
|
|
|
)
|
|
|
|
|
|
|
|
// Marshaler is the interface implemented by types that can marshal themselves
|
|
|
|
// into valid TOML.
|
|
|
|
type Marshaler interface {
|
|
|
|
MarshalTOML() ([]byte, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Encoder encodes a Go to a TOML document.
|
|
|
|
//
|
|
|
|
// The mapping between Go values and TOML values should be precisely the same as
|
|
|
|
// for [Decode].
|
|
|
|
//
|
|
|
|
// time.Time is encoded as a RFC 3339 string, and time.Duration as its string
|
|
|
|
// representation.
|
|
|
|
//
|
|
|
|
// The [Marshaler] and [encoding.TextMarshaler] interfaces are supported to
|
|
|
|
// encoding the value as custom TOML.
|
|
|
|
//
|
|
|
|
// If you want to write arbitrary binary data then you will need to use
|
|
|
|
// something like base64 since TOML does not have any binary types.
|
|
|
|
//
|
|
|
|
// When encoding TOML hashes (Go maps or structs), keys without any sub-hashes
|
|
|
|
// are encoded first.
|
|
|
|
//
|
|
|
|
// Go maps will be sorted alphabetically by key for deterministic output.
|
|
|
|
//
|
|
|
|
// The toml struct tag can be used to provide the key name; if omitted the
|
|
|
|
// struct field name will be used. If the "omitempty" option is present the
|
|
|
|
// following value will be skipped:
|
|
|
|
//
|
|
|
|
// - arrays, slices, maps, and string with len of 0
|
|
|
|
// - struct with all zero values
|
|
|
|
// - bool false
|
|
|
|
//
|
|
|
|
// If omitzero is given all int and float types with a value of 0 will be
|
|
|
|
// skipped.
|
|
|
|
//
|
|
|
|
// Encoding Go values without a corresponding TOML representation will return an
|
|
|
|
// error. Examples of this includes maps with non-string keys, slices with nil
|
|
|
|
// elements, embedded non-struct types, and nested slices containing maps or
|
|
|
|
// structs. (e.g. [][]map[string]string is not allowed but []map[string]string
|
|
|
|
// is okay, as is []map[string][]string).
|
|
|
|
//
|
|
|
|
// NOTE: only exported keys are encoded due to the use of reflection. Unexported
|
|
|
|
// keys are silently discarded.
|
|
|
|
type Encoder struct {
|
|
|
|
// String to use for a single indentation level; default is two spaces.
|
|
|
|
Indent string
|
|
|
|
|
|
|
|
w *bufio.Writer
|
|
|
|
hasWritten bool // written any output to w yet?
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewEncoder create a new Encoder.
|
|
|
|
func NewEncoder(w io.Writer) *Encoder {
|
|
|
|
return &Encoder{
|
|
|
|
w: bufio.NewWriter(w),
|
|
|
|
Indent: " ",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Encode writes a TOML representation of the Go value to the [Encoder]'s writer.
|
|
|
|
//
|
|
|
|
// An error is returned if the value given cannot be encoded to a valid TOML
|
|
|
|
// document.
|
|
|
|
func (enc *Encoder) Encode(v interface{}) error {
|
|
|
|
rv := eindirect(reflect.ValueOf(v))
|
2024-05-15 23:15:00 +00:00
|
|
|
err := enc.safeEncode(Key([]string{}), rv)
|
|
|
|
if err != nil {
|
2022-10-31 15:52:21 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
return enc.w.Flush()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
|
|
|
|
defer func() {
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
if terr, ok := r.(tomlEncodeError); ok {
|
|
|
|
err = terr.error
|
|
|
|
return
|
|
|
|
}
|
|
|
|
panic(r)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
enc.encode(key, rv)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
|
|
|
// If we can marshal the type to text, then we use that. This prevents the
|
|
|
|
// encoder for handling these types as generic structs (or whatever the
|
|
|
|
// underlying type of a TextMarshaler is).
|
|
|
|
switch {
|
|
|
|
case isMarshaler(rv):
|
|
|
|
enc.writeKeyValue(key, rv, false)
|
|
|
|
return
|
|
|
|
case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented.
|
|
|
|
enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
k := rv.Kind()
|
|
|
|
switch k {
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
|
|
|
reflect.Int64,
|
|
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
|
|
|
reflect.Uint64,
|
|
|
|
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
|
|
|
|
enc.writeKeyValue(key, rv, false)
|
|
|
|
case reflect.Array, reflect.Slice:
|
|
|
|
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
|
|
|
|
enc.eArrayOfTables(key, rv)
|
|
|
|
} else {
|
|
|
|
enc.writeKeyValue(key, rv, false)
|
|
|
|
}
|
|
|
|
case reflect.Interface:
|
|
|
|
if rv.IsNil() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
enc.encode(key, rv.Elem())
|
|
|
|
case reflect.Map:
|
|
|
|
if rv.IsNil() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
enc.eTable(key, rv)
|
|
|
|
case reflect.Ptr:
|
|
|
|
if rv.IsNil() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
enc.encode(key, rv.Elem())
|
|
|
|
case reflect.Struct:
|
|
|
|
enc.eTable(key, rv)
|
|
|
|
default:
|
|
|
|
encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// eElement encodes any value that can be an array element.
|
|
|
|
func (enc *Encoder) eElement(rv reflect.Value) {
|
|
|
|
switch v := rv.Interface().(type) {
|
|
|
|
case time.Time: // Using TextMarshaler adds extra quotes, which we don't want.
|
|
|
|
format := time.RFC3339Nano
|
|
|
|
switch v.Location() {
|
|
|
|
case internal.LocalDatetime:
|
|
|
|
format = "2006-01-02T15:04:05.999999999"
|
|
|
|
case internal.LocalDate:
|
|
|
|
format = "2006-01-02"
|
|
|
|
case internal.LocalTime:
|
|
|
|
format = "15:04:05.999999999"
|
|
|
|
}
|
|
|
|
switch v.Location() {
|
|
|
|
default:
|
|
|
|
enc.wf(v.Format(format))
|
|
|
|
case internal.LocalDatetime, internal.LocalDate, internal.LocalTime:
|
|
|
|
enc.wf(v.In(time.UTC).Format(format))
|
|
|
|
}
|
|
|
|
return
|
|
|
|
case Marshaler:
|
|
|
|
s, err := v.MarshalTOML()
|
|
|
|
if err != nil {
|
|
|
|
encPanic(err)
|
|
|
|
}
|
|
|
|
if s == nil {
|
|
|
|
encPanic(errors.New("MarshalTOML returned nil and no error"))
|
|
|
|
}
|
|
|
|
enc.w.Write(s)
|
|
|
|
return
|
|
|
|
case encoding.TextMarshaler:
|
|
|
|
s, err := v.MarshalText()
|
|
|
|
if err != nil {
|
|
|
|
encPanic(err)
|
|
|
|
}
|
|
|
|
if s == nil {
|
|
|
|
encPanic(errors.New("MarshalText returned nil and no error"))
|
|
|
|
}
|
|
|
|
enc.writeQuoted(string(s))
|
|
|
|
return
|
|
|
|
case time.Duration:
|
|
|
|
enc.writeQuoted(v.String())
|
|
|
|
return
|
|
|
|
case json.Number:
|
|
|
|
n, _ := rv.Interface().(json.Number)
|
|
|
|
|
|
|
|
if n == "" { /// Useful zero value.
|
|
|
|
enc.w.WriteByte('0')
|
|
|
|
return
|
|
|
|
} else if v, err := n.Int64(); err == nil {
|
|
|
|
enc.eElement(reflect.ValueOf(v))
|
|
|
|
return
|
|
|
|
} else if v, err := n.Float64(); err == nil {
|
|
|
|
enc.eElement(reflect.ValueOf(v))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
encPanic(fmt.Errorf("unable to convert %q to int64 or float64", n))
|
|
|
|
}
|
|
|
|
|
|
|
|
switch rv.Kind() {
|
|
|
|
case reflect.Ptr:
|
|
|
|
enc.eElement(rv.Elem())
|
|
|
|
return
|
|
|
|
case reflect.String:
|
|
|
|
enc.writeQuoted(rv.String())
|
|
|
|
case reflect.Bool:
|
|
|
|
enc.wf(strconv.FormatBool(rv.Bool()))
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
|
|
enc.wf(strconv.FormatInt(rv.Int(), 10))
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
enc.wf(strconv.FormatUint(rv.Uint(), 10))
|
|
|
|
case reflect.Float32:
|
|
|
|
f := rv.Float()
|
|
|
|
if math.IsNaN(f) {
|
|
|
|
enc.wf("nan")
|
|
|
|
} else if math.IsInf(f, 0) {
|
|
|
|
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
|
|
|
|
} else {
|
|
|
|
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
|
|
|
|
}
|
|
|
|
case reflect.Float64:
|
|
|
|
f := rv.Float()
|
|
|
|
if math.IsNaN(f) {
|
|
|
|
enc.wf("nan")
|
|
|
|
} else if math.IsInf(f, 0) {
|
|
|
|
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
|
|
|
|
} else {
|
|
|
|
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
|
|
|
|
}
|
|
|
|
case reflect.Array, reflect.Slice:
|
|
|
|
enc.eArrayOrSliceElement(rv)
|
|
|
|
case reflect.Struct:
|
|
|
|
enc.eStruct(nil, rv, true)
|
|
|
|
case reflect.Map:
|
|
|
|
enc.eMap(nil, rv, true)
|
|
|
|
case reflect.Interface:
|
|
|
|
enc.eElement(rv.Elem())
|
|
|
|
default:
|
|
|
|
encPanic(fmt.Errorf("unexpected type: %T", rv.Interface()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// By the TOML spec, all floats must have a decimal with at least one number on
|
|
|
|
// either side.
|
|
|
|
func floatAddDecimal(fstr string) string {
|
|
|
|
if !strings.Contains(fstr, ".") {
|
|
|
|
return fstr + ".0"
|
|
|
|
}
|
|
|
|
return fstr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) writeQuoted(s string) {
|
|
|
|
enc.wf("\"%s\"", dblQuotedReplacer.Replace(s))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
|
|
|
|
length := rv.Len()
|
|
|
|
enc.wf("[")
|
|
|
|
for i := 0; i < length; i++ {
|
|
|
|
elem := eindirect(rv.Index(i))
|
|
|
|
enc.eElement(elem)
|
|
|
|
if i != length-1 {
|
|
|
|
enc.wf(", ")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
enc.wf("]")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
|
|
|
|
if len(key) == 0 {
|
|
|
|
encPanic(errNoKey)
|
|
|
|
}
|
|
|
|
for i := 0; i < rv.Len(); i++ {
|
|
|
|
trv := eindirect(rv.Index(i))
|
|
|
|
if isNil(trv) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
enc.newline()
|
|
|
|
enc.wf("%s[[%s]]", enc.indentStr(key), key)
|
|
|
|
enc.newline()
|
|
|
|
enc.eMapOrStruct(key, trv, false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
|
|
|
if len(key) == 1 {
|
|
|
|
// Output an extra newline between top-level tables.
|
|
|
|
// (The newline isn't written if nothing else has been written though.)
|
|
|
|
enc.newline()
|
|
|
|
}
|
|
|
|
if len(key) > 0 {
|
|
|
|
enc.wf("%s[%s]", enc.indentStr(key), key)
|
|
|
|
enc.newline()
|
|
|
|
}
|
|
|
|
enc.eMapOrStruct(key, rv, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) {
|
|
|
|
switch rv.Kind() {
|
|
|
|
case reflect.Map:
|
|
|
|
enc.eMap(key, rv, inline)
|
|
|
|
case reflect.Struct:
|
|
|
|
enc.eStruct(key, rv, inline)
|
|
|
|
default:
|
|
|
|
// Should never happen?
|
|
|
|
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
|
|
|
|
rt := rv.Type()
|
|
|
|
if rt.Key().Kind() != reflect.String {
|
|
|
|
encPanic(errNonString)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort keys so that we have deterministic output. And write keys directly
|
|
|
|
// underneath this key first, before writing sub-structs or sub-maps.
|
|
|
|
var mapKeysDirect, mapKeysSub []string
|
|
|
|
for _, mapKey := range rv.MapKeys() {
|
|
|
|
k := mapKey.String()
|
|
|
|
if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) {
|
|
|
|
mapKeysSub = append(mapKeysSub, k)
|
|
|
|
} else {
|
|
|
|
mapKeysDirect = append(mapKeysDirect, k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var writeMapKeys = func(mapKeys []string, trailC bool) {
|
|
|
|
sort.Strings(mapKeys)
|
|
|
|
for i, mapKey := range mapKeys {
|
|
|
|
val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey)))
|
|
|
|
if isNil(val) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if inline {
|
|
|
|
enc.writeKeyValue(Key{mapKey}, val, true)
|
|
|
|
if trailC || i != len(mapKeys)-1 {
|
|
|
|
enc.wf(", ")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
enc.encode(key.add(mapKey), val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if inline {
|
|
|
|
enc.wf("{")
|
|
|
|
}
|
|
|
|
writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0)
|
|
|
|
writeMapKeys(mapKeysSub, false)
|
|
|
|
if inline {
|
|
|
|
enc.wf("}")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const is32Bit = (32 << (^uint(0) >> 63)) == 32
|
|
|
|
|
|
|
|
func pointerTo(t reflect.Type) reflect.Type {
|
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
return pointerTo(t.Elem())
|
|
|
|
}
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
|
|
|
|
// Write keys for fields directly under this key first, because if we write
|
|
|
|
// a field that creates a new table then all keys under it will be in that
|
|
|
|
// table (not the one we're writing here).
|
|
|
|
//
|
|
|
|
// Fields is a [][]int: for fieldsDirect this always has one entry (the
|
|
|
|
// struct index). For fieldsSub it contains two entries: the parent field
|
|
|
|
// index from tv, and the field indexes for the fields of the sub.
|
|
|
|
var (
|
|
|
|
rt = rv.Type()
|
|
|
|
fieldsDirect, fieldsSub [][]int
|
|
|
|
addFields func(rt reflect.Type, rv reflect.Value, start []int)
|
|
|
|
)
|
|
|
|
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
|
|
|
|
for i := 0; i < rt.NumField(); i++ {
|
|
|
|
f := rt.Field(i)
|
|
|
|
isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct
|
|
|
|
if f.PkgPath != "" && !isEmbed { /// Skip unexported fields.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
opts := getOptions(f.Tag)
|
|
|
|
if opts.skip {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
frv := eindirect(rv.Field(i))
|
|
|
|
|
2024-05-15 23:15:00 +00:00
|
|
|
if is32Bit {
|
|
|
|
// Copy so it works correct on 32bit archs; not clear why this
|
|
|
|
// is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4
|
|
|
|
// This also works fine on 64bit, but 32bit archs are somewhat
|
|
|
|
// rare and this is a wee bit faster.
|
|
|
|
copyStart := make([]int, len(start))
|
|
|
|
copy(copyStart, start)
|
|
|
|
start = copyStart
|
|
|
|
}
|
|
|
|
|
2022-10-31 15:52:21 +00:00
|
|
|
// Treat anonymous struct fields with tag names as though they are
|
|
|
|
// not anonymous, like encoding/json does.
|
|
|
|
//
|
|
|
|
// Non-struct anonymous fields use the normal encoding logic.
|
|
|
|
if isEmbed {
|
|
|
|
if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct {
|
|
|
|
addFields(frv.Type(), frv, append(start, f.Index...))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if typeIsTable(tomlTypeOfGo(frv)) {
|
|
|
|
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
|
|
|
} else {
|
2024-05-15 23:15:00 +00:00
|
|
|
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
2022-10-31 15:52:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
addFields(rt, rv, nil)
|
|
|
|
|
|
|
|
writeFields := func(fields [][]int) {
|
|
|
|
for _, fieldIndex := range fields {
|
|
|
|
fieldType := rt.FieldByIndex(fieldIndex)
|
2024-05-15 23:15:00 +00:00
|
|
|
fieldVal := rv.FieldByIndex(fieldIndex)
|
2022-10-31 15:52:21 +00:00
|
|
|
|
2024-05-15 23:15:00 +00:00
|
|
|
opts := getOptions(fieldType.Tag)
|
|
|
|
if opts.skip {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if opts.omitempty && isEmpty(fieldVal) {
|
2022-10-31 15:52:21 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-05-15 23:15:00 +00:00
|
|
|
fieldVal = eindirect(fieldVal)
|
|
|
|
|
|
|
|
if isNil(fieldVal) { /// Don't write anything for nil fields.
|
2022-10-31 15:52:21 +00:00
|
|
|
continue
|
|
|
|
}
|
2024-05-15 23:15:00 +00:00
|
|
|
|
2022-10-31 15:52:21 +00:00
|
|
|
keyName := fieldType.Name
|
|
|
|
if opts.name != "" {
|
|
|
|
keyName = opts.name
|
|
|
|
}
|
|
|
|
|
|
|
|
if opts.omitzero && isZero(fieldVal) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if inline {
|
|
|
|
enc.writeKeyValue(Key{keyName}, fieldVal, true)
|
|
|
|
if fieldIndex[0] != len(fields)-1 {
|
|
|
|
enc.wf(", ")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
enc.encode(key.add(keyName), fieldVal)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if inline {
|
|
|
|
enc.wf("{")
|
|
|
|
}
|
|
|
|
writeFields(fieldsDirect)
|
|
|
|
writeFields(fieldsSub)
|
|
|
|
if inline {
|
|
|
|
enc.wf("}")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// tomlTypeOfGo returns the TOML type name of the Go value's type.
|
|
|
|
//
|
|
|
|
// It is used to determine whether the types of array elements are mixed (which
|
|
|
|
// is forbidden). If the Go value is nil, then it is illegal for it to be an
|
|
|
|
// array element, and valueIsNil is returned as true.
|
|
|
|
//
|
|
|
|
// The type may be `nil`, which means no concrete TOML type could be found.
|
|
|
|
func tomlTypeOfGo(rv reflect.Value) tomlType {
|
|
|
|
if isNil(rv) || !rv.IsValid() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if rv.Kind() == reflect.Struct {
|
|
|
|
if rv.Type() == timeType {
|
|
|
|
return tomlDatetime
|
|
|
|
}
|
|
|
|
if isMarshaler(rv) {
|
|
|
|
return tomlString
|
|
|
|
}
|
|
|
|
return tomlHash
|
|
|
|
}
|
|
|
|
|
|
|
|
if isMarshaler(rv) {
|
|
|
|
return tomlString
|
|
|
|
}
|
|
|
|
|
|
|
|
switch rv.Kind() {
|
|
|
|
case reflect.Bool:
|
|
|
|
return tomlBool
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32,
|
|
|
|
reflect.Int64,
|
|
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
|
|
|
reflect.Uint64:
|
|
|
|
return tomlInteger
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
|
|
return tomlFloat
|
|
|
|
case reflect.Array, reflect.Slice:
|
|
|
|
if isTableArray(rv) {
|
|
|
|
return tomlArrayHash
|
|
|
|
}
|
|
|
|
return tomlArray
|
|
|
|
case reflect.Ptr, reflect.Interface:
|
|
|
|
return tomlTypeOfGo(rv.Elem())
|
|
|
|
case reflect.String:
|
|
|
|
return tomlString
|
|
|
|
case reflect.Map:
|
|
|
|
return tomlHash
|
|
|
|
default:
|
|
|
|
encPanic(errors.New("unsupported type: " + rv.Kind().String()))
|
|
|
|
panic("unreachable")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func isMarshaler(rv reflect.Value) bool {
|
|
|
|
return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml)
|
|
|
|
}
|
|
|
|
|
|
|
|
// isTableArray reports if all entries in the array or slice are a table.
|
|
|
|
func isTableArray(arr reflect.Value) bool {
|
|
|
|
if isNil(arr) || !arr.IsValid() || arr.Len() == 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
ret := true
|
|
|
|
for i := 0; i < arr.Len(); i++ {
|
|
|
|
tt := tomlTypeOfGo(eindirect(arr.Index(i)))
|
|
|
|
// Don't allow nil.
|
|
|
|
if tt == nil {
|
|
|
|
encPanic(errArrayNilElement)
|
|
|
|
}
|
|
|
|
|
|
|
|
if ret && !typeEqual(tomlHash, tt) {
|
|
|
|
ret = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
type tagOptions struct {
|
|
|
|
skip bool // "-"
|
|
|
|
name string
|
|
|
|
omitempty bool
|
|
|
|
omitzero bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func getOptions(tag reflect.StructTag) tagOptions {
|
|
|
|
t := tag.Get("toml")
|
|
|
|
if t == "-" {
|
|
|
|
return tagOptions{skip: true}
|
|
|
|
}
|
|
|
|
var opts tagOptions
|
|
|
|
parts := strings.Split(t, ",")
|
|
|
|
opts.name = parts[0]
|
|
|
|
for _, s := range parts[1:] {
|
|
|
|
switch s {
|
|
|
|
case "omitempty":
|
|
|
|
opts.omitempty = true
|
|
|
|
case "omitzero":
|
|
|
|
opts.omitzero = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return opts
|
|
|
|
}
|
|
|
|
|
|
|
|
func isZero(rv reflect.Value) bool {
|
|
|
|
switch rv.Kind() {
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
|
|
return rv.Int() == 0
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
return rv.Uint() == 0
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
|
|
return rv.Float() == 0.0
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2024-05-15 23:15:00 +00:00
|
|
|
func isEmpty(rv reflect.Value) bool {
|
2022-10-31 15:52:21 +00:00
|
|
|
switch rv.Kind() {
|
|
|
|
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
|
|
|
return rv.Len() == 0
|
|
|
|
case reflect.Struct:
|
|
|
|
if rv.Type().Comparable() {
|
|
|
|
return reflect.Zero(rv.Type()).Interface() == rv.Interface()
|
|
|
|
}
|
|
|
|
// Need to also check if all the fields are empty, otherwise something
|
|
|
|
// like this with uncomparable types will always return true:
|
|
|
|
//
|
|
|
|
// type a struct{ field b }
|
|
|
|
// type b struct{ s []string }
|
|
|
|
// s := a{field: b{s: []string{"AAA"}}}
|
|
|
|
for i := 0; i < rv.NumField(); i++ {
|
2024-05-15 23:15:00 +00:00
|
|
|
if !isEmpty(rv.Field(i)) {
|
2022-10-31 15:52:21 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
case reflect.Bool:
|
|
|
|
return !rv.Bool()
|
2024-05-15 23:15:00 +00:00
|
|
|
case reflect.Ptr:
|
|
|
|
return rv.IsNil()
|
2022-10-31 15:52:21 +00:00
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) newline() {
|
|
|
|
if enc.hasWritten {
|
|
|
|
enc.wf("\n")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write a key/value pair:
|
|
|
|
//
|
|
|
|
// key = <any value>
|
|
|
|
//
|
|
|
|
// This is also used for "k = v" in inline tables; so something like this will
|
|
|
|
// be written in three calls:
|
|
|
|
//
|
|
|
|
// ┌───────────────────┐
|
|
|
|
// │ ┌───┐ ┌────┐│
|
|
|
|
// v v v v vv
|
|
|
|
// key = {k = 1, k2 = 2}
|
|
|
|
func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
|
2024-05-15 23:15:00 +00:00
|
|
|
/// Marshaler used on top-level document; call eElement() to just call
|
|
|
|
/// Marshal{TOML,Text}.
|
2022-10-31 15:52:21 +00:00
|
|
|
if len(key) == 0 {
|
2024-05-15 23:15:00 +00:00
|
|
|
enc.eElement(val)
|
|
|
|
return
|
2022-10-31 15:52:21 +00:00
|
|
|
}
|
|
|
|
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
|
|
|
enc.eElement(val)
|
|
|
|
if !inline {
|
|
|
|
enc.newline()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) wf(format string, v ...interface{}) {
|
|
|
|
_, err := fmt.Fprintf(enc.w, format, v...)
|
|
|
|
if err != nil {
|
|
|
|
encPanic(err)
|
|
|
|
}
|
|
|
|
enc.hasWritten = true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (enc *Encoder) indentStr(key Key) string {
|
|
|
|
return strings.Repeat(enc.Indent, len(key)-1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func encPanic(err error) {
|
|
|
|
panic(tomlEncodeError{err})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resolve any level of pointers to the actual value (e.g. **string → string).
|
|
|
|
func eindirect(v reflect.Value) reflect.Value {
|
|
|
|
if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
|
|
|
|
if isMarshaler(v) {
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
if v.CanAddr() { /// Special case for marshalers; see #358.
|
|
|
|
if pv := v.Addr(); isMarshaler(pv) {
|
|
|
|
return pv
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
|
|
|
if v.IsNil() {
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
|
|
|
return eindirect(v.Elem())
|
|
|
|
}
|
|
|
|
|
|
|
|
func isNil(rv reflect.Value) bool {
|
|
|
|
switch rv.Kind() {
|
|
|
|
case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
|
|
|
return rv.IsNil()
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|