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.
|
// 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
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-5
@@ -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)
|
require.NoError(t, err)
|
||||||
equalStringsIgnoreNewlines(t, e.expected, string(b))
|
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