Encoder inline tables (#519)
This commit is contained in:
+84
-62
@@ -29,47 +29,11 @@ func Marshal(v interface{}) ([]byte, error) {
|
||||
|
||||
// Encoder writes a TOML document to an output stream.
|
||||
type Encoder struct {
|
||||
// output
|
||||
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
|
||||
|
||||
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
|
||||
// global settings
|
||||
tablesInline bool
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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.inline = enc.tablesInline
|
||||
|
||||
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Encode: %w", err)
|
||||
@@ -127,6 +98,53 @@ func (enc *Encoder) Encode(v interface{}) error {
|
||||
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")
|
||||
|
||||
//nolint:cyclop
|
||||
@@ -396,7 +414,7 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte
|
||||
continue
|
||||
}
|
||||
|
||||
table, err := willConvertToTableOrArrayTable(v)
|
||||
table, err := willConvertToTableOrArrayTable(ctx, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -469,35 +487,39 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
|
||||
continue
|
||||
}
|
||||
|
||||
willConvert, err := willConvertToTableOrArrayTable(f)
|
||||
willConvert, err := willConvertToTableOrArrayTable(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var options valueOptions
|
||||
|
||||
ml, ok := fieldType.Tag.Lookup("multiline")
|
||||
if ok {
|
||||
options.multiline = ml == "true"
|
||||
options := valueOptions{
|
||||
multiline: fieldBoolTag(fieldType, "multiline"),
|
||||
}
|
||||
|
||||
if willConvert {
|
||||
t.pushTable(k, f, options)
|
||||
} else {
|
||||
inline := fieldBoolTag(fieldType, "inline")
|
||||
|
||||
if inline || !willConvert {
|
||||
t.pushKV(k, f, options)
|
||||
} else {
|
||||
t.pushTable(k, f, options)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
var err error
|
||||
|
||||
ctx.shiftKey()
|
||||
|
||||
if ctx.insideKv {
|
||||
return enc.encodeTableInsideKV(b, ctx, t)
|
||||
if ctx.insideKv || (ctx.inline && !ctx.isRoot()) {
|
||||
return enc.encodeTableInline(b, ctx, t)
|
||||
}
|
||||
|
||||
if !ctx.skipTableHeader {
|
||||
@@ -533,7 +555,7 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
|
||||
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
|
||||
|
||||
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, "}"...)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
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
|
||||
switch v.Interface().(type) {
|
||||
case time.Time: // TODO: add TextMarshaler
|
||||
@@ -588,25 +610,25 @@ func willConvertToTable(v reflect.Value) (bool, error) {
|
||||
t := v.Type()
|
||||
switch t.Kind() {
|
||||
case reflect.Map, reflect.Struct:
|
||||
return true, nil
|
||||
return !ctx.inline, nil
|
||||
case reflect.Interface:
|
||||
if v.IsNil() {
|
||||
return false, errNilInterface
|
||||
}
|
||||
|
||||
return willConvertToTable(v.Elem())
|
||||
return willConvertToTable(ctx, v.Elem())
|
||||
case reflect.Ptr:
|
||||
if v.IsNil() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return willConvertToTable(v.Elem())
|
||||
return willConvertToTable(ctx, v.Elem())
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
|
||||
func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) (bool, error) {
|
||||
t := v.Type()
|
||||
|
||||
if t.Kind() == reflect.Interface {
|
||||
@@ -614,7 +636,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
|
||||
return false, errNilInterface
|
||||
}
|
||||
|
||||
return willConvertToTableOrArrayTable(v.Elem())
|
||||
return willConvertToTableOrArrayTable(ctx, v.Elem())
|
||||
}
|
||||
|
||||
if t.Kind() == reflect.Slice {
|
||||
@@ -624,7 +646,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
|
||||
}
|
||||
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
t, err := willConvertToTable(v.Index(i))
|
||||
t, err := willConvertToTable(ctx, v.Index(i))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -637,7 +659,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return willConvertToTable(v)
|
||||
return willConvertToTable(ctx, v)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
allTables, err := willConvertToTableOrArrayTable(v)
|
||||
allTables, err := willConvertToTableOrArrayTable(ctx, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+45
-5
@@ -68,9 +68,6 @@ hello = 'world'`,
|
||||
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",
|
||||
v: map[string]interface{}{
|
||||
"this": map[string]interface{}{
|
||||
@@ -248,6 +245,25 @@ name = 'Alice'
|
||||
hello
|
||||
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 {
|
||||
@@ -258,10 +274,34 @@ world"""`,
|
||||
b, err := toml.Marshal(e.v)
|
||||
if e.err {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user