| @ -0,0 +1,169 @@ | |||
| forge | |||
| ===== | |||
| Forge is a configuration syntax and parser. | |||
| ## Installation | |||
| `git get github.com/brettlangdon/forge` | |||
| ## File format | |||
| The format was influenced a lot by nginx configuration file format. | |||
| ```config | |||
| global_key = "string value"; | |||
| sub_settings { | |||
| sub_int = 500; | |||
| sub_float = 80.80; | |||
| sub_sub_settings { | |||
| sub_sub_sub_settings { | |||
| key = "value"; | |||
| } | |||
| } | |||
| } | |||
| second { | |||
| key = "value"; | |||
| global_reference = sub_settings.sub_float; | |||
| local_reference = .key; | |||
| } | |||
| ``` | |||
| For normal settings the format is the key followed by an equal sign followed by the value and lastly ending with a semicolon. | |||
| `<key> = <value>;` | |||
| Sections (basically a map) is formatted as the section name with the section's settings wrapped in brackets. | |||
| `<section> { <key> = <value>; }` | |||
| ## Data types | |||
| ### String | |||
| A string value is wrapped by double quotes (single quotes will not work). | |||
| `"string value"`, `"single ' quotes ' allowed"`. | |||
| As of right now there is no way to escape double quotes within a string's value; | |||
| ### Number | |||
| There are two supported numbers, Integer and Float, both of which are simply numbers with the later having one period. | |||
| `500`, `50.56`. | |||
| ### Section | |||
| Sections are essentially maps, that is a setting whose purpose is to hold other settings. | |||
| Sections can be used to namespace settings. | |||
| `section { setting = "value"; }`. | |||
| ### References | |||
| References are used to refer to previously defined settings. There are two kinds of references, a global reference and a local reference; | |||
| The general format for a reference is a mix of identifiers and periods, for example `production.db.name`. | |||
| A global reference is a reference which starts looking for its value from the top most section (global section). | |||
| A local reference is a reference whose value starts with a period, this reference will start looking for it's value from the current section it is within (local section). | |||
| ```config | |||
| production { | |||
| db { | |||
| name = "forge"; | |||
| } | |||
| } | |||
| development { | |||
| db { | |||
| name = production.db.name; | |||
| } | |||
| db_name = .db.name; | |||
| } | |||
| ``` | |||
| ## API | |||
| `github.com/brettlangdon/forge` | |||
| * `forge.ParseString(data string) (map[string]interface{}, error)` | |||
| * `forge.ParseBytes(data []byte) (map[string]interface{}, error)` | |||
| * `forge.ParseFile(filename string) (map[string]interface{}, error)` | |||
| * `forge.ParseReader(reader io.Reader) (map[string]interface{}, error)` | |||
| ## Example | |||
| You can see example usage in the `example` folder. | |||
| ```go | |||
| package main | |||
| import ( | |||
| "fmt" | |||
| "json" | |||
| "github.com/brettlangdon/forge" | |||
| ) | |||
| func main() { | |||
| // Parse the file `example.cfg` as a map[string]interface{} | |||
| settings, err := forge.ParseFile("example.cfg") | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| // Convert the settings to JSON for printing | |||
| jsonBytes, err := json.Marshal(settings) | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| // Print the parsed settings | |||
| fmt.Println(string(jsonBytes)) | |||
| } | |||
| ``` | |||
| ## Future Plans | |||
| The following features are currently on my bucket list for the future: | |||
| ### More data types | |||
| I would like to at least add `Boolean` possibly `List` | |||
| ### Operations/Expressions | |||
| Would be nice to have Addition/Subtraction/Multiplication/Division: | |||
| ```config | |||
| whole = 100 | |||
| half = whole / 2; | |||
| double = whole * 2; | |||
| one_more = whole + 1; | |||
| one_less = whole - 1; | |||
| ``` | |||
| Also Concatenation for strings: | |||
| ```config | |||
| domain = "github.com"; | |||
| username = "brettlangdon"; | |||
| name = "forge"; | |||
| repo_url = domain + "/" + username + "/" + name; | |||
| ``` | |||
| ### API | |||
| I'll probably revisit the API, I just threw it together quick, want to make sure it right. | |||
| ### Comments | |||
| This is pretty lacking and should be added soon. | |||
| ### Documentation | |||
| Documentation is a good thing. | |||
| @ -0,0 +1,59 @@ | |||
| package config | |||
| type ConfigType int | |||
| const ( | |||
| SECTION ConfigType = iota | |||
| INTEGER | |||
| FLOAT | |||
| STRING | |||
| ) | |||
| var configTypes = [...]string{ | |||
| SECTION: "SECTION", | |||
| INTEGER: "INTEGER", | |||
| FLOAT: "FLOAT", | |||
| STRING: "STRING", | |||
| } | |||
| func (this ConfigType) String() string { | |||
| s := "" | |||
| if 0 <= this && this < ConfigType(len(configTypes)) { | |||
| s = configTypes[this] | |||
| } | |||
| if s == "" { | |||
| s = "UNKNOWN" | |||
| } | |||
| return s | |||
| } | |||
| type ConfigValue interface { | |||
| GetType() ConfigType | |||
| GetValue() interface{} | |||
| } | |||
| type IntegerValue struct { | |||
| Name string | |||
| Value int64 | |||
| } | |||
| func (this IntegerValue) GetType() ConfigType { return INTEGER } | |||
| func (this IntegerValue) GetValue() interface{} { return this.Value } | |||
| type FloatValue struct { | |||
| Name string | |||
| Value float64 | |||
| } | |||
| func (this FloatValue) GetType() ConfigType { return INTEGER } | |||
| func (this FloatValue) GetValue() interface{} { return this.Value } | |||
| type StringValue struct { | |||
| Name string | |||
| Value string | |||
| } | |||
| func (this StringValue) GetType() ConfigType { return STRING } | |||
| func (this StringValue) GetValue() interface{} { return this.Value } | |||
| @ -0,0 +1,69 @@ | |||
| package config | |||
| import "encoding/json" | |||
| type SectionValue struct { | |||
| Name string | |||
| Value map[string]ConfigValue | |||
| } | |||
| func (this SectionValue) GetType() ConfigType { return SECTION } | |||
| func (this SectionValue) GetValue() interface{} { return this.Value } | |||
| func (this SectionValue) Set(name string, value ConfigValue) { | |||
| this.Value[name] = value | |||
| } | |||
| func (this SectionValue) Get(name string) ConfigValue { | |||
| return this.Value[name] | |||
| } | |||
| func (this SectionValue) GetSection(name string) SectionValue { | |||
| value := this.Value[name] | |||
| return value.(SectionValue) | |||
| } | |||
| func (this SectionValue) GetString(name string) StringValue { | |||
| value := this.Value[name] | |||
| return value.(StringValue) | |||
| } | |||
| func (this SectionValue) GetInteger(name string) IntegerValue { | |||
| value := this.Value[name] | |||
| return value.(IntegerValue) | |||
| } | |||
| func (this SectionValue) GetFloat(name string) FloatValue { | |||
| value := this.Value[name] | |||
| return value.(FloatValue) | |||
| } | |||
| func (this SectionValue) Contains(name string) bool { | |||
| _, ok := this.Value[name] | |||
| return ok | |||
| } | |||
| func (this SectionValue) ToJSON() ([]byte, error) { | |||
| data, err := this.ToMap() | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return json.Marshal(data) | |||
| } | |||
| func (this SectionValue) ToMap() (map[string]interface{}, error) { | |||
| settings := make(map[string]interface{}) | |||
| for name, value := range this.Value { | |||
| if value.GetType() == SECTION { | |||
| data, err := value.(SectionValue).ToMap() | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| settings[name] = data | |||
| } else { | |||
| settings[name] = value.GetValue() | |||
| } | |||
| } | |||
| return settings, nil | |||
| } | |||
| @ -0,0 +1,16 @@ | |||
| global = "global value"; | |||
| master { | |||
| string = "master string value"; | |||
| integer = 500; | |||
| float = 80.80; | |||
| sub { | |||
| key = "master sub key value"; | |||
| } | |||
| } | |||
| slave { | |||
| another = "slave another value"; | |||
| global_reference = global; | |||
| master_sub_key = master.sub.key; | |||
| _under = 50; | |||
| } | |||
| @ -0,0 +1,21 @@ | |||
| package main | |||
| import ( | |||
| "encoding/json" | |||
| "fmt" | |||
| "github.com/brettlangdon/forge" | |||
| ) | |||
| func main() { | |||
| settings, err := forge.ParseFile("example.cfg") | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| data, err := json.Marshal(settings) | |||
| if err != nil { | |||
| panic(err) | |||
| } | |||
| fmt.Println(string(data)) | |||
| } | |||
| @ -0,0 +1,45 @@ | |||
| package forge | |||
| import ( | |||
| "bytes" | |||
| "io" | |||
| "strings" | |||
| "github.com/brettlangdon/forge/parser" | |||
| ) | |||
| func ParseString(data string) (map[string]interface{}, error) { | |||
| settings, err := parser.ParseReader(strings.NewReader(data)) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return settings.ToMap() | |||
| } | |||
| func ParseBytes(data []byte) (map[string]interface{}, error) { | |||
| settings, err := parser.ParseReader(bytes.NewReader(data)) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return settings.ToMap() | |||
| } | |||
| func ParseFile(filename string) (map[string]interface{}, error) { | |||
| settings, err := parser.ParseFile(filename) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return settings.ToMap() | |||
| } | |||
| func ParseReader(reader io.Reader) (map[string]interface{}, error) { | |||
| settings, err := parser.ParseReader(reader) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return settings.ToMap() | |||
| } | |||
| @ -0,0 +1,254 @@ | |||
| package parser | |||
| import ( | |||
| "errors" | |||
| "fmt" | |||
| "io" | |||
| "os" | |||
| "strconv" | |||
| "strings" | |||
| "github.com/brettlangdon/forge/config" | |||
| "github.com/brettlangdon/forge/token" | |||
| ) | |||
| type Parser struct { | |||
| settings config.SectionValue | |||
| tokenizer *token.Tokenizer | |||
| cur_tok token.Token | |||
| cur_section config.SectionValue | |||
| previous []config.SectionValue | |||
| } | |||
| func (this *Parser) SyntaxError(msg string) error { | |||
| msg = fmt.Sprintf( | |||
| "Syntax error line <%d> column <%d>: %s", | |||
| this.cur_tok.Line, | |||
| this.cur_tok.Column, | |||
| msg, | |||
| ) | |||
| return errors.New(msg) | |||
| } | |||
| func (this *Parser) ReferenceTypeError(names []string, expected config.ConfigType, actual config.ConfigType) error { | |||
| reference := strings.Join(names, ".") | |||
| msg := fmt.Sprintf( | |||
| "Reference type error, '%s', expected type %s instead got %s", | |||
| reference, | |||
| expected, | |||
| actual, | |||
| ) | |||
| return errors.New(msg) | |||
| } | |||
| func (this *Parser) ReferenceMissingError(names []string, searching string) error { | |||
| reference := strings.Join(names, ".") | |||
| msg := fmt.Sprintf( | |||
| "Reference missing error, '%s' does not have key '%s'", | |||
| reference, | |||
| searching, | |||
| ) | |||
| return errors.New(msg) | |||
| } | |||
| func (this *Parser) readToken() token.Token { | |||
| this.cur_tok = this.tokenizer.NextToken() | |||
| return this.cur_tok | |||
| } | |||
| func (this *Parser) parseReference(starting_section config.SectionValue, period bool) (config.ConfigValue, error) { | |||
| names := []string{} | |||
| if period == false { | |||
| names = append(names, this.cur_tok.Literal) | |||
| } | |||
| for { | |||
| this.readToken() | |||
| if this.cur_tok.ID == token.PERIOD && period == false { | |||
| period = true | |||
| } else if period && this.cur_tok.ID == token.IDENTIFIER { | |||
| names = append(names, this.cur_tok.Literal) | |||
| period = false | |||
| } else if this.cur_tok.ID == token.SEMICOLON { | |||
| break | |||
| } else { | |||
| msg := fmt.Sprintf("expected ';' instead found '%s'", this.cur_tok.Literal) | |||
| return nil, this.SyntaxError(msg) | |||
| } | |||
| } | |||
| if len(names) == 0 { | |||
| return nil, this.SyntaxError( | |||
| fmt.Sprintf("expected IDENTIFIER instead found %s", this.cur_tok.Literal), | |||
| ) | |||
| } | |||
| if period { | |||
| return nil, this.SyntaxError(fmt.Sprintf("expected IDENTIFIER after PERIOD")) | |||
| } | |||
| var reference config.ConfigValue | |||
| reference = starting_section | |||
| visited := []string{} | |||
| for { | |||
| if len(names) == 0 { | |||
| break | |||
| } | |||
| if reference.GetType() != config.SECTION { | |||
| return nil, this.ReferenceTypeError(visited, config.SECTION, reference.GetType()) | |||
| } | |||
| name := names[0] | |||
| names = names[1:] | |||
| section := reference.(config.SectionValue) | |||
| if section.Contains(name) == false { | |||
| return nil, this.ReferenceMissingError(visited, name) | |||
| } | |||
| reference = section.Get(name) | |||
| visited = append(visited, name) | |||
| } | |||
| return reference, nil | |||
| } | |||
| func (this *Parser) parseSetting(name string) error { | |||
| var value config.ConfigValue | |||
| this.readToken() | |||
| read_next := true | |||
| switch this.cur_tok.ID { | |||
| case token.STRING: | |||
| value = config.StringValue{ | |||
| Name: name, | |||
| Value: this.cur_tok.Literal, | |||
| } | |||
| case token.INTEGER: | |||
| int_val, err := strconv.ParseInt(this.cur_tok.Literal, 10, 64) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| value = config.IntegerValue{ | |||
| Name: name, | |||
| Value: int_val, | |||
| } | |||
| case token.FLOAT: | |||
| float_val, err := strconv.ParseFloat(this.cur_tok.Literal, 64) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| value = config.FloatValue{ | |||
| Name: name, | |||
| Value: float_val, | |||
| } | |||
| case token.PERIOD: | |||
| reference, err := this.parseReference(this.cur_section, true) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| value = reference | |||
| read_next = false | |||
| case token.IDENTIFIER: | |||
| reference, err := this.parseReference(this.settings, false) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| value = reference | |||
| read_next = false | |||
| default: | |||
| return this.SyntaxError( | |||
| fmt.Sprintf("expected STRING, INTEGER or FLOAT, instead found %s", this.cur_tok.ID), | |||
| ) | |||
| } | |||
| if read_next { | |||
| this.readToken() | |||
| } | |||
| if this.cur_tok.ID != token.SEMICOLON { | |||
| msg := fmt.Sprintf("expected ';' instead found '%s'", this.cur_tok.Literal) | |||
| return this.SyntaxError(msg) | |||
| } | |||
| this.cur_section.Set(name, value) | |||
| return nil | |||
| } | |||
| func (this *Parser) parseSection(name string) error { | |||
| section := config.SectionValue{ | |||
| Name: name, | |||
| Value: make(map[string]config.ConfigValue), | |||
| } | |||
| this.cur_section.Set(name, section) | |||
| this.previous = append(this.previous, this.cur_section) | |||
| this.cur_section = section | |||
| return nil | |||
| } | |||
| func (this *Parser) endSection() error { | |||
| if len(this.previous) == 0 { | |||
| return this.SyntaxError("unexpected section end '}'") | |||
| } | |||
| p_len := len(this.previous) | |||
| previous := this.previous[p_len-1] | |||
| this.previous = this.previous[0 : p_len-1] | |||
| this.cur_section = previous | |||
| return nil | |||
| } | |||
| func (this *Parser) Parse() error { | |||
| this.readToken() | |||
| for { | |||
| if this.cur_tok.ID == token.EOF { | |||
| break | |||
| } | |||
| tok := this.cur_tok | |||
| this.readToken() | |||
| switch tok.ID { | |||
| case token.IDENTIFIER: | |||
| if this.cur_tok.ID == token.LBRACKET { | |||
| err := this.parseSection(tok.Literal) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } else if this.cur_tok.ID == token.EQUAL { | |||
| err := this.parseSetting(tok.Literal) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } | |||
| case token.RBRACKET: | |||
| err := this.endSection() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } | |||
| } | |||
| return nil | |||
| } | |||
| func ParseFile(filename string) (settings *config.SectionValue, err error) { | |||
| reader, err := os.Open(filename) | |||
| if err != nil { | |||
| return settings, err | |||
| } | |||
| return ParseReader(reader) | |||
| } | |||
| func ParseReader(reader io.Reader) (*config.SectionValue, error) { | |||
| settings := config.SectionValue{ | |||
| Value: make(map[string]config.ConfigValue), | |||
| } | |||
| parser := &Parser{ | |||
| tokenizer: token.NewTokenizer(reader), | |||
| settings: settings, | |||
| cur_section: settings, | |||
| previous: make([]config.SectionValue, 0), | |||
| } | |||
| err := parser.Parse() | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| if len(parser.previous) > 0 { | |||
| return nil, parser.SyntaxError("expected end of section, instead found EOF") | |||
| } | |||
| return &settings, nil | |||
| } | |||
| @ -0,0 +1,17 @@ | |||
| package token | |||
| import "fmt" | |||
| type Token struct { | |||
| ID TokenID | |||
| Literal string | |||
| Line int | |||
| Column int | |||
| } | |||
| func (this Token) String() string { | |||
| return fmt.Sprintf( | |||
| "ID<%s> Literal<%s> Line<%s> Column<%s>", | |||
| this.ID, this.Literal, this.Line, this.Column, | |||
| ) | |||
| } | |||
| @ -0,0 +1,46 @@ | |||
| package token | |||
| type TokenID int | |||
| const ( | |||
| ILLEGAL TokenID = iota | |||
| EOF | |||
| LBRACKET | |||
| RBRACKET | |||
| EQUAL | |||
| SEMICOLON | |||
| PERIOD | |||
| IDENTIFIER | |||
| INTEGER | |||
| FLOAT | |||
| STRING | |||
| ) | |||
| var tokenNames = [...]string{ | |||
| ILLEGAL: "ILLEGAL", | |||
| EOF: "EOF", | |||
| LBRACKET: "LBRACKET", | |||
| RBRACKET: "RBRACKET", | |||
| EQUAL: "EQUAL", | |||
| SEMICOLON: "SEMICOLON", | |||
| PERIOD: "PERIOD", | |||
| IDENTIFIER: "IDENTIFIER", | |||
| INTEGER: "INTEGER", | |||
| FLOAT: "FLOAT", | |||
| STRING: "STRING", | |||
| } | |||
| func (this TokenID) String() string { | |||
| s := "" | |||
| if 0 <= this && this < TokenID(len(tokenNames)) { | |||
| s = tokenNames[this] | |||
| } | |||
| if s == "" { | |||
| s = "UNKNOWN" | |||
| } | |||
| return s | |||
| } | |||
| @ -0,0 +1,154 @@ | |||
| package token | |||
| import ( | |||
| "bufio" | |||
| "io" | |||
| ) | |||
| var eof = rune(0) | |||
| func isLetter(ch rune) bool { | |||
| return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') | |||
| } | |||
| func isDigit(ch rune) bool { | |||
| return ('0' <= ch && ch <= '9') | |||
| } | |||
| func isWhitespace(ch rune) bool { | |||
| return (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') | |||
| } | |||
| type Tokenizer struct { | |||
| cur_line int | |||
| cur_col int | |||
| cur_tok Token | |||
| cur_ch rune | |||
| newline bool | |||
| reader *bufio.Reader | |||
| } | |||
| func NewTokenizer(reader io.Reader) *Tokenizer { | |||
| tokenizer := &Tokenizer{ | |||
| reader: bufio.NewReader(reader), | |||
| cur_line: 0, | |||
| cur_col: 0, | |||
| newline: false, | |||
| } | |||
| tokenizer.readRune() | |||
| return tokenizer | |||
| } | |||
| func (this *Tokenizer) readRune() { | |||
| if this.newline { | |||
| this.cur_line += 1 | |||
| this.cur_col = 0 | |||
| this.newline = false | |||
| } else { | |||
| this.cur_col += 1 | |||
| } | |||
| next_ch, _, err := this.reader.ReadRune() | |||
| if err != nil { | |||
| this.cur_ch = eof | |||
| return | |||
| } | |||
| this.cur_ch = next_ch | |||
| if this.cur_ch == '\n' { | |||
| this.newline = true | |||
| } | |||
| } | |||
| func (this *Tokenizer) parseIdentifier() { | |||
| this.cur_tok.ID = IDENTIFIER | |||
| this.cur_tok.Literal = string(this.cur_ch) | |||
| for { | |||
| this.readRune() | |||
| if !isLetter(this.cur_ch) && this.cur_ch != '_' { | |||
| break | |||
| } | |||
| this.cur_tok.Literal += string(this.cur_ch) | |||
| } | |||
| } | |||
| func (this *Tokenizer) parseNumber() { | |||
| this.cur_tok.ID = INTEGER | |||
| this.cur_tok.Literal = string(this.cur_ch) | |||
| digit := false | |||
| for { | |||
| this.readRune() | |||
| if this.cur_ch == '.' && digit == false { | |||
| this.cur_tok.ID = FLOAT | |||
| digit = true | |||
| } else if !isDigit(this.cur_ch) { | |||
| break | |||
| } | |||
| this.cur_tok.Literal += string(this.cur_ch) | |||
| } | |||
| } | |||
| func (this *Tokenizer) parseString() { | |||
| this.cur_tok.ID = STRING | |||
| this.cur_tok.Literal = string(this.cur_ch) | |||
| for { | |||
| this.readRune() | |||
| if this.cur_ch == '"' { | |||
| break | |||
| } | |||
| this.cur_tok.Literal += string(this.cur_ch) | |||
| } | |||
| this.readRune() | |||
| } | |||
| func (this *Tokenizer) skipWhitespace() { | |||
| for { | |||
| this.readRune() | |||
| if !isWhitespace(this.cur_ch) { | |||
| break | |||
| } | |||
| } | |||
| } | |||
| func (this *Tokenizer) NextToken() Token { | |||
| if isWhitespace(this.cur_ch) { | |||
| this.skipWhitespace() | |||
| } | |||
| this.cur_tok = Token{ | |||
| ID: ILLEGAL, | |||
| Literal: string(this.cur_ch), | |||
| Line: this.cur_line, | |||
| Column: this.cur_col, | |||
| } | |||
| switch ch := this.cur_ch; { | |||
| case isLetter(ch) || ch == '_': | |||
| this.parseIdentifier() | |||
| case isDigit(ch): | |||
| this.parseNumber() | |||
| case ch == eof: | |||
| this.cur_tok.ID = EOF | |||
| this.cur_tok.Literal = "EOF" | |||
| default: | |||
| this.readRune() | |||
| this.cur_tok.Literal = string(ch) | |||
| switch ch { | |||
| case '=': | |||
| this.cur_tok.ID = EQUAL | |||
| case '"': | |||
| this.parseString() | |||
| case '{': | |||
| this.cur_tok.ID = LBRACKET | |||
| case '}': | |||
| this.cur_tok.ID = RBRACKET | |||
| case ';': | |||
| this.cur_tok.ID = SEMICOLON | |||
| case '.': | |||
| this.cur_tok.ID = PERIOD | |||
| } | |||
| } | |||
| return this.cur_tok | |||
| } | |||