diff --git a/parse.go b/parse.go index 2c3a27c..eb7680e 100644 --- a/parse.go +++ b/parse.go @@ -1,21 +1,29 @@ package ini import ( + "errors" "fmt" ) -type errParse struct { - line int - col int - s string +type unexpectedTokenErr struct { + got token + want token } -func (e *errParse) Error() string { - return fmt.Sprintf("parse:%v:%v: %v", e.line, e.col, e.s) +func (e *unexpectedTokenErr) Error() string { + return fmt.Sprintf("unexpected token: %v, want %v", e.got, e.want) +} + +type invalidPropertyErr struct { + p property +} + +func (e *invalidPropertyErr) Error() string { + return "invalid property: " + e.p.key } type parser struct { - ast ast + tree parseTree l *lexer tok token prev *token @@ -23,8 +31,8 @@ type parser struct { func newParser(data []byte) *parser { p := parser{ - ast: newAST(), - l: lex(string(data)), + tree: newParseTree(), + l: lex(string(data)), } return &p } @@ -47,52 +55,56 @@ func (p *parser) parse() error { if p.tok.typ == tokenEOF { return nil } + p.nextToken() switch p.tok.typ { case tokenEOF: return nil case tokenError: - return &errParse{p.l.line, p.l.col, p.tok.val} + return errors.New(p.tok.val) case tokenSection: sec := newSection(p.tok.val) if err := p.parseSection(&sec); err != nil { return err } - p.ast.addSection(sec) - p.backup() - case tokenKey: - prop := newProperty(p.tok.val) - if err := p.parseProperty(&prop); err != nil { + p.tree.add(sec) + case tokenPropKey: + prop, err := p.tree.global.get(p.tok.val) + if err != nil { + return err + } + if err := p.parseProperty(prop); err != nil { return err } - p.ast[""][0].addProperty(prop) + p.tree.global.add(*prop) + default: + return &unexpectedTokenErr{got: p.tok} } } } func (p *parser) parseSection(out *section) error { name := p.tok.val - - if name != out.name { - panic(fmt.Sprintf("section name mismatch: expected '%v', got '%v'", name, out.name)) - } + out.name = name for { - if p.tok.typ == tokenEOF { - return nil - } p.nextToken() switch p.tok.typ { - case tokenEOF: - return nil case tokenError: - return &errParse{p.l.line, p.l.col, p.tok.val} - case tokenKey: - prop := newProperty(p.tok.val) - if err := p.parseProperty(&prop); err != nil { + return errors.New(p.tok.val) + case tokenPropKey: + prop, err := out.get(p.tok.val) + if err != nil { + return err + } + if err := p.parseProperty(prop); err != nil { return err } - out.addProperty(prop) + out.add(*prop) + case tokenSection: + // we've parsed too far; backup so we can parse the next section + p.backup() + return nil default: return nil } @@ -101,23 +113,37 @@ func (p *parser) parseSection(out *section) error { func (p *parser) parseProperty(out *property) error { key := p.tok.val + subkey := "" p.nextToken() + if p.tok.typ == tokenMapKey { + subkey = p.tok.val + p.nextToken() + } + if p.tok.typ != tokenAssignment { - return &errParse{p.l.line, p.l.col, p.tok.val} + return &unexpectedTokenErr{ + got: p.tok, + want: token{ + typ: tokenAssignment, + val: "=", + }, + } } p.nextToken() - if p.tok.typ != tokenText { - return &errParse{p.l.line, p.l.col, p.tok.val} + if p.tok.typ != tokenPropValue { + return &unexpectedTokenErr{ + got: p.tok, + want: token{ + typ: tokenPropValue, + }, + } } val := p.tok.val - if key != out.key { - panic(fmt.Sprintf("property key mismatch: expected '%v', got '%v'", key, out.key)) - } - - out.val = append(out.val, val) + out.key = key + out.add(subkey, val) return nil } diff --git a/parse_test.go b/parse_test.go index 476956b..1a88f9e 100644 --- a/parse_test.go +++ b/parse_test.go @@ -9,46 +9,27 @@ import ( func TestParse(t *testing.T) { tests := []struct { input string - want ast + want parseTree }{ { - input: "version=1.2.3\n\n[user]\nname=root\nshell=/bin/bash\n\n[user]\nname=admin\nshell=/bin/bash", - want: ast{ - "": []section{ - section{ - name: "", - props: map[string]property{ - "version": property{ - key: "version", - val: []string{"1.2.3"}, - }, - }, - }, - }, - "user": []section{ - section{ - name: "user", - props: map[string]property{ - "name": property{ - key: "name", - val: []string{"root"}, - }, - "shell": property{ - key: "shell", - val: []string{"/bin/bash"}, - }, - }, - }, - section{ - name: "user", - props: map[string]property{ - "name": property{ - key: "name", - val: []string{"admin"}, - }, - "shell": property{ - key: "shell", - val: []string{"/bin/bash"}, + input: "", + want: newParseTree(), + }, + { + input: "[user]\nshell=/bin/bash", + want: parseTree{ + global: newSection(""), + sections: map[string][]section{ + "user": []section{ + { + name: "user", + props: map[string]property{ + "shell": property{ + key: "shell", + vals: map[string][]string{ + "": []string{"/bin/bash"}, + }, + }, }, }, }, @@ -56,59 +37,63 @@ func TestParse(t *testing.T) { }, }, { - input: ` -version=1.2.3 - -[owner] -name=John Doe -organization=Acme Widgets Inc. - -[database] -server=192.0.2.62 -port=143 -file="payroll.dat"`, - want: ast{ - "": []section{ - section{ - name: "", - props: map[string]property{ - "version": property{ - key: "version", - val: []string{"1.2.3"}, + input: "Greeting[en]=Hello\nGreeting[fr]=Bonjour", + want: parseTree{ + global: section{ + name: "", + props: map[string]property{ + "Greeting": property{ + key: "Greeting", + vals: map[string][]string{ + "en": []string{"Hello"}, + "fr": []string{"Bonjour"}, }, }, }, }, - "owner": []section{ - section{ - name: "owner", - props: map[string]property{ - "name": property{ - key: "name", - val: []string{"John Doe"}, - }, - "organization": property{ - key: "organization", - val: []string{"Acme Widgets Inc."}, + sections: map[string][]section{}, + }, + }, + { + input: "[user]\nname=root\nshell[unix]=/bin/bash\nshell[win32]=PowerShell.exe\n[user]\nname=admin\nshell[unix]=/bin/bash\nshell[win32]=PowerShell.exe", + want: parseTree{ + global: newSection(""), + sections: map[string][]section{ + "user": []section{ + { + name: "user", + props: map[string]property{ + "name": property{ + key: "name", + vals: map[string][]string{ + "": []string{"root"}, + }, + }, + "shell": property{ + key: "shell", + vals: map[string][]string{ + "unix": []string{"/bin/bash"}, + "win32": []string{"PowerShell.exe"}, + }, + }, }, }, - }, - }, - "database": []section{ - section{ - name: "database", - props: map[string]property{ - "server": property{ - key: "server", - val: []string{"192.0.2.62"}, - }, - "port": property{ - key: "port", - val: []string{"143"}, - }, - "file": property{ - key: "file", - val: []string{`"payroll.dat"`}, + { + name: "user", + props: map[string]property{ + "name": property{ + key: "name", + vals: map[string][]string{ + "": []string{"admin"}, + }, + }, + "shell": property{ + key: "shell", + vals: map[string][]string{ + "unix": []string{"/bin/bash"}, + "win32": []string{"PowerShell.exe"}, + }, + }, }, }, }, @@ -123,8 +108,8 @@ file="payroll.dat"`, if err != nil { t.Fatal(err) } - if !cmp.Equal(p.ast, test.want, cmp.Options{cmp.AllowUnexported(section{}, property{})}) { - t.Fatalf("%v != %v", p.ast, test.want) + if !cmp.Equal(p.tree, test.want, cmp.Options{cmp.AllowUnexported(section{}, property{}, parseTree{})}) { + t.Fatalf("%+v != %+v", p.tree, test.want) } } } @@ -138,8 +123,18 @@ func TestParseProp(t *testing.T) { input: "shell=/bin/bash", want: property{ key: "shell", - val: []string{ - "/bin/bash", + vals: map[string][]string{ + "": []string{"/bin/bash"}, + }, + }, + }, + { + input: "Greeting[en]=Hello\nGreeting[fr]=Bonjour", + want: property{ + key: "Greeting", + vals: map[string][]string{ + "en": []string{"Hello"}, + "fr": []string{"Bonjour"}, }, }, }, @@ -148,13 +143,16 @@ func TestParseProp(t *testing.T) { for _, test := range tests { p := newParser([]byte(test.input)) p.nextToken() - got := property{ - key: p.tok.val, - val: []string{}, - } - err := p.parseProperty(&got) - if err != nil { - t.Fatal(err) + got := newProperty(p.tok.val) + for { + err := p.parseProperty(&got) + if err != nil { + t.Fatal(err) + } + p.nextToken() + if p.tok.typ == tokenEOF { + break + } } if !cmp.Equal(got, test.want, cmp.Options{cmp.AllowUnexported(property{})}) { t.Errorf("%v != %v", got, test.want) @@ -174,11 +172,15 @@ func TestParseSection(t *testing.T) { props: map[string]property{ "name": property{ key: "name", - val: []string{"root"}, + vals: map[string][]string{ + "": []string{"root"}, + }, }, "shell": property{ key: "shell", - val: []string{"/bin/bash"}, + vals: map[string][]string{ + "": []string{"/bin/bash"}, + }, }, }, }, @@ -188,10 +190,7 @@ func TestParseSection(t *testing.T) { for _, test := range tests { p := newParser([]byte(test.input)) p.nextToken() - got := section{ - name: p.tok.val, - props: map[string]property{}, - } + got := newSection(p.tok.val) err := p.parseSection(&got) if err != nil { t.Fatal(err)