diff --git a/scanner.go b/scanner.go index 4786b26..8c92c00 100644 --- a/scanner.go +++ b/scanner.go @@ -21,25 +21,6 @@ var scanFollowsMultilineLiteralStringDelimiter = scanFollows([]byte{'\'', '\'', var scanFollowsTrue = scanFollows([]byte{'t', 'r', 'u', 'e'}) var scanFollowsFalse = scanFollows([]byte{'f', 'a', 'l', 's', 'e'}) -func scanNewline(b []byte) ([]byte, []byte, error) { - if len(b) == 0 { - return nil, nil, fmt.Errorf("not enough bytes for new line") - } - if b[0] == '\n' { - return b[:1], b[1:], nil - } - if b[0] == '\r' { - if len(b) < 2 { - return nil, nil, fmt.Errorf("not enough bytes for windows newline") - } - if b[1] == '\n' { - return b[:2], b[2:], nil - } - return nil, nil, unexpectedCharacter{r: '\n', b: b[2:]} - } - return nil, nil, unexpectedCharacter{b: b} -} - func scanUnquotedKey(b []byte) ([]byte, []byte, error) { //unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _ for i := 0; i < len(b); i++ { diff --git a/unmarshal.go b/unmarshal.go index 04a4237..310ef71 100644 --- a/unmarshal.go +++ b/unmarshal.go @@ -34,9 +34,63 @@ type unmarshaler struct { // When set all callbacks are no-ops. err error - // State that indicates the parser is processing a [table] name. If false - // keys are interpreted as part of a key-value. - parsingTable bool + // State that indicates the parser is processing a [[table-array]] name. + // If false keys are interpreted as part of a key-value or [table]. + parsingTableArray bool + + // Table Arrays need a buffer of keys because we need to know which one is + // the last one, as it may result in creating a new element in the array. + arrayTableKey [][]byte +} + +func (u *unmarshaler) ArrayTableBegin() { + if u.err != nil { + return + } + + u.parsingTableArray = true +} + +func (u *unmarshaler) ArrayTableEnd() { + if u.err != nil { + return + } + + u.parsingTableArray = false + + u.stack = u.stack[:1] + + parent := u.top() + for _, k := range u.arrayTableKey { + switch parent.Type().Kind() { + case reflect.Slice: + l := parent.Len() + parent = parent.Index(l - 1) + case reflect.Struct: + default: + u.err = fmt.Errorf("value of type '%s' cannot have children", parent) + return + } + + f := parent.FieldByName(string(k)) + if !f.IsValid() { + // TODO: implement alternative names + u.err = fmt.Errorf("field '%s' not found", string(k)) + return + } + parent = f + } + + if parent.Type().Kind() != reflect.Slice { + u.err = fmt.Errorf("array table key is not a slice") + return + } + + n := reflect.New(parent.Type().Elem()) + parent.Set(reflect.Append(parent, n.Elem())) + last := parent.Index(parent.Len() - 1) + u.push(last) + u.arrayTableKey = u.arrayTableKey[:0] } func (u *unmarshaler) KeyValBegin() { @@ -47,10 +101,17 @@ func (u *unmarshaler) KeyValEnd() { u.pop() } -func getOrCreateChild(parent reflect.Value, key string) (reflect.Value, error) { - if parent.Type().Kind() != reflect.Struct { +func (u *unmarshaler) getOrCreateChild(key string) (reflect.Value, error) { + parent := u.top() + switch parent.Type().Kind() { + case reflect.Slice: + l := parent.Len() + parent = parent.Index(l - 1) + case reflect.Struct: + default: return reflect.Value{}, fmt.Errorf("value of type '%s' cannot have children", parent) } + f := parent.FieldByName(key) if !f.IsValid() { // TODO: implement alternative names @@ -88,13 +149,16 @@ func (u *unmarshaler) SimpleKey(v []byte) { return } - target, err := getOrCreateChild(u.top(), string(v)) - if err != nil { - u.err = err - return + if u.parsingTableArray { + u.arrayTableKey = append(u.arrayTableKey, v) + } else { + target, err := u.getOrCreateChild(string(v)) + if err != nil { + u.err = err + return + } + u.replace(target) } - - u.replace(target) } func (u *unmarshaler) StandardTableBegin() { @@ -119,7 +183,8 @@ type builder interface { StandardTableBegin() StandardTableEnd() - + ArrayTableBegin() + ArrayTableEnd() KeyValBegin() KeyValEnd() @@ -212,6 +277,9 @@ func (p parser) parseArrayTable(b []byte) ([]byte, error) { //array-table-open = %x5B.5B ws ; [[ Double left square bracket //array-table-close = ws %x5D.5D ; ]] Double right square bracket + p.builder.ArrayTableBegin() + defer p.builder.ArrayTableEnd() + b = b[2:] b = p.parseWhitespace(b) b, err := p.parseKey(b) diff --git a/unmarshal_test.go b/unmarshal_test.go index 3b1439f..1319027 100644 --- a/unmarshal_test.go +++ b/unmarshal_test.go @@ -65,3 +65,126 @@ D = "D" assert.Equal(t, "D", x.Bar.D) assert.Equal(t, "E", x.E) } + +func TestUnmarshalDoesNotEraseBaseStruct(t *testing.T) { + x := struct { + A string + B string + }{ + A: "preset", + } + err := Unmarshal([]byte(`B = "data"`), &x) + + require.NoError(t, err) + assert.Equal(t, "preset", x.A) + assert.Equal(t, "data", x.B) +} + +func TestArrayTableSimple(t *testing.T) { + doc := ` +[[Products]] +Name = "Hammer" + +[[Products]] +Name = "Nail" +` + + type Product struct { + Name string + } + + type Data struct { + Products []Product + } + + x := Data{} + err := Unmarshal([]byte(doc), &x) + + require.NoError(t, err) + + expected := Data{ + Products: []Product{ + { + Name: "Hammer", + }, + { + Name: "Nail", + }, + }, + } + + assert.Equal(t, expected, x) +} + +//func TestUnmarshalArrayTablesMultiple(t *testing.T) { +// doc := ` +//[[Products]] +//Name = "Hammer" +//Sku = "738594937" +// +//[[Products]] # empty table within the array +// +//[[Products]] +//Name = "Nail" +//Sku = "284758393" +// +//Color = "gray" +//` +// +// type Product struct { +// Name string +// Sku string +// Color string +// } +// +// type Data struct { +// Products []Product +// } +// +// x := Data{} +// err := Unmarshal([]byte(doc), &x) +// +// require.NoError(t, err) +// +// expected := Data{ +// Products: []Product{ +// { +// Name: "Hammer", +// Sku: "738594937", +// }, +// {}, +// { +// Name: "Nail", +// Sku: "284758393", +// Color: "gray", +// }, +// }, +// } +// +// assert.Equal(t, expected, x) +//} + +//func TestUnmarshalArrayTablesNested(t *testing.T) { +// doc := ` +//[[Fruits]] +//Name = "apple" +// +//[Fruits.Physical] # subtable +//Color = "red" +//Shape = "round" +// +//[[Fruits.Varieties]] # nested array of tables +//Name = "red delicious" +// +//[[fruits.varieties]] +//Name = "granny smith" +// +// +//[[fruits]] +//Name = "banana" +// +//[[fruits.varieties]] +//Name = "plantain" +//` +// +//}