Encoder inline tables (#519)

This commit is contained in:
Thomas Pelletier
2021-04-21 19:11:15 -04:00
committed by GitHub
parent 32c1a8d372
commit 6fe332a869
2 changed files with 130 additions and 68 deletions
+84 -62
View File
@@ -29,47 +29,11 @@ func Marshal(v interface{}) ([]byte, error) {
// Encoder writes a TOML document to an output stream. // Encoder writes a TOML document to an output stream.
type Encoder struct { type Encoder struct {
// output
w io.Writer w io.Writer
}
type encoderCtx struct { // global settings
// Current top-level key. tablesInline bool
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
options valueOptions
}
type valueOptions struct {
multiline 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. // NewEncoder returns a new Encoder that writes to w.
@@ -79,6 +43,11 @@ func NewEncoder(w io.Writer) *Encoder {
} }
} }
// SetTablesInline forces the encoder to emit all tables inline.
func (e *Encoder) SetTablesInline(inline bool) {
e.tablesInline = inline
}
// Encode writes a TOML representation of v to the stream. // Encode writes a TOML representation of v to the stream.
// //
// If v cannot be represented to TOML it returns an error. // If v cannot be represented to TOML it returns an error.
@@ -114,6 +83,8 @@ func (enc *Encoder) Encode(v interface{}) error {
ctx encoderCtx ctx encoderCtx
) )
ctx.inline = enc.tablesInline
b, err := enc.encode(b, ctx, reflect.ValueOf(v)) b, err := enc.encode(b, ctx, reflect.ValueOf(v))
if err != nil { if err != nil {
return fmt.Errorf("Encode: %w", err) return fmt.Errorf("Encode: %w", err)
@@ -127,6 +98,53 @@ func (enc *Encoder) Encode(v interface{}) error {
return nil return nil
} }
type valueOptions struct {
multiline bool
}
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
// Should the next table be encoded as inline
inline bool
options valueOptions
}
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
}
func (ctx *encoderCtx) isRoot() bool {
return len(ctx.parentKey) == 0 && !ctx.hasKey
}
var errUnsupportedValue = errors.New("unsupported encode value kind") var errUnsupportedValue = errors.New("unsupported encode value kind")
//nolint:cyclop //nolint:cyclop
@@ -396,7 +414,7 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte
continue continue
} }
table, err := willConvertToTableOrArrayTable(v) table, err := willConvertToTableOrArrayTable(ctx, v)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -469,35 +487,39 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
continue continue
} }
willConvert, err := willConvertToTableOrArrayTable(f) willConvert, err := willConvertToTableOrArrayTable(ctx, f)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var options valueOptions options := valueOptions{
multiline: fieldBoolTag(fieldType, "multiline"),
ml, ok := fieldType.Tag.Lookup("multiline")
if ok {
options.multiline = ml == "true"
} }
if willConvert { inline := fieldBoolTag(fieldType, "inline")
t.pushTable(k, f, options)
} else { if inline || !willConvert {
t.pushKV(k, f, options) t.pushKV(k, f, options)
} else {
t.pushTable(k, f, options)
} }
} }
return enc.encodeTable(b, ctx, t) return enc.encodeTable(b, ctx, t)
} }
func fieldBoolTag(field reflect.StructField, tag string) bool {
x, ok := field.Tag.Lookup(tag)
return ok && x == "true"
}
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) { func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var err error var err error
ctx.shiftKey() ctx.shiftKey()
if ctx.insideKv { if ctx.insideKv || (ctx.inline && !ctx.isRoot()) {
return enc.encodeTableInsideKV(b, ctx, t) return enc.encodeTableInline(b, ctx, t)
} }
if !ctx.skipTableHeader { if !ctx.skipTableHeader {
@@ -533,7 +555,7 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
return b, nil return b, nil
} }
func (enc *Encoder) encodeTableInsideKV(b []byte, ctx encoderCtx, t table) ([]byte, error) { func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var err error var err error
b = append(b, '{') b = append(b, '{')
@@ -571,14 +593,14 @@ func (enc *Encoder) encodeTableInsideKV(b []byte, ctx encoderCtx, t table) ([]by
b = append(b, '\n') b = append(b, '\n')
} }
b = append(b, "}\n"...) b = append(b, "}"...)
return b, nil return b, nil
} }
var errNilInterface = errors.New("nil interface not supported") var errNilInterface = errors.New("nil interface not supported")
func willConvertToTable(v reflect.Value) (bool, error) { func willConvertToTable(ctx encoderCtx, v reflect.Value) (bool, error) {
//nolint:gocritic,godox //nolint:gocritic,godox
switch v.Interface().(type) { switch v.Interface().(type) {
case time.Time: // TODO: add TextMarshaler case time.Time: // TODO: add TextMarshaler
@@ -588,25 +610,25 @@ func willConvertToTable(v reflect.Value) (bool, error) {
t := v.Type() t := v.Type()
switch t.Kind() { switch t.Kind() {
case reflect.Map, reflect.Struct: case reflect.Map, reflect.Struct:
return true, nil return !ctx.inline, nil
case reflect.Interface: case reflect.Interface:
if v.IsNil() { if v.IsNil() {
return false, errNilInterface return false, errNilInterface
} }
return willConvertToTable(v.Elem()) return willConvertToTable(ctx, v.Elem())
case reflect.Ptr: case reflect.Ptr:
if v.IsNil() { if v.IsNil() {
return false, nil return false, nil
} }
return willConvertToTable(v.Elem()) return willConvertToTable(ctx, v.Elem())
default: default:
return false, nil return false, nil
} }
} }
func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) { func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) (bool, error) {
t := v.Type() t := v.Type()
if t.Kind() == reflect.Interface { if t.Kind() == reflect.Interface {
@@ -614,7 +636,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
return false, errNilInterface return false, errNilInterface
} }
return willConvertToTableOrArrayTable(v.Elem()) return willConvertToTableOrArrayTable(ctx, v.Elem())
} }
if t.Kind() == reflect.Slice { if t.Kind() == reflect.Slice {
@@ -624,7 +646,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
} }
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
t, err := willConvertToTable(v.Index(i)) t, err := willConvertToTable(ctx, v.Index(i))
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -637,7 +659,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
return true, nil return true, nil
} }
return willConvertToTable(v) return willConvertToTable(ctx, v)
} }
func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
@@ -647,7 +669,7 @@ func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]by
return b, nil return b, nil
} }
allTables, err := willConvertToTableOrArrayTable(v) allTables, err := willConvertToTableOrArrayTable(ctx, v)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+46 -6
View File
@@ -68,9 +68,6 @@ hello = 'world'`,
a = 'test'`, a = 'test'`,
}, },
{ {
//nolint:godox
// TODO: this test is flaky because output changes depending on
// the map iteration order.
desc: "map in map in map and string with values", desc: "map in map in map and string with values",
v: map[string]interface{}{ v: map[string]interface{}{
"this": map[string]interface{}{ "this": map[string]interface{}{
@@ -248,6 +245,25 @@ name = 'Alice'
hello hello
world"""`, world"""`,
}, },
{
desc: "inline field",
v: struct {
A map[string]string `inline:"true"`
B map[string]string
}{
A: map[string]string{
"isinline": "yes",
},
B: map[string]string{
"isinline": "no",
},
},
expected: `
A = {isinline = 'yes'}
[B]
isinline = 'no'
`,
},
} }
for _, e := range examples { for _, e := range examples {
@@ -258,10 +274,34 @@ world"""`,
b, err := toml.Marshal(e.v) b, err := toml.Marshal(e.v)
if e.err { if e.err {
require.Error(t, err) require.Error(t, err)
} else { return
require.NoError(t, err)
equalStringsIgnoreNewlines(t, e.expected, string(b))
} }
require.NoError(t, err)
equalStringsIgnoreNewlines(t, e.expected, string(b))
// make sure the output is always valid TOML
defaultMap := map[string]interface{}{}
err = toml.Unmarshal(b, &defaultMap)
require.NoError(t, err)
// checks that the TablesInline mode generates valid,
// equivalent TOML
t.Run("tables inline", func(t *testing.T) {
var buf bytes.Buffer
enc := toml.NewEncoder(&buf)
enc.SetTablesInline(true)
err := enc.Encode(e.v)
require.NoError(t, err)
inlineMap := map[string]interface{}{}
err = toml.Unmarshal(buf.Bytes(), &inlineMap)
require.NoError(t, err)
require.Equal(t, defaultMap, inlineMap)
})
}) })
} }
} }