From ee17979d6a7adeb2be4f1ea37e2441fce0d5df71 Mon Sep 17 00:00:00 2001 From: Derek Collison Date: Mon, 25 Mar 2013 20:29:42 -0700 Subject: [PATCH] Added first pass parser --- conf/parse.go | 177 +++++++++++++++++++++++++++++++++++++++++++++ conf/parse_test.go | 55 ++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 conf/parse.go create mode 100644 conf/parse_test.go diff --git a/conf/parse.go b/conf/parse.go new file mode 100644 index 00000000..57ec646c --- /dev/null +++ b/conf/parse.go @@ -0,0 +1,177 @@ +// Copyright 2013 Apcera Inc. All rights reserved. + +// Parser will return a map of keys to interface{}, although concrete types +// underly them. The values supported are string, bool, int64, float64, DateTime. +// Arrays and nested Maps are also supported. + +// The format supported is less restrictive than today's formats. +// Supports mixed Arrays [], nexted Maps {}, multiple comment types (# and //) +// Also supports key value assigments using '=' or ':' or whiteSpace() +// e.g. foo = 2, foo : 2, foo 2 +// maps can be assigned with no key separator as well +// semicolons as value terminators in key/value assignments are optional +// +// see parse_test.go for more examples. + +package conf + +import ( + "fmt" + "strconv" + "time" +) + +type parser struct { + mapping map[string]interface{} + lx *lexer + + // The current scoped context, can be array or map + ctx interface{} + + // stack of contexts, either map or array/slice stack + ctxs []interface{} + + // Keys stack + keys []string +} + +func Parse(data string) (map[string]interface{}, error) { + p, err := parse(data) + if err != nil { + return nil, err + } + return p.mapping, nil +} + +func parse(data string) (p *parser, err error) { + + p = &parser{ + mapping: make(map[string]interface{}), + lx: lex(data), + ctxs: make([]interface{}, 0, 4), + keys: make([]string, 0, 4), + } + p.pushContext(p.mapping) + + for { + it := p.next() + if it.typ == itemEOF { + break + } + p.processItem(it) + } + + return p, nil +} + +func (p *parser) next() item { + return p.lx.nextItem() +} + +func (p *parser) pushContext(ctx interface{}) { + p.ctxs = append(p.ctxs, ctx) + p.ctx = ctx +} + +func (p *parser) popContext() interface{} { + if len(p.ctxs) == 0 { + panic("BUG in parser, context stack empty") + } + li := len(p.ctxs) - 1 + last := p.ctxs[li] + p.ctxs = p.ctxs[0:li] + p.ctx = p.ctxs[len(p.ctxs)-1] + return last +} + +func (p *parser) pushKey(key string) { + p.keys = append(p.keys, key) +} + +func (p *parser) popKey() string { + if len(p.keys) == 0 { + panic("BUG in parser, keys stack empty") + } + li := len(p.keys) - 1 + last := p.keys[li] + p.keys = p.keys[0:li] + return last +} + +func (p *parser) processItem(it item) error { + switch it.typ { + case itemError: + return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val) + case itemKey: + p.pushKey(it.val) + case itemMapStart: + newCtx := make(map[string]interface{}) + p.pushContext(newCtx) + case itemMapEnd: + p.setValue(p.popContext()) + case itemString: + p.setValue(it.val) // FIXME(dlc) sanitize string? + case itemInteger: + num, err := strconv.ParseInt(it.val, 10, 64) + if err != nil { + if e, ok := err.(*strconv.NumError); ok && + e.Err == strconv.ErrRange { + return fmt.Errorf("Integer '%s' is out of the range.", it.val) + } else { + return fmt.Errorf("Expected integer, but got '%s'.", it.val) + } + } + p.setValue(num) + case itemFloat: + num, err := strconv.ParseFloat(it.val, 64) + if err != nil { + if e, ok := err.(*strconv.NumError); ok && + e.Err == strconv.ErrRange { + return fmt.Errorf("Float '%s' is out of the range.", it.val) + } else { + return fmt.Errorf("Expected float, but got '%s'.", it.val) + } + } + p.setValue(num) + case itemBool: + switch it.val { + case "true": + p.setValue(true) + case "false": + p.setValue(false) + default: + return fmt.Errorf("Expected boolean value, but got '%s'.", it.val) + } + case itemDatetime: + dt, err := time.Parse("2006-01-02T15:04:05Z", it.val) + if err != nil { + return fmt.Errorf("Expected Zulu formatted DateTime, but got '%s'.", it.val) + } + p.setValue(dt) + case itemArrayStart: + array := make([]interface{}, 0) + p.pushContext(array) + case itemArrayEnd: + array := p.ctx + p.popContext() + p.setValue(array) + } + + return nil +} + +func (p *parser) setValue(val interface{}) { + // Test to see if we are on an array or a map + + // Array processing + if ctx, ok := p.ctx.([]interface{}); ok { + p.ctx = append(ctx, val) + } + + // Map processing + if ctx, ok := p.ctx.(map[string]interface{}); ok { + key := p.popKey() + // FIXME(dlc), make sure error if redefining same key? + ctx[key] = val + } +} diff --git a/conf/parse_test.go b/conf/parse_test.go new file mode 100644 index 00000000..57077a3f --- /dev/null +++ b/conf/parse_test.go @@ -0,0 +1,55 @@ +package conf + +import ( + "reflect" + "testing" +) + +// Test to make sure we get what we expect. + +func test(t *testing.T, data string, ex map[string]interface{}) { + m, err := Parse(data) + if err != nil { + t.Fatalf("Received err: %v\n", err) + } + if m == nil { + t.Fatal("Received nil map") + } + + if !reflect.DeepEqual(m, ex) { + t.Fatalf("Not Equal:\nReceived: '%+v'\nExpected: '%+v'\n", m, ex) + } +} + +func TestSimpleTopLevel(t *testing.T) { + ex := map[string]interface{} { + "foo":"1", + "bar":float64(2.2), + "baz":true, + "boo":int64(22), + } + test(t, "foo='1'; bar=2.2; baz=true; boo=22", ex) +} + +var sample1 = ` +foo { + host { + ip = '127.0.0.1' + port = 4242 + } + servers = [ "a.com", "b.com", "c.com"] +} +` + +func TestSample1(t *testing.T) { + ex := map[string]interface{} { + "foo": map[string]interface{} { + "host": map[string]interface{} { + "ip": "127.0.0.1", + "port": int64(4242), + }, + "servers": []interface{} {"a.com", "b.com", "c.com"}, + }, + } + test(t, sample1, ex) +}