diff --git a/internal/unmarshaler/parser.go b/internal/unmarshaler/parser.go index 9a1fb8d..4bdb2d7 100644 --- a/internal/unmarshaler/parser.go +++ b/internal/unmarshaler/parser.go @@ -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) } diff --git a/internal/unmarshaler/parser_test.go b/internal/unmarshaler/parser_test.go index f4be26b..e3e22db 100644 --- a/internal/unmarshaler/parser_test.go +++ b/internal/unmarshaler/parser_test.go @@ -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 { diff --git a/internal/unmarshaler/targets.go b/internal/unmarshaler/targets.go index 17c4ad9..daff6bb 100644 --- a/internal/unmarshaler/targets.go +++ b/internal/unmarshaler/targets.go @@ -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()) } diff --git a/internal/unmarshaler/targets_test.go b/internal/unmarshaler/targets_test.go index 598384b..7db9993 100644 --- a/internal/unmarshaler/targets_test.go +++ b/internal/unmarshaler/targets_test.go @@ -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) { diff --git a/internal/unmarshaler/unmarshaler.go b/internal/unmarshaler/unmarshaler.go index d2a35f6..00bdae4 100644 --- a/internal/unmarshaler/unmarshaler.go +++ b/internal/unmarshaler/unmarshaler.go @@ -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() diff --git a/internal/unmarshaler/unmarshaler_test.go b/internal/unmarshaler/unmarshaler_test.go index d99b62c..06cf0eb 100644 --- a/internal/unmarshaler/unmarshaler_test.go +++ b/internal/unmarshaler/unmarshaler_test.go @@ -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) + }) }