625 lines
12 KiB
Go
625 lines
12 KiB
Go
package toml
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 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 {
|
|
w io.Writer
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// NewEncoder returns a new Encoder that writes to w.
|
|
func NewEncoder(w io.Writer) *Encoder {
|
|
return &Encoder{
|
|
w: w,
|
|
}
|
|
}
|
|
|
|
// Encode writes a TOML representation of v to the stream.
|
|
//
|
|
// If v cannot be represented to TOML it returns an error.
|
|
//
|
|
// Encoding rules:
|
|
//
|
|
// 1. A top level slice containing only maps or structs is encoded as [[table
|
|
// array]].
|
|
//
|
|
// 2. 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}.
|
|
//
|
|
// 3. Nil interfaces and nil pointers are not supported.
|
|
//
|
|
// 4. Keys in key-values always have one part.
|
|
//
|
|
// 5. Intermediate tables are always printed.
|
|
func (enc *Encoder) Encode(v interface{}) error {
|
|
var b []byte
|
|
var ctx encoderCtx
|
|
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = enc.w.Write(b)
|
|
return err
|
|
}
|
|
|
|
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
switch i := v.Interface().(type) {
|
|
case time.Time: // TODO: add TextMarshaler
|
|
b = i.AppendFormat(b, time.RFC3339)
|
|
return b, nil
|
|
}
|
|
|
|
// containers
|
|
switch v.Kind() {
|
|
case reflect.Map:
|
|
return enc.encodeMap(b, ctx, v)
|
|
case reflect.Struct:
|
|
return enc.encodeStruct(b, ctx, v)
|
|
case reflect.Slice:
|
|
return enc.encodeSlice(b, ctx, v)
|
|
case reflect.Interface:
|
|
if v.IsNil() {
|
|
return nil, errNilInterface
|
|
}
|
|
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
|
|
var err error
|
|
switch v.Kind() {
|
|
case reflect.String:
|
|
b, err = enc.encodeString(b, v.String())
|
|
case reflect.Float32:
|
|
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32)
|
|
case reflect.Float64:
|
|
b = strconv.AppendFloat(b, v.Float(), '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:
|
|
b = strconv.AppendUint(b, v.Uint(), 10)
|
|
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
|
|
b = strconv.AppendInt(b, v.Int(), 10)
|
|
default:
|
|
err = fmt.Errorf("unsupported encode value kind: %s", v.Kind())
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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 (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
var err error
|
|
|
|
if !ctx.hasKey {
|
|
panic("caller of encodeKv should have set the key in the context")
|
|
}
|
|
|
|
if isNil(v) {
|
|
return b, nil
|
|
}
|
|
|
|
b, err = enc.encodeKey(b, ctx.key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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()
|
|
|
|
b, err = enc.encode(b, subctx, v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
const literalQuote = '\''
|
|
|
|
func (enc *Encoder) encodeString(b []byte, v string) ([]byte, error) {
|
|
if needsQuoting(v) {
|
|
b = enc.encodeQuotedString(b, v)
|
|
} else {
|
|
b = enc.encodeLiteralString(b, v)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func needsQuoting(v string) bool {
|
|
return strings.ContainsAny(v, "'\b\f\n\r\t")
|
|
}
|
|
|
|
// 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(b []byte, v string) []byte {
|
|
const stringQuote = '"'
|
|
const hextable = "0123456789ABCDEF"
|
|
|
|
b = append(b, stringQuote)
|
|
|
|
for _, r := range []byte(v) {
|
|
switch r {
|
|
case '\\':
|
|
b = append(b, `\\`...)
|
|
case '"':
|
|
b = append(b, `\"`...)
|
|
case '\b':
|
|
b = append(b, `\b`...)
|
|
case '\f':
|
|
b = append(b, `\f`...)
|
|
case '\n':
|
|
b = append(b, `\n`...)
|
|
case '\r':
|
|
b = append(b, `\r`...)
|
|
case '\t':
|
|
b = append(b, `\t`...)
|
|
default:
|
|
switch {
|
|
case r >= 0x0 && r <= 0x8, r >= 0xA && r <= 0x1F, r == 0x7F:
|
|
b = append(b, `\u00`...)
|
|
b = append(b, hextable[r>>4])
|
|
b = append(b, hextable[r&0x0f])
|
|
default:
|
|
b = append(b, r)
|
|
}
|
|
}
|
|
// U+0000 to U+0008, U+000A to U+001F, U+007F
|
|
}
|
|
|
|
b = append(b, stringQuote)
|
|
return b
|
|
}
|
|
|
|
// called 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(b []byte, key []string) ([]byte, error) {
|
|
if len(key) == 0 {
|
|
return b, nil
|
|
}
|
|
|
|
b = append(b, '[')
|
|
|
|
var err error
|
|
b, err = enc.encodeKey(b, key[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, k := range key[1:] {
|
|
b = append(b, '.')
|
|
b, err = enc.encodeKey(b, k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
b = append(b, "]\n"...)
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
|
|
needsQuotation := false
|
|
cannotUseLiteral := false
|
|
|
|
for _, c := range k {
|
|
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
|
|
continue
|
|
}
|
|
if c == '\n' {
|
|
return nil, fmt.Errorf("TOML does not support multiline keys")
|
|
}
|
|
if c == literalQuote {
|
|
cannotUseLiteral = true
|
|
}
|
|
needsQuotation = true
|
|
}
|
|
|
|
if cannotUseLiteral {
|
|
b = enc.encodeQuotedString(b, k)
|
|
} else if needsQuotation {
|
|
b = enc.encodeLiteralString(b, k)
|
|
} else {
|
|
b = enc.encodeUnquotedKey(b, k)
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
if v.Type().Key().Kind() != reflect.String {
|
|
return nil, fmt.Errorf("type '%s' not supported as map key", v.Type().Key().Kind())
|
|
}
|
|
|
|
t := table{}
|
|
|
|
iter := v.MapRange()
|
|
for iter.Next() {
|
|
k := iter.Key().String()
|
|
v := iter.Value()
|
|
|
|
if isNil(v) {
|
|
continue
|
|
}
|
|
|
|
table, err := willConvertToTableOrArrayTable(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if table {
|
|
t.pushTable(k, v)
|
|
} else {
|
|
t.pushKV(k, v)
|
|
}
|
|
}
|
|
|
|
sortEntriesByKey(t.kvs)
|
|
sortEntriesByKey(t.tables)
|
|
|
|
return enc.encodeTable(b, ctx, t)
|
|
}
|
|
|
|
func sortEntriesByKey(e []entry) {
|
|
sort.Slice(e, func(i, j int) bool {
|
|
return e[i].Key < e[j].Key
|
|
})
|
|
}
|
|
|
|
type entry struct {
|
|
Key string
|
|
Value reflect.Value
|
|
}
|
|
|
|
type table struct {
|
|
kvs []entry
|
|
tables []entry
|
|
}
|
|
|
|
func (t *table) pushKV(k string, v reflect.Value) {
|
|
t.kvs = append(t.kvs, entry{Key: k, Value: v})
|
|
}
|
|
|
|
func (t *table) pushTable(k string, v reflect.Value) {
|
|
t.tables = append(t.tables, entry{Key: k, Value: v})
|
|
}
|
|
|
|
func (t *table) hasKVs() bool {
|
|
return len(t.kvs) > 0
|
|
}
|
|
|
|
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
t := table{}
|
|
|
|
// 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
|
|
}
|
|
|
|
k, ok := fieldType.Tag.Lookup("toml")
|
|
if !ok {
|
|
k = fieldType.Name
|
|
}
|
|
|
|
// special field name to skip field
|
|
if k == "-" {
|
|
continue
|
|
}
|
|
|
|
f := v.Field(i)
|
|
|
|
if isNil(f) {
|
|
continue
|
|
}
|
|
|
|
willConvert, err := willConvertToTableOrArrayTable(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if willConvert {
|
|
t.pushTable(k, f)
|
|
} else {
|
|
t.pushKV(k, f)
|
|
}
|
|
}
|
|
|
|
return enc.encodeTable(b, ctx, t)
|
|
}
|
|
|
|
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
|
|
var err error
|
|
|
|
ctx.shiftKey()
|
|
|
|
if ctx.insideKv {
|
|
b = append(b, '{')
|
|
|
|
first := true
|
|
for _, kv := range t.kvs {
|
|
if first {
|
|
first = false
|
|
} else {
|
|
b = append(b, `, `...)
|
|
}
|
|
ctx.setKey(kv.Key)
|
|
b, err = enc.encodeKv(b, ctx, kv.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for _, table := range t.tables {
|
|
if first {
|
|
first = false
|
|
} else {
|
|
b = append(b, `, `...)
|
|
}
|
|
ctx.setKey(table.Key)
|
|
b, err = enc.encode(b, ctx, table.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b = append(b, '\n')
|
|
}
|
|
|
|
b = append(b, "}\n"...)
|
|
return b, nil
|
|
}
|
|
|
|
if !ctx.skipTableHeader {
|
|
b, err = enc.encodeTableHeader(b, ctx.parentKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
ctx.skipTableHeader = false
|
|
|
|
for _, kv := range t.kvs {
|
|
ctx.setKey(kv.Key)
|
|
b, err = enc.encodeKv(b, ctx, kv.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b = append(b, '\n')
|
|
}
|
|
|
|
for _, table := range t.tables {
|
|
ctx.setKey(table.Key)
|
|
b, err = enc.encode(b, ctx, table.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b = append(b, '\n')
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
var errNilInterface = errors.New("nil interface not supported")
|
|
var errNilPointer = errors.New("nil pointer not supported")
|
|
|
|
func willConvertToTable(v reflect.Value) (bool, error) {
|
|
switch v.Interface().(type) {
|
|
case time.Time: // TODO: add TextMarshaler
|
|
return false, nil
|
|
}
|
|
|
|
t := v.Type()
|
|
switch t.Kind() {
|
|
case reflect.Map, reflect.Struct:
|
|
return true, nil
|
|
case reflect.Interface:
|
|
if v.IsNil() {
|
|
return false, errNilInterface
|
|
}
|
|
return willConvertToTable(v.Elem())
|
|
case reflect.Ptr:
|
|
if v.IsNil() {
|
|
return false, nil
|
|
}
|
|
return willConvertToTable(v.Elem())
|
|
default:
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
|
|
t := v.Type()
|
|
|
|
if t.Kind() == reflect.Interface {
|
|
if v.IsNil() {
|
|
return false, errNilInterface
|
|
}
|
|
return willConvertToTableOrArrayTable(v.Elem())
|
|
}
|
|
|
|
if t.Kind() == reflect.Slice {
|
|
if v.Len() == 0 {
|
|
// An empty slice should be a kv = [].
|
|
return false, nil
|
|
}
|
|
for i := 0; i < v.Len(); i++ {
|
|
t, err := willConvertToTable(v.Index(i))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !t {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
return willConvertToTable(v)
|
|
}
|
|
|
|
func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
|
if v.Len() == 0 {
|
|
b = append(b, "[]"...)
|
|
return b, nil
|
|
}
|
|
|
|
allTables, err := willConvertToTableOrArrayTable(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if allTables {
|
|
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) {
|
|
if v.Len() == 0 {
|
|
return b, nil
|
|
}
|
|
|
|
ctx.shiftKey()
|
|
|
|
var err error
|
|
scratch := make([]byte, 0, 64)
|
|
scratch = append(scratch, "[["...)
|
|
for i, k := range ctx.parentKey {
|
|
if i > 0 {
|
|
scratch = append(scratch, '.')
|
|
}
|
|
scratch, err = enc.encodeKey(scratch, k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
scratch = append(scratch, "]]\n"...)
|
|
ctx.skipTableHeader = true
|
|
|
|
for i := 0; i < v.Len(); i++ {
|
|
b = append(b, scratch...)
|
|
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) {
|
|
b = append(b, '[')
|
|
|
|
var err error
|
|
first := true
|
|
for i := 0; i < v.Len(); i++ {
|
|
if !first {
|
|
b = append(b, ", "...)
|
|
}
|
|
first = false
|
|
|
|
b, err = enc.encode(b, ctx, v.Index(i))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
b = append(b, ']')
|
|
return b, nil
|
|
}
|