Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
568 lines
15 KiB
568 lines
15 KiB
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 |
|
}
|
|
|