Unmarshal slices of strings

This commit is contained in:
Thomas Pelletier
2021-03-13 22:07:36 -05:00
parent 1fafb71fd9
commit a0548e793c
6 changed files with 286 additions and 139 deletions
+44 -42
View File
@@ -218,8 +218,9 @@ func (p *parser) parseVal(b []byte) (ast.Node, []byte, error) {
// TODO
return node, b[5:], nil
case '[':
// TODO
//return p.parseValArray(b)
node.Kind = ast.Array
b, err := p.parseValArray(&node, b)
return node, b, err
case '{':
// TODO
//return p.parseInlineTable(b)
@@ -275,7 +276,7 @@ func (p *parser) parseInlineTable(b []byte) ([]byte, error) {
return expect('}', b)
}
func (p *parser) parseValArray(b []byte) ([]byte, error) {
func (p *parser) parseValArray(node *ast.Node, b []byte) ([]byte, error) {
//array = array-open [ array-values ] ws-comment-newline array-close
//array-open = %x5B ; [
//array-close = %x5D ; ]
@@ -284,45 +285,46 @@ func (p *parser) parseValArray(b []byte) ([]byte, error) {
//array-sep = %x2C ; , Comma
//ws-comment-newline = *( wschar / [ comment ] newline )
// TODO
//b = b[1:]
//
//first := true
//var err error
//for len(b) > 0 {
// b, err = p.parseOptionalWhitespaceCommentNewline(b)
// if err != nil {
// return nil, err
// }
//
// if len(b) == 0 {
// return nil, unexpectedCharacter{b: b}
// }
//
// if b[0] == ']' {
// break
// }
// if b[0] == ',' {
// if first {
// return nil, fmt.Errorf("array cannot start with comma")
// }
// b = b[1:]
// b, err = p.parseOptionalWhitespaceCommentNewline(b)
// if err != nil {
// return nil, err
// }
// }
//
// b, err = p.parseVal(b)
// if err != nil {
// return nil, err
// }
// b, err = p.parseOptionalWhitespaceCommentNewline(b)
// if err != nil {
// return nil, err
// }
// first = false
//}
b = b[1:]
first := true
var err error
for len(b) > 0 {
b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return nil, err
}
if len(b) == 0 {
return nil, unexpectedCharacter{b: b}
}
if b[0] == ']' {
break
}
if b[0] == ',' {
if first {
return nil, fmt.Errorf("array cannot start with comma")
}
b = b[1:]
b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return nil, err
}
}
var valueNode ast.Node
valueNode, b, err = p.parseVal(b)
if err != nil {
return nil, err
}
node.Children = append(node.Children, valueNode)
b, err = p.parseOptionalWhitespaceCommentNewline(b)
if err != nil {
return nil, err
}
first = false
}
return expect(']', b)
}
+66 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestParser_Simple(t *testing.T) {
func TestParser_AST(t *testing.T) {
examples := []struct {
desc string
input string
@@ -33,6 +33,71 @@ func TestParser_Simple(t *testing.T) {
},
},
},
{
desc: "array of strings",
input: `A = ["hello", ["world", "again"]]`,
ast: ast.Root{
ast.Node{
Kind: ast.KeyValue,
Children: []ast.Node{
{
Kind: ast.Key,
Data: []byte(`A`),
},
{
Kind: ast.Array,
Children: []ast.Node{
{
Kind: ast.String,
Data: []byte(`hello`),
},
{
Kind: ast.Array,
Children: []ast.Node{
{
Kind: ast.String,
Data: []byte(`world`),
},
{
Kind: ast.String,
Data: []byte(`again`),
},
},
},
},
},
},
},
},
},
{
desc: "array of arrays of strings",
input: `A = ["hello", "world"]`,
ast: ast.Root{
ast.Node{
Kind: ast.KeyValue,
Children: []ast.Node{
{
Kind: ast.Key,
Data: []byte(`A`),
},
{
Kind: ast.Array,
Children: []ast.Node{
{
Kind: ast.String,
Data: []byte(`hello`),
},
{
Kind: ast.String,
Data: []byte(`world`),
},
},
},
},
},
},
},
}
for _, e := range examples {
+35 -27
View File
@@ -6,15 +6,12 @@ import (
)
type target interface {
// Ensure the target's reflect value is not nil.
ensure()
// Ensure the target's value is compatible with a slice and initialized.
ensureSlice() error
// Store a string at the target.
setString(v string) error
// Appends an arbitrary value to the container.
pushValue(v reflect.Value) error
// Creates a new value of the container's element type, and returns a
// target to it.
pushNew() (target, error)
@@ -31,38 +28,38 @@ func (t valueTarget) get() reflect.Value {
return reflect.Value(t)
}
func (t valueTarget) ensure() {
func (t valueTarget) ensureSlice() error {
f := t.get()
if !f.IsNil() {
return
}
switch f.Kind() {
switch f.Type().Kind() {
case reflect.Slice:
f.Set(reflect.MakeSlice(f.Type(), 0, 0))
if f.IsNil() {
f.Set(reflect.MakeSlice(f.Type(), 0, 0))
}
case reflect.Interface:
if f.IsNil() {
f.Set(reflect.MakeSlice(reflect.TypeOf([]interface{}{}), 0, 0))
} else {
if f.Type().Elem().Kind() != reflect.Slice {
return fmt.Errorf("interface is pointing to a %s, not a slice", f.Kind())
}
}
default:
panic(fmt.Errorf("don't know how to ensure %s", f.Kind()))
return fmt.Errorf("cannot initialize a slice in %s", f.Kind())
}
return nil
}
func (t valueTarget) setString(v string) error {
f := t.get()
if f.Kind() != reflect.String {
return fmt.Errorf("cannot assign string to a %s", f.String())
}
f.SetString(v)
return nil
}
func (t valueTarget) pushValue(v reflect.Value) error {
f := t.get()
switch f.Kind() {
case reflect.Slice:
t.ensure()
f.Set(reflect.Append(f, v))
case reflect.String:
f.SetString(v)
case reflect.Interface:
f.Set(reflect.ValueOf(v))
default:
return fmt.Errorf("cannot push %s on a %s", v.Kind(), f.Kind())
return fmt.Errorf("cannot assign string to a %s", f.String())
}
return nil
@@ -73,11 +70,22 @@ func (t valueTarget) pushNew() (target, error) {
switch f.Kind() {
case reflect.Slice:
t.ensure()
f = t.get()
idx := f.Len()
f.Set(reflect.Append(f, reflect.New(f.Type().Elem()).Elem()))
return valueTarget(f.Index(idx)), nil
case reflect.Interface:
if f.IsNil() {
panic("interface should have been initialized")
}
ifaceElem := f.Elem()
if ifaceElem.Kind() != reflect.Slice {
return nil, fmt.Errorf("cannot pushNew on a %s", f.Kind())
}
idx := ifaceElem.Len()
newElem := reflect.New(ifaceElem.Type().Elem()).Elem()
newSlice := reflect.Append(ifaceElem, newElem)
f.Set(newSlice)
return valueTarget(f.Elem().Index(idx)), nil
default:
return nil, fmt.Errorf("cannot pushNew on a %s", f.Kind())
}
+27 -41
View File
@@ -13,13 +13,14 @@ func TestStructTarget_Ensure(t *testing.T) {
desc string
input reflect.Value
name string
test func(v reflect.Value)
test func(v reflect.Value, err error)
}{
{
desc: "handle a nil slice of string",
input: reflect.ValueOf(&struct{ A []string }{}).Elem(),
name: "A",
test: func(v reflect.Value) {
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
assert.False(t, v.IsNil())
},
},
@@ -27,7 +28,8 @@ func TestStructTarget_Ensure(t *testing.T) {
desc: "handle an existing slice of string",
input: reflect.ValueOf(&struct{ A []string }{A: []string{"foo"}}).Elem(),
name: "A",
test: func(v reflect.Value) {
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
require.False(t, v.IsNil())
s := v.Interface().([]string)
assert.Equal(t, []string{"foo"}, s)
@@ -39,9 +41,9 @@ func TestStructTarget_Ensure(t *testing.T) {
t.Run(e.desc, func(t *testing.T) {
target, err := scope(e.input, e.name)
require.NoError(t, err)
target.ensure()
err = target.ensureSlice()
v := target.get()
e.test(v)
e.test(v, err)
})
}
}
@@ -93,42 +95,6 @@ func TestStructTarget_SetString(t *testing.T) {
}
}
func TestPushValue_Struct(t *testing.T) {
examples := []struct {
desc string
input reflect.Value
expected []string
error bool
}{
{
desc: "push to nil slice",
input: reflect.ValueOf(&struct{ A []string }{}).Elem(),
expected: []string{"hello"},
},
{
desc: "push to string",
input: reflect.ValueOf(&struct{ A string }{}).Elem(),
error: true,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
target, err := scope(e.input, "A")
require.NoError(t, err)
v := reflect.ValueOf("hello")
err = target.pushValue(v)
if e.error {
require.Error(t, err)
} else {
require.NoError(t, err)
x := target.get().Interface().([]string)
assert.Equal(t, e.expected, x)
}
})
}
}
func TestPushNew(t *testing.T) {
t.Run("slice of strings", func(t *testing.T) {
type Doc struct {
@@ -149,6 +115,26 @@ func TestPushNew(t *testing.T) {
require.NoError(t, n.setString("world"))
require.Equal(t, []string{"hello", "world"}, d.A)
})
t.Run("slice of interfaces", func(t *testing.T) {
type Doc struct {
A []interface{}
}
d := Doc{}
x, err := scope(reflect.ValueOf(&d).Elem(), "A")
require.NoError(t, err)
n, err := x.pushNew()
require.NoError(t, err)
require.NoError(t, n.setString("hello"))
require.Equal(t, []interface{}{"hello"}, d.A)
n, err = x.pushNew()
require.NoError(t, err)
require.NoError(t, n.setString("world"))
require.Equal(t, []interface{}{"hello", "world"}, d.A)
})
}
func TestScope_Struct(t *testing.T) {
+4 -1
View File
@@ -77,7 +77,10 @@ func unmarshalString(x target, node *ast.Node) error {
func unmarshalArray(x target, node *ast.Node) error {
assertNode(ast.Array, node)
x.ensure()
err := x.ensureSlice()
if err != nil {
return err
}
for _, n := range node.Children {
v, err := x.pushNew()
+110 -27
View File
@@ -38,37 +38,120 @@ func TestFromAst_KV(t *testing.T) {
}
func TestFromAst_Slice(t *testing.T) {
root := ast.Root{
ast.Node{
Kind: ast.KeyValue,
Children: []ast.Node{
{
Kind: ast.Key,
Data: []byte(`Foo`),
},
{
Kind: ast.Array,
Children: []ast.Node{
{
Kind: ast.String,
Data: []byte(`hello`),
},
{
Kind: ast.String,
Data: []byte(`world`),
t.Run("slice of string", func(t *testing.T) {
root := ast.Root{
ast.Node{
Kind: ast.KeyValue,
Children: []ast.Node{
{
Kind: ast.Key,
Data: []byte(`Foo`),
},
{
Kind: ast.Array,
Children: []ast.Node{
{
Kind: ast.String,
Data: []byte(`hello`),
},
{
Kind: ast.String,
Data: []byte(`world`),
},
},
},
},
},
},
}
}
type Doc struct {
Foo []string
}
type Doc struct {
Foo []string
}
x := Doc{}
err := unmarshaler.FromAst(root, &x)
require.NoError(t, err)
assert.Equal(t, Doc{Foo: []string{"hello", "world"}}, x)
x := Doc{}
err := unmarshaler.FromAst(root, &x)
require.NoError(t, err)
assert.Equal(t, Doc{Foo: []string{"hello", "world"}}, x)
})
t.Run("slice of interfaces for strings", func(t *testing.T) {
root := ast.Root{
ast.Node{
Kind: ast.KeyValue,
Children: []ast.Node{
{
Kind: ast.Key,
Data: []byte(`Foo`),
},
{
Kind: ast.Array,
Children: []ast.Node{
{
Kind: ast.String,
Data: []byte(`hello`),
},
{
Kind: ast.String,
Data: []byte(`world`),
},
},
},
},
},
}
type Doc struct {
Foo []interface{}
}
x := Doc{}
err := unmarshaler.FromAst(root, &x)
require.NoError(t, err)
assert.Equal(t, Doc{Foo: []interface{}{"hello", "world"}}, x)
})
t.Run("slice of interfaces with slices", func(t *testing.T) {
root := ast.Root{
ast.Node{
Kind: ast.KeyValue,
Children: []ast.Node{
{
Kind: ast.Key,
Data: []byte(`Foo`),
},
{
Kind: ast.Array,
Children: []ast.Node{
{
Kind: ast.String,
Data: []byte(`hello`),
},
{
Kind: ast.Array,
Children: []ast.Node{
{
Kind: ast.String,
Data: []byte(`inner1`),
},
{
Kind: ast.String,
Data: []byte(`inner2`),
},
},
},
},
},
},
},
}
type Doc struct {
Foo []interface{}
}
x := Doc{}
err := unmarshaler.FromAst(root, &x)
require.NoError(t, err)
assert.Equal(t, Doc{Foo: []interface{}{"hello", []interface{}{"inner1", "inner2"}}}, x)
})
}