Fin/parser.go
2025-03-02 06:05:58 -06:00

283 lines
6.1 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
}
// 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.stack = append(p.stack, p.currentObject)
p.currentObject = newMap
// Copy values from scanned map to our object
for k, v := range mapValue {
p.currentObject[k] = v
}
// Restore parent object
n := len(p.stack)
if n > 0 {
p.currentObject = p.stack[n-1]
p.stack = p.stack[:n-1]
}
} 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)
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
}
// Handle based on token type
switch token.Type {
case 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)
// Convert to appropriate type if possible
var value any = name
// Try to infer type
if name == "true" {
value = true
} else if name == "false" {
value = false
} else if isDigitOrMinus(name) {
// Try to parse as number
numValue, err := parseStringAsNumber(name)
if err == nil {
value = numValue
}
}
arrayElements = append(arrayElements, value)
}
case TokenString, TokenNumber, TokenBoolean:
// Direct array element
var value any
switch token.Type {
case TokenString:
value = string(token.Value)
case TokenNumber:
strVal := string(token.Value)
value, _ = parseStringAsNumber(strVal)
case TokenBoolean:
value = string(token.Value) == "true"
}
arrayElements = append(arrayElements, value)
case TokenOpenBrace:
// Nested object in array
nestedObj, err := p.parseObject()
if err != nil {
return nil, err
}
arrayElements = append(arrayElements, nestedObj)
default:
return nil, p.Error(fmt.Sprintf("unexpected token in object: %v", token.Type))
}
}
}