golangci-lint: marshaler (#516)

This commit is contained in:
Vincent Serpoul
2021-04-21 08:24:44 +08:00
committed by GitHub
parent a713a96e69
commit dca2103910
3 changed files with 162 additions and 78 deletions
+3
View File
@@ -4,6 +4,9 @@ golangci-lint-version = "1.39.0"
[linters-settings.wsl]
allow-assign-and-anything = true
[linters-settings.exhaustive]
default-signifies-exhaustive = true
[linters]
disable-all = true
enable = [
+140 -78
View File
@@ -18,10 +18,12 @@ import (
func Marshal(v interface{}) ([]byte, error) {
var buf bytes.Buffer
enc := NewEncoder(&buf)
err := enc.Encode(v)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
@@ -96,7 +98,7 @@ func NewEncoder(w io.Writer) *Encoder {
// 5. Intermediate tables are always printed.
//
// By default, strings are encoded as literal string, unless they contain either
// a newline character or a single quote. In that case they are emited as quoted
// a newline character or a single quote. In that case they are emitted as quoted
// strings.
//
// When encoding structs, fields are encoded in order of definition, with their
@@ -107,25 +109,38 @@ func NewEncoder(w io.Writer) *Encoder {
// `multiline:"true"`: when the field contains a string, it will be emitted as
// a quoted multi-line TOML string.
func (enc *Encoder) Encode(v interface{}) error {
var b []byte
var ctx encoderCtx
var (
b []byte
ctx encoderCtx
)
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
if err != nil {
return err
}
_, err = enc.w.Write(b)
return err
return fmt.Errorf("Encode: %w", err)
}
_, err = enc.w.Write(b)
if err != nil {
return fmt.Errorf("Encode: %w", err)
}
return nil
}
var errUnsupportedValue = errors.New("unsupported encode value kind")
//nolint:cyclop
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
//nolint:gocritic,godox
switch i := v.Interface().(type) {
case time.Time: // TODO: add TextMarshaler
b = i.AppendFormat(b, time.RFC3339)
return b, nil
}
// containers
switch v.Kind() {
// containers
case reflect.Map:
return enc.encodeMap(b, ctx, v)
case reflect.Struct:
@@ -136,19 +151,18 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
if v.IsNil() {
return nil, errNilInterface
}
return enc.encode(b, ctx, v.Elem())
case reflect.Ptr:
if v.IsNil() {
return enc.encode(b, ctx, reflect.Zero(v.Type().Elem()))
}
return enc.encode(b, ctx, v.Elem())
}
// values
var err error
switch v.Kind() {
case reflect.String:
b, err = enc.encodeString(b, v.String(), ctx.options)
b = enc.encodeString(b, v.String(), ctx.options)
case reflect.Float32:
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32)
case reflect.Float64:
@@ -164,10 +178,7 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
b = strconv.AppendInt(b, v.Int(), 10)
default:
err = fmt.Errorf("unsupported encode value kind: %s", v.Kind())
}
if err != nil {
return nil, err
return nil, fmt.Errorf("encode(type %s): %w", v.Kind(), errUnsupportedValue)
}
return b, nil
@@ -217,30 +228,31 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r
const literalQuote = '\''
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) ([]byte, error) {
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
if needsQuoting(v) {
b = enc.encodeQuotedString(options.multiline, b, v)
} else {
b = enc.encodeLiteralString(b, v)
return enc.encodeQuotedString(options.multiline, b, v)
}
return b, nil
return enc.encodeLiteralString(b, v)
}
func needsQuoting(v string) bool {
return strings.ContainsAny(v, "'\b\f\n\r\t")
}
// caller should have checked that the string does not contain new lines or '
// caller should have checked that the string does not contain new lines or ' .
func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
b = append(b, literalQuote)
b = append(b, v...)
b = append(b, literalQuote)
return b
}
//nolint:cyclop
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
const hextable = "0123456789ABCDEF"
stringQuote := `"`
if multiline {
stringQuote = `"""`
}
@@ -250,6 +262,16 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
b = append(b, '\n')
}
const (
hextable = "0123456789ABCDEF"
// U+0000 to U+0008, U+000A to U+001F, U+007F
nul = 0x0
bs = 0x8
lf = 0xa
us = 0x1f
del = 0x7f
)
for _, r := range []byte(v) {
switch r {
case '\\':
@@ -272,7 +294,7 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
b = append(b, `\t`...)
default:
switch {
case r >= 0x0 && r <= 0x8, r >= 0xA && r <= 0x1F, r == 0x7F:
case r >= nul && r <= bs, r >= lf && r <= us, r == del:
b = append(b, `\u00`...)
b = append(b, hextable[r>>4])
b = append(b, hextable[r&0x0f])
@@ -280,14 +302,14 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt
b = append(b, r)
}
}
// U+0000 to U+0008, U+000A to U+001F, U+007F
}
b = append(b, stringQuote...)
return b
}
// called should have checked that the string is in A-Z / a-z / 0-9 / - / _
// called should have checked that the string is in A-Z / a-z / 0-9 / - / _ .
func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
return append(b, v...)
}
@@ -300,6 +322,7 @@ func (enc *Encoder) encodeTableHeader(b []byte, key []string) ([]byte, error) {
b = append(b, '[')
var err error
b, err = enc.encodeKey(b, key[0])
if err != nil {
return nil, err
@@ -307,6 +330,7 @@ func (enc *Encoder) encodeTableHeader(b []byte, key []string) ([]byte, error) {
for _, k := range key[1:] {
b = append(b, '.')
b, err = enc.encodeKey(b, k)
if err != nil {
return nil, err
@@ -318,6 +342,9 @@ func (enc *Encoder) encodeTableHeader(b []byte, key []string) ([]byte, error) {
return b, nil
}
var errTomlNoMultiline = errors.New("TOML does not support multiline keys")
//nolint:cyclop
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
needsQuotation := false
cannotUseLiteral := false
@@ -326,32 +353,39 @@ func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
continue
}
if c == '\n' {
return nil, fmt.Errorf("TOML does not support multiline keys")
return nil, errTomlNoMultiline
}
if c == literalQuote {
cannotUseLiteral = true
}
needsQuotation = true
}
if cannotUseLiteral {
b = enc.encodeQuotedString(false, b, k)
} else if needsQuotation {
b = enc.encodeLiteralString(b, k)
} else {
b = enc.encodeUnquotedKey(b, k)
switch {
case cannotUseLiteral:
return enc.encodeQuotedString(false, b, k), nil
case needsQuotation:
return enc.encodeLiteralString(b, k), nil
default:
return enc.encodeUnquotedKey(b, k), nil
}
}
return b, nil
}
var errNotSupportedAsMapKey = errors.New("type not supported as map key")
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Type().Key().Kind() != reflect.String {
return nil, fmt.Errorf("type '%s' not supported as map key", v.Type().Key().Kind())
return nil, fmt.Errorf("encodeMap '%s': %w", v.Type().Key().Kind(), errNotSupportedAsMapKey)
}
t := table{}
var (
t table
emptyValueOptions valueOptions
)
iter := v.MapRange()
for iter.Next() {
@@ -368,9 +402,9 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte
}
if table {
t.pushTable(k, v, valueOptions{})
t.pushTable(k, v, emptyValueOptions)
} else {
t.pushKV(k, v, valueOptions{})
t.pushKV(k, v, emptyValueOptions)
}
}
@@ -405,13 +439,10 @@ func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
}
func (t *table) hasKVs() bool {
return len(t.kvs) > 0
}
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
t := table{}
var t table
//nolint:godox
// TODO: cache this?
typ := v.Type()
for i := 0; i < typ.NumField(); i++ {
@@ -443,7 +474,7 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b
return nil, err
}
options := valueOptions{}
var options valueOptions
ml, ok := fieldType.Tag.Lookup("multiline")
if ok {
@@ -466,38 +497,7 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
ctx.shiftKey()
if ctx.insideKv {
b = append(b, '{')
first := true
for _, kv := range t.kvs {
if first {
first = false
} else {
b = append(b, `, `...)
}
ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
if err != nil {
return nil, err
}
}
for _, table := range t.tables {
if first {
first = false
} else {
b = append(b, `, `...)
}
ctx.setKey(table.Key)
b, err = enc.encode(b, ctx, table.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
b = append(b, "}\n"...)
return b, nil
return enc.encodeTableInsideKV(b, ctx, t)
}
if !ctx.skipTableHeader {
@@ -510,29 +510,76 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro
for _, kv := range t.kvs {
ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
for _, table := range t.tables {
ctx.setKey(table.Key)
b, err = enc.encode(b, ctx, table.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
return b, nil
}
func (enc *Encoder) encodeTableInsideKV(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var err error
b = append(b, '{')
first := true
for _, kv := range t.kvs {
if first {
first = false
} else {
b = append(b, `, `...)
}
ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
if err != nil {
return nil, err
}
}
for _, table := range t.tables {
if first {
first = false
} else {
b = append(b, `, `...)
}
ctx.setKey(table.Key)
b, err = enc.encode(b, ctx, table.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
b = append(b, "}\n"...)
return b, nil
}
var errNilInterface = errors.New("nil interface not supported")
var errNilPointer = errors.New("nil pointer not supported")
func willConvertToTable(v reflect.Value) (bool, error) {
//nolint:gocritic,godox
switch v.Interface().(type) {
case time.Time: // TODO: add TextMarshaler
return false, nil
@@ -546,11 +593,13 @@ func willConvertToTable(v reflect.Value) (bool, error) {
if v.IsNil() {
return false, errNilInterface
}
return willConvertToTable(v.Elem())
case reflect.Ptr:
if v.IsNil() {
return false, nil
}
return willConvertToTable(v.Elem())
default:
return false, nil
@@ -564,6 +613,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
if v.IsNil() {
return false, errNilInterface
}
return willConvertToTableOrArrayTable(v.Elem())
}
@@ -572,15 +622,18 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
// An empty slice should be a kv = [].
return false, nil
}
for i := 0; i < v.Len(); i++ {
t, err := willConvertToTable(v.Index(i))
if err != nil {
return false, err
}
if !t {
return false, nil
}
}
return true, nil
}
@@ -590,6 +643,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Len() == 0 {
b = append(b, "[]"...)
return b, nil
}
@@ -617,25 +671,30 @@ func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.
var err error
scratch := make([]byte, 0, 64)
scratch = append(scratch, "[["...)
for i, k := range ctx.parentKey {
if i > 0 {
scratch = append(scratch, '.')
}
scratch, err = enc.encodeKey(scratch, k)
if err != nil {
return nil, err
}
}
scratch = append(scratch, "]]\n"...)
ctx.skipTableHeader = true
for i := 0; i < v.Len(); i++ {
b = append(b, scratch...)
b, err = enc.encode(b, ctx, v.Index(i))
if err != nil {
return nil, err
}
}
return b, nil
}
@@ -644,10 +703,12 @@ func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value
var err error
first := true
for i := 0; i < v.Len(); i++ {
if !first {
b = append(b, ", "...)
}
first = false
b, err = enc.encode(b, ctx, v.Index(i))
@@ -657,5 +718,6 @@ func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value
}
b = append(b, ']')
return b, nil
}
+21 -2
View File
@@ -11,7 +11,10 @@ import (
"github.com/stretchr/testify/require"
)
//nolint:funlen
func TestMarshal(t *testing.T) {
t.Parallel()
examples := []struct {
desc string
v interface{}
@@ -65,6 +68,7 @@ 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",
@@ -89,6 +93,16 @@ a = 'test'`,
},
expected: `array = ['one', 'two', 'three']`,
},
{
desc: "empty string array",
v: map[string][]string{},
expected: ``,
},
{
desc: "map",
v: map[string][]string{},
expected: ``,
},
{
desc: "nested string arrays",
v: map[string][][]string{
@@ -104,7 +118,7 @@ a = 'test'`,
expected: `array = ['a string', ['one', 'two'], 'last']`,
},
{
desc: "slice of maps",
desc: "array of maps",
v: map[string][]map[string]string{
"top": {
{"map1.1": "v1.1"},
@@ -157,7 +171,7 @@ K2 = 'v2'
`,
},
{
desc: "structs in slice with interfaces",
desc: "structs in array with interfaces",
v: map[string]interface{}{
"root": map[string]interface{}{
"nested": []interface{}{
@@ -237,7 +251,10 @@ world"""`,
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
t.Parallel()
b, err := toml.Marshal(e.v)
if e.err {
require.Error(t, err)
@@ -256,6 +273,8 @@ func equalStringsIgnoreNewlines(t *testing.T, expected string, actual string) {
}
func TestIssue436(t *testing.T) {
t.Parallel()
data := []byte(`{"a": [ { "b": { "c": "d" } } ]}`)
var v interface{}