568 lines
15 KiB
Go
568 lines
15 KiB
Go
package toml
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type tomlEncodeError struct{ error }
|
|
|
|
var (
|
|
errArrayMixedElementTypes = errors.New(
|
|
"toml: cannot encode array with mixed element types")
|
|
errArrayNilElement = errors.New(
|
|
"toml: cannot encode array with nil element")
|
|
errNonString = errors.New(
|
|
"toml: cannot encode a map with non-string key type")
|
|
errAnonNonStruct = errors.New(
|
|
"toml: cannot encode an anonymous field that is not a struct")
|
|
errArrayNoTable = errors.New(
|
|
"toml: TOML array element cannot contain a table")
|
|
errNoKey = errors.New(
|
|
"toml: top-level values must be Go maps or structs")
|
|
errAnything = errors.New("") // used in testing
|
|
)
|
|
|
|
var quotedReplacer = strings.NewReplacer(
|
|
"\t", "\\t",
|
|
"\n", "\\n",
|
|
"\r", "\\r",
|
|
"\"", "\\\"",
|
|
"\\", "\\\\",
|
|
)
|
|
|
|
// Encoder controls the encoding of Go values to a TOML document to some
|
|
// io.Writer.
|
|
//
|
|
// The indentation level can be controlled with the Indent field.
|
|
type Encoder struct {
|
|
// A single indentation level. By default it is two spaces.
|
|
Indent string
|
|
|
|
// hasWritten is whether we have written any output to w yet.
|
|
hasWritten bool
|
|
w *bufio.Writer
|
|
}
|
|
|
|
// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer
|
|
// given. By default, a single indentation level is 2 spaces.
|
|
func NewEncoder(w io.Writer) *Encoder {
|
|
return &Encoder{
|
|
w: bufio.NewWriter(w),
|
|
Indent: " ",
|
|
}
|
|
}
|
|
|
|
// Encode writes a TOML representation of the Go value to the underlying
|
|
// io.Writer. If the value given cannot be encoded to a valid TOML document,
|
|
// then an error is returned.
|
|
//
|
|
// The mapping between Go values and TOML values should be precisely the same
|
|
// as for the Decode* functions. Similarly, the TextMarshaler interface is
|
|
// supported by encoding the resulting bytes as strings. (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 (i.e., Go maps or structs), keys without any
|
|
// sub-hashes are encoded first.
|
|
//
|
|
// If a Go map is encoded, then its keys are sorted alphabetically for
|
|
// deterministic output. More control over this behavior may be provided if
|
|
// there is demand for it.
|
|
//
|
|
// Encoding Go values without a corresponding TOML representation---like map
|
|
// types with non-string keys---will cause an error to be returned. Similarly
|
|
// for mixed arrays/slices, arrays/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 OK
|
|
// and so is []map[string][]string.)
|
|
func (enc *Encoder) Encode(v interface{}) error {
|
|
rv := eindirect(reflect.ValueOf(v))
|
|
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
|
|
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) {
|
|
// Special case. Time needs to be in ISO8601 format.
|
|
// Special case. If we can marshal the type to text, then we used that.
|
|
// Basically, this prevents the encoder for handling these types as
|
|
// generic structs (or whatever the underlying type of a TextMarshaler is).
|
|
switch rv.Interface().(type) {
|
|
case time.Time, TextMarshaler:
|
|
enc.keyEqElement(key, rv)
|
|
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.keyEqElement(key, rv)
|
|
case reflect.Array, reflect.Slice:
|
|
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
|
|
enc.eArrayOfTables(key, rv)
|
|
} else {
|
|
enc.keyEqElement(key, rv)
|
|
}
|
|
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:
|
|
panic(e("unsupported type for key '%s': %s", key, k))
|
|
}
|
|
}
|
|
|
|
// eElement encodes any value that can be an array element (primitives and
|
|
// arrays).
|
|
func (enc *Encoder) eElement(rv reflect.Value) {
|
|
switch v := rv.Interface().(type) {
|
|
case time.Time:
|
|
// Special case time.Time as a primitive. Has to come before
|
|
// TextMarshaler below because time.Time implements
|
|
// encoding.TextMarshaler, but we need to always use UTC.
|
|
enc.wf(v.UTC().Format("2006-01-02T15:04:05Z"))
|
|
return
|
|
case TextMarshaler:
|
|
// Special case. Use text marshaler if it's available for this value.
|
|
if s, err := v.MarshalText(); err != nil {
|
|
encPanic(err)
|
|
} else {
|
|
enc.writeQuoted(string(s))
|
|
}
|
|
return
|
|
}
|
|
switch rv.Kind() {
|
|
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:
|
|
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
|
|
case reflect.Float64:
|
|
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
|
|
case reflect.Array, reflect.Slice:
|
|
enc.eArrayOrSliceElement(rv)
|
|
case reflect.Interface:
|
|
enc.eElement(rv.Elem())
|
|
case reflect.String:
|
|
enc.writeQuoted(rv.String())
|
|
default:
|
|
panic(e("unexpected primitive type: %s", rv.Kind()))
|
|
}
|
|
}
|
|
|
|
// 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\"", quotedReplacer.Replace(s))
|
|
}
|
|
|
|
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
|
|
length := rv.Len()
|
|
enc.wf("[")
|
|
for i := 0; i < length; i++ {
|
|
elem := 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 := rv.Index(i)
|
|
if isNil(trv) {
|
|
continue
|
|
}
|
|
panicIfInvalidKey(key)
|
|
enc.newline()
|
|
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
|
|
enc.newline()
|
|
enc.eMapOrStruct(key, trv)
|
|
}
|
|
}
|
|
|
|
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
|
panicIfInvalidKey(key)
|
|
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.maybeQuotedAll())
|
|
enc.newline()
|
|
}
|
|
enc.eMapOrStruct(key, rv)
|
|
}
|
|
|
|
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) {
|
|
switch rv := eindirect(rv); rv.Kind() {
|
|
case reflect.Map:
|
|
enc.eMap(key, rv)
|
|
case reflect.Struct:
|
|
enc.eStruct(key, rv)
|
|
default:
|
|
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
|
|
}
|
|
}
|
|
|
|
func (enc *Encoder) eMap(key Key, rv reflect.Value) {
|
|
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 typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
|
|
mapKeysSub = append(mapKeysSub, k)
|
|
} else {
|
|
mapKeysDirect = append(mapKeysDirect, k)
|
|
}
|
|
}
|
|
|
|
var writeMapKeys = func(mapKeys []string) {
|
|
sort.Strings(mapKeys)
|
|
for _, mapKey := range mapKeys {
|
|
mrv := rv.MapIndex(reflect.ValueOf(mapKey))
|
|
if isNil(mrv) {
|
|
// Don't write anything for nil fields.
|
|
continue
|
|
}
|
|
enc.encode(key.add(mapKey), mrv)
|
|
}
|
|
}
|
|
writeMapKeys(mapKeysDirect)
|
|
writeMapKeys(mapKeysSub)
|
|
}
|
|
|
|
func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
|
|
// 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).
|
|
rt := rv.Type()
|
|
var fieldsDirect, fieldsSub [][]int
|
|
var 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)
|
|
// skip unexported fields
|
|
if f.PkgPath != "" && !f.Anonymous {
|
|
continue
|
|
}
|
|
frv := rv.Field(i)
|
|
if f.Anonymous {
|
|
t := f.Type
|
|
switch t.Kind() {
|
|
case reflect.Struct:
|
|
// Treat anonymous struct fields with
|
|
// tag names as though they are not
|
|
// anonymous, like encoding/json does.
|
|
if getOptions(f.Tag).name == "" {
|
|
addFields(t, frv, f.Index)
|
|
continue
|
|
}
|
|
case reflect.Ptr:
|
|
if t.Elem().Kind() == reflect.Struct &&
|
|
getOptions(f.Tag).name == "" {
|
|
if !frv.IsNil() {
|
|
addFields(t.Elem(), frv.Elem(), f.Index)
|
|
}
|
|
continue
|
|
}
|
|
// Fall through to the normal field encoding logic below
|
|
// for non-struct anonymous fields.
|
|
}
|
|
}
|
|
|
|
if typeIsHash(tomlTypeOfGo(frv)) {
|
|
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
|
} else {
|
|
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
|
}
|
|
}
|
|
}
|
|
addFields(rt, rv, nil)
|
|
|
|
var writeFields = func(fields [][]int) {
|
|
for _, fieldIndex := range fields {
|
|
sft := rt.FieldByIndex(fieldIndex)
|
|
sf := rv.FieldByIndex(fieldIndex)
|
|
if isNil(sf) {
|
|
// Don't write anything for nil fields.
|
|
continue
|
|
}
|
|
|
|
opts := getOptions(sft.Tag)
|
|
if opts.skip {
|
|
continue
|
|
}
|
|
keyName := sft.Name
|
|
if opts.name != "" {
|
|
keyName = opts.name
|
|
}
|
|
if opts.omitempty && isEmpty(sf) {
|
|
continue
|
|
}
|
|
if opts.omitzero && isZero(sf) {
|
|
continue
|
|
}
|
|
|
|
enc.encode(key.add(keyName), sf)
|
|
}
|
|
}
|
|
writeFields(fieldsDirect)
|
|
writeFields(fieldsSub)
|
|
}
|
|
|
|
// tomlTypeName 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.
|
|
|
|
// Returns the TOML type of a Go value. 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
|
|
}
|
|
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 typeEqual(tomlHash, tomlArrayType(rv)) {
|
|
return tomlArrayHash
|
|
}
|
|
return tomlArray
|
|
case reflect.Ptr, reflect.Interface:
|
|
return tomlTypeOfGo(rv.Elem())
|
|
case reflect.String:
|
|
return tomlString
|
|
case reflect.Map:
|
|
return tomlHash
|
|
case reflect.Struct:
|
|
switch rv.Interface().(type) {
|
|
case time.Time:
|
|
return tomlDatetime
|
|
case TextMarshaler:
|
|
return tomlString
|
|
default:
|
|
return tomlHash
|
|
}
|
|
default:
|
|
panic("unexpected reflect.Kind: " + rv.Kind().String())
|
|
}
|
|
}
|
|
|
|
// tomlArrayType returns the element type of a TOML array. The type returned
|
|
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
|
|
// slize). This function may also panic if it finds a type that cannot be
|
|
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
|
|
// nested arrays of tables).
|
|
func tomlArrayType(rv reflect.Value) tomlType {
|
|
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
|
|
return nil
|
|
}
|
|
firstType := tomlTypeOfGo(rv.Index(0))
|
|
if firstType == nil {
|
|
encPanic(errArrayNilElement)
|
|
}
|
|
|
|
rvlen := rv.Len()
|
|
for i := 1; i < rvlen; i++ {
|
|
elem := rv.Index(i)
|
|
switch elemType := tomlTypeOfGo(elem); {
|
|
case elemType == nil:
|
|
encPanic(errArrayNilElement)
|
|
case !typeEqual(firstType, elemType):
|
|
encPanic(errArrayMixedElementTypes)
|
|
}
|
|
}
|
|
// If we have a nested array, then we must make sure that the nested
|
|
// array contains ONLY primitives.
|
|
// This checks arbitrarily nested arrays.
|
|
if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
|
|
nest := tomlArrayType(eindirect(rv.Index(0)))
|
|
if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) {
|
|
encPanic(errArrayNoTable)
|
|
}
|
|
}
|
|
return firstType
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func isEmpty(rv reflect.Value) bool {
|
|
switch rv.Kind() {
|
|
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
|
return rv.Len() == 0
|
|
case reflect.Bool:
|
|
return !rv.Bool()
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (enc *Encoder) newline() {
|
|
if enc.hasWritten {
|
|
enc.wf("\n")
|
|
}
|
|
}
|
|
|
|
func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
|
|
if len(key) == 0 {
|
|
encPanic(errNoKey)
|
|
}
|
|
panicIfInvalidKey(key)
|
|
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
|
enc.eElement(val)
|
|
enc.newline()
|
|
}
|
|
|
|
func (enc *Encoder) wf(format string, v ...interface{}) {
|
|
if _, err := fmt.Fprintf(enc.w, format, v...); 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})
|
|
}
|
|
|
|
func eindirect(v reflect.Value) reflect.Value {
|
|
switch v.Kind() {
|
|
case reflect.Ptr, reflect.Interface:
|
|
return eindirect(v.Elem())
|
|
default:
|
|
return v
|
|
}
|
|
}
|
|
|
|
func isNil(rv reflect.Value) bool {
|
|
switch rv.Kind() {
|
|
case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
|
return rv.IsNil()
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func panicIfInvalidKey(key Key) {
|
|
for _, k := range key {
|
|
if len(k) == 0 {
|
|
encPanic(e("Key '%s' is not a valid table name. Key names "+
|
|
"cannot be empty.", key.maybeQuotedAll()))
|
|
}
|
|
}
|
|
}
|
|
|
|
func isValidKeyName(s string) bool {
|
|
return len(s) != 0
|
|
}
|