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 // TODO
return node, b[5:], nil return node, b[5:], nil
case '[': case '[':
// TODO node.Kind = ast.Array
//return p.parseValArray(b) b, err := p.parseValArray(&node, b)
return node, b, err
case '{': case '{':
// TODO // TODO
//return p.parseInlineTable(b) //return p.parseInlineTable(b)
@@ -275,7 +276,7 @@ func (p *parser) parseInlineTable(b []byte) ([]byte, error) {
return expect('}', b) 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 = array-open [ array-values ] ws-comment-newline array-close
//array-open = %x5B ; [ //array-open = %x5B ; [
//array-close = %x5D ; ] //array-close = %x5D ; ]
@@ -284,45 +285,46 @@ func (p *parser) parseValArray(b []byte) ([]byte, error) {
//array-sep = %x2C ; , Comma //array-sep = %x2C ; , Comma
//ws-comment-newline = *( wschar / [ comment ] newline ) //ws-comment-newline = *( wschar / [ comment ] newline )
// TODO b = b[1:]
//b = b[1:]
// first := true
//first := true var err error
//var err error for len(b) > 0 {
//for len(b) > 0 { b, err = p.parseOptionalWhitespaceCommentNewline(b)
// b, err = p.parseOptionalWhitespaceCommentNewline(b) if err != nil {
// if err != nil { return nil, err
// return nil, err }
// }
// if len(b) == 0 {
// if len(b) == 0 { return nil, unexpectedCharacter{b: b}
// return nil, unexpectedCharacter{b: b} }
// }
// if b[0] == ']' {
// if b[0] == ']' { break
// break }
// } if b[0] == ',' {
// if b[0] == ',' { if first {
// if first { return nil, fmt.Errorf("array cannot start with comma")
// return nil, fmt.Errorf("array cannot start with comma") }
// } b = b[1:]
// b = b[1:] b, err = p.parseOptionalWhitespaceCommentNewline(b)
// b, err = p.parseOptionalWhitespaceCommentNewline(b) if err != nil {
// if err != nil { return nil, err
// return nil, err }
// } }
// }
// var valueNode ast.Node
// b, err = p.parseVal(b) valueNode, b, err = p.parseVal(b)
// if err != nil { if err != nil {
// return nil, err return nil, err
// } }
// b, err = p.parseOptionalWhitespaceCommentNewline(b) node.Children = append(node.Children, valueNode)
// if err != nil { b, err = p.parseOptionalWhitespaceCommentNewline(b)
// return nil, err if err != nil {
// } return nil, err
// first = false }
//} first = false
}
return expect(']', b) return expect(']', b)
} }
+66 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestParser_Simple(t *testing.T) { func TestParser_AST(t *testing.T) {
examples := []struct { examples := []struct {
desc string desc string
input 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 { for _, e := range examples {
+35 -27
View File
@@ -6,15 +6,12 @@ import (
) )
type target interface { type target interface {
// Ensure the target's reflect value is not nil. // Ensure the target's value is compatible with a slice and initialized.
ensure() ensureSlice() error
// Store a string at the target. // Store a string at the target.
setString(v string) error 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 // Creates a new value of the container's element type, and returns a
// target to it. // target to it.
pushNew() (target, error) pushNew() (target, error)
@@ -31,38 +28,38 @@ func (t valueTarget) get() reflect.Value {
return reflect.Value(t) return reflect.Value(t)
} }
func (t valueTarget) ensure() { func (t valueTarget) ensureSlice() error {
f := t.get() f := t.get()
if !f.IsNil() {
return
}
switch f.Kind() { switch f.Type().Kind() {
case reflect.Slice: 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: 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 { func (t valueTarget) setString(v string) error {
f := t.get() 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() { switch f.Kind() {
case reflect.Slice: case reflect.String:
t.ensure() f.SetString(v)
f.Set(reflect.Append(f, v)) case reflect.Interface:
f.Set(reflect.ValueOf(v))
default: 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 return nil
@@ -73,11 +70,22 @@ func (t valueTarget) pushNew() (target, error) {
switch f.Kind() { switch f.Kind() {
case reflect.Slice: case reflect.Slice:
t.ensure()
f = t.get()
idx := f.Len() idx := f.Len()
f.Set(reflect.Append(f, reflect.New(f.Type().Elem()).Elem())) f.Set(reflect.Append(f, reflect.New(f.Type().Elem()).Elem()))
return valueTarget(f.Index(idx)), nil 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: default:
return nil, fmt.Errorf("cannot pushNew on a %s", f.Kind()) 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 desc string
input reflect.Value input reflect.Value
name string name string
test func(v reflect.Value) test func(v reflect.Value, err error)
}{ }{
{ {
desc: "handle a nil slice of string", desc: "handle a nil slice of string",
input: reflect.ValueOf(&struct{ A []string }{}).Elem(), input: reflect.ValueOf(&struct{ A []string }{}).Elem(),
name: "A", name: "A",
test: func(v reflect.Value) { test: func(v reflect.Value, err error) {
assert.NoError(t, err)
assert.False(t, v.IsNil()) assert.False(t, v.IsNil())
}, },
}, },
@@ -27,7 +28,8 @@ func TestStructTarget_Ensure(t *testing.T) {
desc: "handle an existing slice of string", desc: "handle an existing slice of string",
input: reflect.ValueOf(&struct{ A []string }{A: []string{"foo"}}).Elem(), input: reflect.ValueOf(&struct{ A []string }{A: []string{"foo"}}).Elem(),
name: "A", name: "A",
test: func(v reflect.Value) { test: func(v reflect.Value, err error) {
assert.NoError(t, err)
require.False(t, v.IsNil()) require.False(t, v.IsNil())
s := v.Interface().([]string) s := v.Interface().([]string)
assert.Equal(t, []string{"foo"}, s) assert.Equal(t, []string{"foo"}, s)
@@ -39,9 +41,9 @@ func TestStructTarget_Ensure(t *testing.T) {
t.Run(e.desc, func(t *testing.T) { t.Run(e.desc, func(t *testing.T) {
target, err := scope(e.input, e.name) target, err := scope(e.input, e.name)
require.NoError(t, err) require.NoError(t, err)
target.ensure() err = target.ensureSlice()
v := target.get() 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) { func TestPushNew(t *testing.T) {
t.Run("slice of strings", func(t *testing.T) { t.Run("slice of strings", func(t *testing.T) {
type Doc struct { type Doc struct {
@@ -149,6 +115,26 @@ func TestPushNew(t *testing.T) {
require.NoError(t, n.setString("world")) require.NoError(t, n.setString("world"))
require.Equal(t, []string{"hello", "world"}, d.A) 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) { 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 { func unmarshalArray(x target, node *ast.Node) error {
assertNode(ast.Array, node) assertNode(ast.Array, node)
x.ensure() err := x.ensureSlice()
if err != nil {
return err
}
for _, n := range node.Children { for _, n := range node.Children {
v, err := x.pushNew() v, err := x.pushNew()
+110 -27
View File
@@ -38,37 +38,120 @@ func TestFromAst_KV(t *testing.T) {
} }
func TestFromAst_Slice(t *testing.T) { func TestFromAst_Slice(t *testing.T) {
root := ast.Root{ t.Run("slice of string", func(t *testing.T) {
ast.Node{ root := ast.Root{
Kind: ast.KeyValue, ast.Node{
Children: []ast.Node{ Kind: ast.KeyValue,
{ Children: []ast.Node{
Kind: ast.Key, {
Data: []byte(`Foo`), Kind: ast.Key,
}, Data: []byte(`Foo`),
{ },
Kind: ast.Array, {
Children: []ast.Node{ Kind: ast.Array,
{ Children: []ast.Node{
Kind: ast.String, {
Data: []byte(`hello`), Kind: ast.String,
}, Data: []byte(`hello`),
{ },
Kind: ast.String, {
Data: []byte(`world`), Kind: ast.String,
Data: []byte(`world`),
},
}, },
}, },
}, },
}, },
}, }
}
type Doc struct { type Doc struct {
Foo []string Foo []string
} }
x := Doc{} x := Doc{}
err := unmarshaler.FromAst(root, &x) err := unmarshaler.FromAst(root, &x)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, Doc{Foo: []string{"hello", "world"}}, x) 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)
})
} }