172 lines
3.7 KiB
Go
172 lines
3.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// Parser parses configuration files
|
|
type Parser struct {
|
|
scanner *Scanner
|
|
config *Config
|
|
currentObject map[string]any
|
|
stack []map[string]any
|
|
}
|
|
|
|
// 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 scanner
|
|
func (p *Parser) Error(msg string) error {
|
|
return fmt.Errorf("line %d: %s", p.scanner.line, 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]
|
|
}
|
|
}
|
|
|
|
// parseContent is the main parsing function
|
|
func (p *Parser) parseContent() error {
|
|
skipErr := p.scanner.SkipWhitespace()
|
|
for ; skipErr == nil; skipErr = p.scanner.SkipWhitespace() {
|
|
r, peekErr := p.scanner.PeekRune()
|
|
if peekErr == io.EOF {
|
|
break
|
|
}
|
|
if peekErr != nil {
|
|
return peekErr
|
|
}
|
|
|
|
// Handle comments
|
|
if r == '-' {
|
|
if err := p.scanner.ScanComment(); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Handle name=value pairs or named objects
|
|
if isLetter(r) {
|
|
name, err := p.scanner.ScanName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = p.scanner.SkipWhitespace(); err != nil {
|
|
return err
|
|
}
|
|
|
|
r, err = p.scanner.PeekRune()
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
|
|
// Assignment or direct map/array
|
|
if r == '=' {
|
|
// It's a standard key=value pair
|
|
p.scanner.ReadRune() // consume '='
|
|
|
|
if err = p.scanner.SkipWhitespace(); err != nil {
|
|
return err
|
|
}
|
|
|
|
value, err := p.scanner.ScanValue()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store the value directly
|
|
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
|
|
}
|
|
} else if r == '{' {
|
|
// It's a map/array without '='
|
|
value, err := p.scanner.ScanValue()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store the complex value directly
|
|
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 arrays
|
|
p.currentObject[name] = value
|
|
}
|
|
} else {
|
|
return p.Error("expected '=' or '{' after name")
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
return p.Error("unexpected character")
|
|
}
|
|
|
|
if skipErr != nil && skipErr != io.EOF {
|
|
return skipErr
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper function
|
|
func isLetter(r rune) bool {
|
|
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
|
|
}
|