300 lines
6.7 KiB
Go
300 lines
6.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
)
|
|
|
|
// Parser parses configuration files
|
|
type Parser struct {
|
|
scanner *Scanner
|
|
config *Config
|
|
currentObject map[string]any
|
|
stack []map[string]any
|
|
currentToken Token
|
|
}
|
|
|
|
// NewParser creates a new parser with a reader and empty config
|
|
func NewParser(r io.Reader) *Parser {
|
|
config := NewConfig()
|
|
return &Parser{
|
|
scanner: NewScanner(r),
|
|
config: config,
|
|
currentObject: config.data,
|
|
stack: make([]map[string]any, 0, 8), // Pre-allocate stack with reasonable capacity
|
|
}
|
|
}
|
|
|
|
// Error creates an error with line information from the current token
|
|
func (p *Parser) Error(msg string) error {
|
|
return fmt.Errorf("line %d, column %d: %s",
|
|
p.currentToken.Line, p.currentToken.Column, msg)
|
|
}
|
|
|
|
// Parse parses the config file and returns a Config
|
|
func (p *Parser) Parse() (*Config, error) {
|
|
err := p.parseContent()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.config, nil
|
|
}
|
|
|
|
// pushObject enters a new object scope
|
|
func (p *Parser) pushObject(obj map[string]any) {
|
|
p.stack = append(p.stack, p.currentObject)
|
|
p.currentObject = obj
|
|
}
|
|
|
|
// popObject exits the current object scope
|
|
func (p *Parser) popObject() {
|
|
n := len(p.stack)
|
|
if n > 0 {
|
|
p.currentObject = p.stack[n-1]
|
|
p.stack = p.stack[:n-1]
|
|
}
|
|
}
|
|
|
|
// nextToken gets the next meaningful token (skipping comments)
|
|
func (p *Parser) nextToken() (Token, error) {
|
|
for {
|
|
token, err := p.scanner.NextToken()
|
|
if err != nil {
|
|
return token, err
|
|
}
|
|
|
|
// Skip comment tokens
|
|
if token.Type != TokenComment {
|
|
p.currentToken = token
|
|
return token, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseContent is the main parsing function
|
|
func (p *Parser) parseContent() error {
|
|
for {
|
|
token, err := p.nextToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check for end of file
|
|
if token.Type == TokenEOF {
|
|
break
|
|
}
|
|
|
|
// We expect top level entries to be names
|
|
if token.Type != TokenName {
|
|
return p.Error("expected name at top level")
|
|
}
|
|
|
|
// Get the property name
|
|
name := string(token.Value)
|
|
|
|
// Get the next token (should be = or {)
|
|
token, err = p.nextToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var value any
|
|
|
|
if token.Type == TokenEquals {
|
|
// It's a standard key=value assignment
|
|
value, err = p.parseValue()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if token.Type == TokenOpenBrace {
|
|
// It's a map/array without '='
|
|
value, err = p.parseObject()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return p.Error("expected '=' or '{' after name")
|
|
}
|
|
|
|
// Store the value in the config
|
|
if mapValue, ok := value.(map[string]any); ok {
|
|
// Add an entry in current object
|
|
newMap := make(map[string]any, 8) // Pre-allocate with capacity
|
|
p.currentObject[name] = newMap
|
|
|
|
// Process the map contents
|
|
p.pushObject(newMap)
|
|
|
|
// Copy values from scanned map to our object
|
|
for k, v := range mapValue {
|
|
p.currentObject[k] = v
|
|
}
|
|
|
|
p.popObject()
|
|
} else {
|
|
// Direct storage for primitives and arrays
|
|
p.currentObject[name] = value
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseValue parses a value after an equals sign
|
|
func (p *Parser) parseValue() (any, error) {
|
|
token, err := p.nextToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch token.Type {
|
|
case TokenString:
|
|
return string(token.Value), nil
|
|
|
|
case TokenNumber:
|
|
strValue := string(token.Value)
|
|
// Check if it's a float or int
|
|
for i := 0; i < len(strValue); i++ {
|
|
if strValue[i] == '.' {
|
|
// It's a float
|
|
val, err := strconv.ParseFloat(strValue, 64)
|
|
if err != nil {
|
|
return nil, p.Error(fmt.Sprintf("invalid float: %s", strValue))
|
|
}
|
|
return val, nil
|
|
}
|
|
}
|
|
// It's an integer
|
|
val, err := strconv.ParseInt(strValue, 10, 64)
|
|
if err != nil {
|
|
return nil, p.Error(fmt.Sprintf("invalid integer: %s", strValue))
|
|
}
|
|
return val, nil
|
|
|
|
case TokenBoolean:
|
|
return string(token.Value) == "true", nil
|
|
|
|
case TokenOpenBrace:
|
|
// It's a map or array
|
|
return p.parseObject()
|
|
|
|
case TokenName:
|
|
// Treat as a string value
|
|
return string(token.Value), nil
|
|
|
|
default:
|
|
return nil, p.Error(fmt.Sprintf("unexpected token: %v", token.Type))
|
|
}
|
|
}
|
|
|
|
// parseObject parses a map or array
|
|
func (p *Parser) parseObject() (any, error) {
|
|
contents := make(map[string]any)
|
|
var arrayElements []any
|
|
isArray := true
|
|
|
|
for {
|
|
token, err := p.nextToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check for end of object
|
|
if token.Type == TokenCloseBrace {
|
|
if isArray && len(contents) == 0 {
|
|
return arrayElements, nil
|
|
}
|
|
return contents, nil
|
|
}
|
|
|
|
// If we find a name, it could be a map entry or array element
|
|
if token.Type == TokenName {
|
|
// Get the name value
|
|
name := string(token.Value)
|
|
|
|
// Get the next token to determine if it's a map entry or array element
|
|
nextToken, err := p.nextToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if nextToken.Type == TokenEquals {
|
|
// It's a key-value pair
|
|
value, err := p.parseValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isArray = false
|
|
contents[name] = value
|
|
} else if nextToken.Type == TokenOpenBrace {
|
|
// It's a nested object
|
|
objValue, err := p.parseObject()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isArray = false
|
|
contents[name] = objValue
|
|
} else {
|
|
// Put the token back and treat the name as an array element
|
|
p.scanner.UnreadToken(nextToken)
|
|
|
|
// Try to convert to appropriate type
|
|
var value any = name
|
|
|
|
// Do some type inference for common values
|
|
if name == "true" {
|
|
value = true
|
|
} else if name == "false" {
|
|
value = false
|
|
} else if isDigit(name[0]) || (len(name) > 1 && name[0] == '-' && isDigit(name[1])) {
|
|
// Try to parse as number
|
|
if hasDot(name) {
|
|
if f, err := strconv.ParseFloat(name, 64); err == nil {
|
|
value = f
|
|
}
|
|
} else {
|
|
if i, err := strconv.ParseInt(name, 10, 64); err == nil {
|
|
value = i
|
|
}
|
|
}
|
|
}
|
|
|
|
arrayElements = append(arrayElements, value)
|
|
}
|
|
} else if token.Type == TokenString || token.Type == TokenNumber || token.Type == TokenBoolean {
|
|
// Direct array element
|
|
var value any
|
|
|
|
switch token.Type {
|
|
case TokenString:
|
|
value = string(token.Value)
|
|
case TokenNumber:
|
|
strVal := string(token.Value)
|
|
if hasDot(strVal) {
|
|
f, _ := strconv.ParseFloat(strVal, 64)
|
|
value = f
|
|
} else {
|
|
i, _ := strconv.ParseInt(strVal, 10, 64)
|
|
value = i
|
|
}
|
|
case TokenBoolean:
|
|
value = string(token.Value) == "true"
|
|
}
|
|
|
|
arrayElements = append(arrayElements, value)
|
|
} else if token.Type == TokenOpenBrace {
|
|
// Nested object in array
|
|
nestedObj, err := p.parseObject()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
arrayElements = append(arrayElements, nestedObj)
|
|
} else {
|
|
return nil, p.Error(fmt.Sprintf("unexpected token in object: %v", token.Type))
|
|
}
|
|
}
|
|
}
|