package toml import ( "bytes" "encoding" "fmt" "io" "reflect" "sort" "strconv" "time" "github.com/influxdata/toml/ast" ) const ( tagOmitempty = "omitempty" tagDocName = "doc" tagSkip = "-" ) // Marshal returns the TOML encoding of v. // // Struct values encode as TOML. Each exported struct field becomes a field of // the TOML structure unless // - the field's tag is "-", or // - the field is empty and its tag specifies the "omitempty" option. // // The "toml" key in the struct field's tag value is the key name, followed by // an optional comma and options. Examples: // // // Field is ignored by this package. // Field int `toml:"-"` // // // Field appears in TOML as key "myName". // Field int `toml:"myName"` // // // Field appears in TOML as key "myName" and the field is omitted from the // // result of encoding if its value is empty. // Field int `toml:"myName,omitempty"` // // // Field appears in TOML as key "field", but the field is skipped if // // empty. Note the leading comma. // Field int `toml:",omitempty"` func (cfg *Config) Marshal(v interface{}) ([]byte, error) { buf := new(bytes.Buffer) err := cfg.NewEncoder(buf).Encode(v) return buf.Bytes(), err } // A Encoder writes TOML to an output stream. type Encoder struct { w io.Writer cfg *Config } // NewEncoder returns a new Encoder that writes to w. func (cfg *Config) NewEncoder(w io.Writer) *Encoder { return &Encoder{w, cfg} } // Encode writes the TOML of v to the stream. // See the documentation for Marshal for details about the conversion of Go values to TOML. func (e *Encoder) Encode(v interface{}) error { var ( buf = &tableBuf{typ: ast.TableTypeNormal} rv = reflect.ValueOf(v) err error ) for rv.Kind() == reflect.Ptr { if rv.IsNil() { return &marshalNilError{rv.Type()} } rv = rv.Elem() } switch rv.Kind() { case reflect.Struct: err = buf.structFields(e.cfg, rv) case reflect.Map: err = buf.mapFields(e.cfg, rv) case reflect.Interface: return e.Encode(rv.Interface()) default: err = &marshalTableError{rv.Type()} } if err != nil { return err } return buf.writeTo(e.w, "") } // Marshaler can be implemented to override the encoding of TOML values. The returned text // must be a simple TOML value (i.e. not a table) and is inserted into marshaler output. // // This interface exists for backwards-compatibility reasons. You probably want to // implement encoding.TextMarshaler or MarshalerRec instead. type Marshaler interface { MarshalTOML() ([]byte, error) } // MarshalerRec can be implemented to override the TOML encoding of a type. // The returned value is marshaled in place of the receiver. type MarshalerRec interface { MarshalTOML() (interface{}, error) } type tableBuf struct { name string // already escaped / quoted body []byte children []*tableBuf typ ast.TableType arrayDepth int } func (b *tableBuf) writeTo(w io.Writer, prefix string) error { key := b.name // TODO: escape dots if prefix != "" { key = prefix + "." + key } if b.name != "" { head := "[" + key + "]" if b.typ == ast.TableTypeArray { head = "[" + head + "]" } head += "\n" if _, err := io.WriteString(w, head); err != nil { return err } } if _, err := w.Write(b.body); err != nil { return err } for i, child := range b.children { if len(b.body) > 0 || i > 0 { if _, err := w.Write([]byte("\n")); err != nil { return err } } if err := child.writeTo(w, key); err != nil { return err } } return nil } func (b *tableBuf) newChild(name string) *tableBuf { child := &tableBuf{name: quoteName(name), typ: ast.TableTypeNormal} if b.arrayDepth > 0 { child.typ = ast.TableTypeArray } return child } func (b *tableBuf) addChild(child *tableBuf) { // Empty table elision: we can avoid writing a table that doesn't have any keys on its // own. Array tables can't be elided because they define array elements (which would // be missing if elided). if len(child.body) == 0 && child.typ == ast.TableTypeNormal { for _, gchild := range child.children { gchild.name = child.name + "." + gchild.name b.addChild(gchild) } return } b.children = append(b.children, child) } func (b *tableBuf) structFields(cfg *Config, rv reflect.Value) error { rt := rv.Type() for rt.Kind() == reflect.Ptr { rv = rv.Elem() rt = rv.Type() } for i := 0; i < rv.NumField(); i++ { ft := rt.Field(i) if ft.PkgPath != "" && !ft.Anonymous { // not exported continue } name, rest := extractTag(ft.Tag.Get(fieldTagName)) if name == tagSkip { continue } fv := rv.Field(i) if rest == tagOmitempty && isEmptyValue(fv) { continue } if name == "" { name = cfg.FieldToKey(rt, ft.Name) } if err := b.field(cfg, name, fv); err != nil { return err } } return nil } type mapKeyList []struct { key string value reflect.Value } func (l mapKeyList) Len() int { return len(l) } func (l mapKeyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func (l mapKeyList) Less(i, j int) bool { return l[i].key < l[j].key } func (b *tableBuf) mapFields(cfg *Config, rv reflect.Value) error { keys := rv.MapKeys() keylist := make(mapKeyList, len(keys)) for i, key := range keys { var err error keylist[i].key, err = encodeMapKey(key) if err != nil { return err } keylist[i].value = rv.MapIndex(key) } sort.Sort(keylist) for _, kv := range keylist { if err := b.field(cfg, kv.key, kv.value); err != nil { return err } } return nil } func (b *tableBuf) field(cfg *Config, name string, rv reflect.Value) error { off := len(b.body) b.body = append(b.body, quoteName(name)...) b.body = append(b.body, " = "...) isTable, err := b.value(cfg, rv, name) if isTable { b.body = b.body[:off] // rub out "key =" } else { b.body = append(b.body, '\n') } return err } func (b *tableBuf) value(cfg *Config, rv reflect.Value, name string) (bool, error) { isMarshaler, isTable, err := b.marshaler(cfg, rv, name) if isMarshaler { return isTable, err } switch rv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: b.body = strconv.AppendInt(b.body, rv.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: b.body = strconv.AppendUint(b.body, rv.Uint(), 10) case reflect.Float32, reflect.Float64: b.body = strconv.AppendFloat(b.body, rv.Float(), 'e', -1, 64) case reflect.Bool: b.body = strconv.AppendBool(b.body, rv.Bool()) case reflect.String: b.body = strconv.AppendQuote(b.body, rv.String()) case reflect.Ptr, reflect.Interface: if rv.IsNil() { return false, &marshalNilError{rv.Type()} } return b.value(cfg, rv.Elem(), name) case reflect.Slice, reflect.Array: rvlen := rv.Len() if rvlen == 0 { b.body = append(b.body, '[', ']') return false, nil } b.arrayDepth++ wroteElem := false b.body = append(b.body, '[') for i := 0; i < rvlen; i++ { isTable, err := b.value(cfg, rv.Index(i), name) if err != nil { return isTable, err } wroteElem = wroteElem || !isTable if wroteElem { if i < rvlen-1 { b.body = append(b.body, ',', ' ') } else { b.body = append(b.body, ']') } } } if !wroteElem { b.body = b.body[:len(b.body)-1] // rub out '[' } b.arrayDepth-- return !wroteElem, nil case reflect.Struct: child := b.newChild(name) err := child.structFields(cfg, rv) b.addChild(child) return true, err case reflect.Map: child := b.newChild(name) err := child.mapFields(cfg, rv) b.addChild(child) return true, err default: return false, fmt.Errorf("toml: marshal: unsupported type %v", rv.Kind()) } return false, nil } func (b *tableBuf) marshaler(cfg *Config, rv reflect.Value, name string) (handled, isTable bool, err error) { switch t := rv.Interface().(type) { case encoding.TextMarshaler: enc, err := t.MarshalText() if err != nil { return true, false, err } b.body = encodeTextMarshaler(b.body, string(enc)) return true, false, nil case MarshalerRec: newval, err := t.MarshalTOML() if err != nil { return true, false, err } isTable, err = b.value(cfg, reflect.ValueOf(newval), name) return true, isTable, err case Marshaler: enc, err := t.MarshalTOML() if err != nil { return true, false, err } b.body = append(b.body, enc...) return true, false, nil } return false, false, nil } func encodeTextMarshaler(buf []byte, v string) []byte { // Emit the value without quotes if possible. if v == "true" || v == "false" { return append(buf, v...) } else if _, err := time.Parse(time.RFC3339Nano, v); err == nil { return append(buf, v...) } else if _, err := strconv.ParseInt(v, 10, 64); err == nil { return append(buf, v...) } else if _, err := strconv.ParseUint(v, 10, 64); err == nil { return append(buf, v...) } else if _, err := strconv.ParseFloat(v, 64); err == nil { return append(buf, v...) } return strconv.AppendQuote(buf, v) } func encodeMapKey(rv reflect.Value) (string, error) { if rv.Kind() == reflect.String { return rv.String(), nil } if tm, ok := rv.Interface().(encoding.TextMarshaler); ok { b, err := tm.MarshalText() return string(b), err } switch rv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(rv.Int(), 10), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(rv.Uint(), 10), nil } return "", fmt.Errorf("toml: invalid map key type %v", rv.Type()) } func isEmptyValue(v reflect.Value) bool { switch v.Kind() { case reflect.Array: // encoding/json treats all arrays with non-zero length as non-empty. We check the // array content here because zero-length arrays are almost never used. len := v.Len() for i := 0; i < len; i++ { if !isEmptyValue(v.Index(i)) { return false } } return true case 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() } return false } func quoteName(s string) string { if len(s) == 0 { return strconv.Quote(s) } for _, r := range s { if r >= '0' && r <= '9' || r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r == '-' || r == '_' { continue } return strconv.Quote(s) } return s }