Skip to content

Commit

Permalink
Add parser for decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
subpop committed Sep 17, 2019
1 parent 57970bf commit ee24c02
Show file tree
Hide file tree
Showing 2 changed files with 341 additions and 0 deletions.
161 changes: 161 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package ini

import (
"fmt"
)

type errParse struct {
line int
col int
s string
}

func (e *errParse) Error() string {
return fmt.Sprintf("parse:%v:%v: %v", e.line, e.col, e.s)
}

type section struct {
name string
props map[string]property
}

type property struct {
key string
val []string
}

type parser struct {
ast map[string]section
l *lexer
tok token
}

func newParser(data []byte) *parser {
p := parser{
ast: map[string]section{
"": section{
name: "",
props: map[string]property{},
},
},
l: lex(string(data)),
}
return &p
}

func (p *parser) nextToken() {
p.tok = p.l.nextToken()
}

func (p *parser) parse() error {
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 tokenSection:
var sec section
sec, ok := p.ast[p.tok.val]
if !ok {
sec = section{
name: p.tok.val,
props: map[string]property{},
}
}
if err := p.parseSection(&sec); err != nil {
return err
}
p.ast[sec.name] = sec
case tokenKey:
var prop property
prop, ok := p.ast[""].props[p.tok.val]
if !ok {
prop = property{
key: p.tok.val,
val: []string{},
}
}
if err := p.parseProperty(&prop); err != nil {
return err
}
p.ast[""].props[prop.key] = prop
}
}
}

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))
}

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:
var prop property
prop, ok := out.props[p.tok.val]
if !ok {
prop = property{
key: p.tok.val,
val: []string{},
}
}
if err := p.parseProperty(&prop); err != nil {
return err
}
out.props[prop.key] = prop
case tokenSection:
var sec section
sec, ok := p.ast[p.tok.val]
if !ok {
sec = section{
name: p.tok.val,
props: map[string]property{},
}
}
if err := p.parseSection(&sec); err != nil {
return err
}
p.ast[sec.name] = sec
default:
return nil
}
}
}

func (p *parser) parseProperty(out *property) error {
key := p.tok.val

p.nextToken()
if p.tok.typ != tokenAssignment {
return &errParse{p.l.line, p.l.col, p.tok.val}
}

p.nextToken()
if p.tok.typ != tokenText {
return &errParse{p.l.line, p.l.col, p.tok.val}
}
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)

return nil
}
180 changes: 180 additions & 0 deletions parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package ini

import (
"testing"

"github.com/google/go-cmp/cmp"
)

func TestParse(t *testing.T) {
tests := []struct {
input string
want map[string]section
}{
{
input: "version=1.2.3\n\n[user]\nname=root\nshell=/bin/bash",
want: map[string]section{
"": section{
name: "",
props: map[string]property{
"version": property{
key: "version",
val: []string{"1.2.3"},
},
},
},
"user": section{
name: "user",
props: map[string]property{
"name": property{
key: "name",
val: []string{"root"},
},
"shell": property{
key: "shell",
val: []string{"/bin/bash"},
},
},
},
},
},
{
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: map[string]section{
"": section{
name: "",
props: map[string]property{
"version": property{
key: "version",
val: []string{"1.2.3"},
},
},
},
"owner": section{
name: "owner",
props: map[string]property{
"name": property{
key: "name",
val: []string{"John Doe"},
},
"organization": property{
key: "organization",
val: []string{"Acme Widgets Inc."},
},
},
},
"database": 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"`},
},
},
},
},
},
}

for _, test := range tests {
p := newParser([]byte(test.input))
err := p.parse()
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)
}
}
}

func TestParseProp(t *testing.T) {
tests := []struct {
input string
want property
}{
{
input: "shell=/bin/bash",
want: property{
key: "shell",
val: []string{
"/bin/bash",
},
},
},
}

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)
}
if !cmp.Equal(got, test.want, cmp.Options{cmp.AllowUnexported(property{})}) {
t.Errorf("%v != %v", got, test.want)
}
}
}

func TestParseSection(t *testing.T) {
tests := []struct {
input string
want section
}{
{
input: "[user]\nname=root\nshell=/bin/bash",
want: section{
name: "user",
props: map[string]property{
"name": property{
key: "name",
val: []string{"root"},
},
"shell": property{
key: "shell",
val: []string{"/bin/bash"},
},
},
},
},
}

for _, test := range tests {
p := newParser([]byte(test.input))
p.nextToken()
got := section{
name: p.tok.val,
props: map[string]property{},
}
err := p.parseSection(&got)
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(got, test.want, cmp.Options{cmp.AllowUnexported(property{}, section{})}) {
t.Errorf("%v != %v", got, test.want)
}
}
}

0 comments on commit ee24c02

Please sign in to comment.