diff --git a/README.md b/README.md index 8ae7d35..1a07e09 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ primary { negative = FALSE nothing = NULL + list = [50.5, true, false, "hello", 'world']; + # Include external files include "./include*.cfg"; # Primary-sub section diff --git a/forge.go b/forge.go index 543b470..76d44e5 100644 --- a/forge.go +++ b/forge.go @@ -14,6 +14,13 @@ // secondary_bool = true; // secondary_null = null; // +// list = [ +// "any", +// 'value', +// true, +// 55.5, +// ] +// // # Reference other config value // local_ref = .secondary_null; // global_ref = primary.sub_section.sub_float; @@ -36,9 +43,10 @@ // STRING: ['"] .* ['"] // REFERENCE: (IDENTIFIER)? ('.' IDENTIFIER)+ // VALUE: BOOL | NULL | INTEGER | FLOAT | STRING | REFERENCE +// LIST: '[' (VALUE | LIST) (',' NEWLINE* (VALUE | LIST))+ ']' // // INCLUDE: 'include ' STRING END -// DIRECTIVE: (IDENTIFIER '=' VALUE | INCLUDE) END +// DIRECTIVE: (IDENTIFIER '=' (VALUE | LIST) | INCLUDE) END // SECTION: IDENTIFIER '{' (DIRECTIVE | SECTION)* '}' // COMMENT: '#' .* '\n' // @@ -57,6 +65,9 @@ // The identifiers 'true' or 'false' of any case (e.g. TRUE, True, true, FALSE, False, false) // * Null: // The identifier 'null' of any case (e.g. NULL, Null, null) +// * List: +// A list value is any number of other values separated by commas and surrounded by brackets. +// (e.g. [50.5, 'some', "string", true, false]) // * Global reference: // An identifier which may contain periods, the references are resolved from the global // section (e.g. global_value, section.sub_section.value) @@ -74,7 +85,7 @@ // All directives must end in either a semicolon or newline. The value can be any of the types defined above. // * Section: // A section is a grouping of directives under a common name. They are in the format ' { }'. -// All sections must be wrapped in brackets ('{', '}') and must all have a name. They do not end in a semicolon. +// All sections must be wrapped in braces ('{', '}') and must all have a name. They do not end in a semicolon. // Sections may be left empty, they do not have to contain any directives. // * Include: // An include statement tells the config parser to include the contents of another config file where the include diff --git a/forge_test.go b/forge_test.go index 153b7c1..60cd800 100644 --- a/forge_test.go +++ b/forge_test.go @@ -26,6 +26,14 @@ primary { not_true = FALSE nothing = NULL + list = [ + TRUE, + FALSE, + 50.5, + "hello", + 'list', + ] + # Reference secondary._under (which hasn't been defined yet) sec_ref = secondary._under; # Primary-sub stuff @@ -71,6 +79,15 @@ func assertDirectives(values map[string]interface{}, t *testing.T) { assertEqual(primary["nothing"], nil, t) assertEqual(primary["sec_ref"], int64(50), t) + // Primary list + list := primary["list"].([]interface{}) + assertEqual(len(list), 5, t) + assertEqual(list[0], true, t) + assertEqual(list[1], false, t) + assertEqual(list[2], float64(50.5), t) + assertEqual(list[3], "hello", t) + assertEqual(list[4], "list", t) + // Primary Sub sub := primary["sub"].(map[string]interface{}) assertEqual(sub["key"], "primary sub key value", t) diff --git a/list.go b/list.go new file mode 100644 index 0000000..b7347ff --- /dev/null +++ b/list.go @@ -0,0 +1,153 @@ +package forge + +import ( + "errors" + "fmt" +) + +// List struct used for holding data neede for Reference data type +type List struct { + values []Value +} + +// NewList will create and initialize a new List value +func NewList() *List { + return &List{ + values: make([]Value, 0), + } +} + +// GetType will simply return back LIST +func (list *List) GetType() ValueType { + return LIST +} + +// GetValue will resolve and return the value from the underlying list +// this is necessary to inherit from Value +func (list *List) GetValue() interface{} { + var values []interface{} + for _, val := range list.values { + values = append(values, val.GetValue()) + } + return values +} + +// GetValues will return back the list of underlygin values +func (list *List) GetValues() []Value { + return list.values +} + +// UpdateValue will set the underlying list value +func (list *List) UpdateValue(value interface{}) error { + // Valid types + switch value.(type) { + case []Value: + list.values = value.([]Value) + default: + msg := fmt.Sprintf("Unsupported type, %s must be of type []Value", value) + return errors.New(msg) + } + return nil +} + +// Get will return the Value at the index +func (list *List) Get(idx int) (Value, error) { + if idx > list.Length() { + return nil, errors.New("index out of range") + } + return list.values[idx], nil +} + +// GetBoolean will try to get the value stored at the index as a bool +// will respond with an error if the value does not exist or cannot be converted to a bool +func (list *List) GetBoolean(idx int) (bool, error) { + value, err := list.Get(idx) + if err != nil { + return false, err + } + + switch value.(type) { + case *Primative: + return value.(*Primative).AsBoolean() + } + + return false, errors.New("could not convert unknown value to boolean") +} + +// GetFloat will try to get the value stored at the index as a float64 +// will respond with an error if the value does not exist or cannot be converted to a float64 +func (list *List) GetFloat(idx int) (float64, error) { + value, err := list.Get(idx) + if err != nil { + return float64(0), err + } + + switch value.(type) { + case *Primative: + return value.(*Primative).AsFloat() + } + + return float64(0), errors.New("could not convert non-primative value to float") +} + +// GetInteger will try to get the value stored at the index as a int64 +// will respond with an error if the value does not exist or cannot be converted to a int64 +func (list *List) GetInteger(idx int) (int64, error) { + value, err := list.Get(idx) + if err != nil { + return int64(0), err + } + + switch value.(type) { + case *Primative: + return value.(*Primative).AsInteger() + } + + return int64(0), errors.New("could not convert non-primative value to integer") +} + +// GetList will try to get the value stored at the index as a List +// will respond with an error if the value does not exist or is not a List +func (list *List) GetList(idx int) (*List, error) { + value, err := list.Get(idx) + if err != nil { + return nil, err + } + + if value.GetType() == LIST { + return value.(*List), nil + } + + return nil, errors.New("could not fetch value as list") +} + +// GetString will try to get the value stored at the index as a string +// will respond with an error if the value does not exist or cannot be converted to a string +func (list *List) GetString(idx int) (string, error) { + value, err := list.Get(idx) + if err != nil { + return "", err + } + + switch value.(type) { + case *Primative: + return value.(*Primative).AsString() + } + + return "", errors.New("could not convert non-primative value to string") +} + +// Set will set the new Value at the index +func (list *List) Set(idx int, value Value) { + list.values[idx] = value +} + +// Append will append a new Value on the end of the internal list +func (list *List) Append(value Value) { + list.values = append(list.values, value) +} + +// Length will return back the total number of items in the list +func (list *List) Length() int { + return len(list.values) +} diff --git a/parser.go b/parser.go index b2d81bd..fbf800f 100644 --- a/parser.go +++ b/parser.go @@ -77,6 +77,38 @@ func (parser *Parser) readToken() token.Token { return parser.curTok } +func (parser *Parser) skipNewlines() { + for parser.curTok.ID == token.NEWLINE { + parser.readToken() + } +} + +func (parser *Parser) parseList() ([]Value, error) { + var values []Value + for { + parser.skipNewlines() + + value, err := parser.parseSettingValue() + if err != nil { + return nil, err + } + values = append(values, value) + + if parser.curTok.ID == token.COMMA { + parser.readToken() + } + + parser.skipNewlines() + + if parser.curTok.ID == token.RBRACKET { + parser.readToken() + break + } + } + + return values, nil +} + func (parser *Parser) parseReference(startingSection *Section, period bool) (Value, error) { name := "" if period == false { @@ -112,9 +144,8 @@ func (parser *Parser) parseReference(startingSection *Section, period bool) (Val return NewReference(name, startingSection), nil } -func (parser *Parser) parseSetting(name string) error { +func (parser *Parser) parseSettingValue() (Value, error) { var value Value - parser.readToken() readNext := true switch parser.curTok.ID { @@ -123,7 +154,7 @@ func (parser *Parser) parseSetting(name string) error { case token.BOOLEAN: boolVal, err := strconv.ParseBool(parser.curTok.Literal) if err != nil { - return nil + return value, nil } value = NewBoolean(boolVal) case token.NULL: @@ -131,31 +162,39 @@ func (parser *Parser) parseSetting(name string) error { case token.INTEGER: intVal, err := strconv.ParseInt(parser.curTok.Literal, 10, 64) if err != nil { - return err + return value, err } value = NewInteger(intVal) case token.FLOAT: floatVal, err := strconv.ParseFloat(parser.curTok.Literal, 64) if err != nil { - return err + return value, err } value = NewFloat(floatVal) case token.PERIOD: reference, err := parser.parseReference(parser.curSection, true) if err != nil { - return err + return value, err } value = reference readNext = false case token.IDENTIFIER: reference, err := parser.parseReference(parser.settings, false) if err != nil { - return err + return value, err } value = reference readNext = false + case token.LBRACKET: + parser.readToken() + listVal, err := parser.parseList() + if err != nil { + return value, err + } + value = NewList() + value.UpdateValue(listVal) default: - return parser.syntaxError( + return value, parser.syntaxError( fmt.Sprintf("expected STRING, INTEGER, FLOAT, BOOLEAN or IDENTIFIER, instead found %s", parser.curTok.ID), ) } @@ -163,6 +202,15 @@ func (parser *Parser) parseSetting(name string) error { if readNext { parser.readToken() } + return value, nil +} + +func (parser *Parser) parseSetting(name string) error { + parser.readToken() + value, err := parser.parseSettingValue() + if err != nil { + return err + } if isSemicolonOrNewline(parser.curTok.ID) == false { msg := fmt.Sprintf("expected ';' or '\n' instead found '%s'", parser.curTok.Literal) return parser.syntaxError(msg) @@ -247,7 +295,7 @@ func (parser *Parser) parse() error { case token.INCLUDE: parser.parseInclude() case token.IDENTIFIER: - if parser.curTok.ID == token.LBRACKET { + if parser.curTok.ID == token.LBRACE { err := parser.parseSection(tok.Literal) if err != nil { return err @@ -259,7 +307,7 @@ func (parser *Parser) parse() error { return err } } - case token.RBRACKET: + case token.RBRACE: err := parser.endSection() if err != nil { return err diff --git a/scanner.go b/scanner.go index 9d7a8c7..ee60da1 100644 --- a/scanner.go +++ b/scanner.go @@ -207,14 +207,20 @@ func (scanner *Scanner) NextToken() token.Token { scanner.readRune() scanner.curTok.Literal = string(ch) switch ch { + case ',': + scanner.curTok.ID = token.COMMA case '=': scanner.curTok.ID = token.EQUAL case '"', '\'': scanner.parseString(ch) - case '{': + case '[': scanner.curTok.ID = token.LBRACKET - case '}': + case ']': scanner.curTok.ID = token.RBRACKET + case '{': + scanner.curTok.ID = token.LBRACE + case '}': + scanner.curTok.ID = token.RBRACE case ';': scanner.curTok.ID = token.SEMICOLON case '\n': diff --git a/section.go b/section.go index c4a612c..cd79049 100644 --- a/section.go +++ b/section.go @@ -150,6 +150,21 @@ func (section *Section) GetInteger(name string) (int64, error) { return int64(0), errors.New("could not convert non-primative value to integer") } +// GetList will try to get the value stored under name as a List +// will respond with an error if the value does not exist or is not a List +func (section *Section) GetList(name string) (*List, error) { + value, err := section.Get(name) + if err != nil { + return nil, err + } + + if value.GetType() == LIST { + return value.(*List), nil + } + + return nil, errors.New("could not fetch value as list") +} + // GetSection will try to get the value stored under name as a Section // will respond with an error if the value does not exist or is not a Section func (section *Section) GetSection(name string) (*Section, error) { diff --git a/test.cfg b/test.cfg index 160b1c5..f3ef9a4 100644 --- a/test.cfg +++ b/test.cfg @@ -15,6 +15,14 @@ primary { not_true = FALSE nothing = NULL + list = [ + TRUE, + FALSE, + 50.5, + "hello", + 'list', + ] + # Reference secondary._under (which hasn't been defined yet) sec_ref = secondary._under; # Primary-sub stuff diff --git a/token/token.go b/token/token.go index 75efa92..6d6ee2f 100644 --- a/token/token.go +++ b/token/token.go @@ -22,11 +22,14 @@ const ( ILLEGAL TokenID = iota EOF + LBRACE + RBRACE LBRACKET RBRACKET EQUAL SEMICOLON NEWLINE + COMMA PERIOD IDENTIFIER @@ -42,11 +45,14 @@ const ( var tokenNames = [...]string{ ILLEGAL: "ILLEGAL", EOF: "EOF", + LBRACE: "LBRACE", + RBRACE: "RBRACE", LBRACKET: "LBRACKET", RBRACKET: "RBRACKET", EQUAL: "EQUAL", SEMICOLON: "SEMICOLON", NEWLINE: "NEWLINE", + COMMA: "COMMA", PERIOD: "PERIOD", IDENTIFIER: "IDENTIFIER", BOOLEAN: "BOOLEAN", diff --git a/value.go b/value.go index 50c0f45..f5e0441 100644 --- a/value.go +++ b/value.go @@ -21,6 +21,8 @@ const ( primativesDnd complexStart + // LIST ValueType + LIST // REFERENCE ValueType REFERENCE // SECTION ValueType @@ -35,6 +37,7 @@ var valueTypes = [...]string{ NULL: "NULL", STRING: "STRING", + LIST: "LIST", REFERENCE: "REFERENCE", SECTION: "SECTION", }