Fin/config.go
2025-03-03 07:47:41 -06:00

596 lines
13 KiB
Go

package config
import (
"fmt"
"io"
"strconv"
)
// ParseState represents a parsing level state
type ParseState struct {
object map[string]any
arrayElements []any
isArray bool
currentKey string
expectValue bool
}
// Config holds a single hierarchical structure like JSON and handles parsing
type Config struct {
data map[string]any
scanner *Scanner
currentObject map[string]any
stack []map[string]any
currentToken Token
}
// NewConfig creates a new empty config
func NewConfig() *Config {
cfg := &Config{
data: make(map[string]any, 16), // Pre-allocate with expected capacity
stack: make([]map[string]any, 0, 8),
}
cfg.currentObject = cfg.data
return cfg
}
// Get retrieves a value from the config using dot notation
func (c *Config) Get(key string) (any, error) {
if key == "" {
return c.data, nil
}
// Parse the dot-notation path manually
var start, i int
var current any = c.data
for i = 0; i < len(key); i++ {
if key[i] == '.' || i == len(key)-1 {
end := i
if i == len(key)-1 && key[i] != '.' {
end = i + 1
}
part := key[start:end]
// Handle current node based on its type
switch node := current.(type) {
case map[string]any:
// Simple map lookup
val, ok := node[part]
if !ok {
return nil, fmt.Errorf("key %s not found", part)
}
current = val
case []any:
// Must be numeric index
index, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("invalid array index: %s", part)
}
if index < 0 || index >= len(node) {
return nil, fmt.Errorf("array index out of bounds: %d", index)
}
current = node[index]
default:
return nil, fmt.Errorf("cannot access %s in non-container value", part)
}
// If we've processed the entire key, return the current value
if i == len(key)-1 || (i < len(key)-1 && key[i] == '.' && end == i) {
if i == len(key)-1 {
return current, nil
}
}
start = i + 1
}
}
return current, nil
}
// GetOr retrieves a value or returns a default if not found
func (c *Config) GetOr(key string, defaultValue any) any {
val, err := c.Get(key)
if err != nil {
return defaultValue
}
return val
}
// GetString gets a value as string
func (c *Config) GetString(key string) (string, error) {
val, err := c.Get(key)
if err != nil {
return "", err
}
switch v := val.(type) {
case string:
return v, nil
case bool:
return strconv.FormatBool(v), nil
case int64:
return strconv.FormatInt(v, 10), nil
case float64:
return strconv.FormatFloat(v, 'f', -1, 64), nil
default:
return "", fmt.Errorf("value for key %s cannot be converted to string", key)
}
}
// GetBool gets a value as boolean
func (c *Config) GetBool(key string) (bool, error) {
val, err := c.Get(key)
if err != nil {
return false, err
}
switch v := val.(type) {
case bool:
return v, nil
case string:
return strconv.ParseBool(v)
default:
return false, fmt.Errorf("value for key %s cannot be converted to bool", key)
}
}
// GetInt gets a value as int64
func (c *Config) GetInt(key string) (int64, error) {
val, err := c.Get(key)
if err != nil {
return 0, err
}
switch v := val.(type) {
case int64:
return v, nil
case float64:
return int64(v), nil
case string:
return strconv.ParseInt(v, 10, 64)
default:
return 0, fmt.Errorf("value for key %s cannot be converted to int", key)
}
}
// GetFloat gets a value as float64
func (c *Config) GetFloat(key string) (float64, error) {
val, err := c.Get(key)
if err != nil {
return 0, err
}
switch v := val.(type) {
case float64:
return v, nil
case int64:
return float64(v), nil
case string:
return strconv.ParseFloat(v, 64)
default:
return 0, fmt.Errorf("value for key %s cannot be converted to float", key)
}
}
// GetArray gets a value as []any
func (c *Config) GetArray(key string) ([]any, error) {
val, err := c.Get(key)
if err != nil {
return nil, err
}
if arr, ok := val.([]any); ok {
return arr, nil
}
return nil, fmt.Errorf("value for key %s is not an array", key)
}
// GetMap gets a value as map[string]any
func (c *Config) GetMap(key string) (map[string]any, error) {
val, err := c.Get(key)
if err != nil {
return nil, err
}
if m, ok := val.(map[string]any); ok {
return m, nil
}
return nil, fmt.Errorf("value for key %s is not a map", key)
}
// --- Parser Methods (integrated into Config) ---
// Error creates an error with line information from the current token
func (c *Config) Error(msg string) error {
return fmt.Errorf("line %d, column %d: %s",
c.currentToken.Line, c.currentToken.Column, msg)
}
// Parse parses the config from a reader
func (c *Config) Parse(r io.Reader) error {
c.scanner = NewScanner(r)
c.currentObject = c.data
err := c.parseContent()
// Clean up scanner resources even on success
if c.scanner != nil {
ReleaseScanner(c.scanner)
c.scanner = nil
}
return err
}
// nextToken gets the next meaningful token (skipping comments)
func (c *Config) nextToken() (Token, error) {
for {
token, err := c.scanner.NextToken()
if err != nil {
return token, err
}
// Skip comment tokens
if token.Type != TokenComment {
c.currentToken = token
return token, nil
}
}
}
// parseContent is the main parsing function
func (c *Config) parseContent() error {
for {
token, err := c.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 c.Error("expected name at top level")
}
// Get the property name - copy to create a stable key
nameBytes := token.Value
name := string(nameBytes)
// Get the next token (should be = or {)
token, err = c.nextToken()
if err != nil {
return err
}
var value any
if token.Type == TokenEquals {
// It's a standard key=value assignment
value, err = c.parseValue()
if err != nil {
return err
}
} else if token.Type == TokenOpenBrace {
// It's a map/array without '='
value, err = c.parseObject()
if err != nil {
return err
}
} else {
return c.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
c.currentObject[name] = newMap
// Process the map contents
c.stack = append(c.stack, c.currentObject)
c.currentObject = newMap
// Copy values from scanned map to our object
for k, v := range mapValue {
c.currentObject[k] = v
}
// Restore parent object
n := len(c.stack)
if n > 0 {
c.currentObject = c.stack[n-1]
c.stack = c.stack[:n-1]
}
} else {
// Direct storage for primitives and arrays
c.currentObject[name] = value
}
}
return nil
}
// parseValue parses a value after an equals sign
func (c *Config) parseValue() (any, error) {
token, err := c.nextToken()
if err != nil {
return nil, err
}
switch token.Type {
case TokenString:
// Copy the value for string stability
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, c.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, c.Error(fmt.Sprintf("invalid integer: %s", strValue))
}
return val, nil
case TokenBoolean:
return bytesEqual(token.Value, []byte("true")), nil
case TokenOpenBrace:
// It's a map or array
return c.parseObject()
case TokenName:
// Treat as a string value - copy to create a stable string
return string(token.Value), nil
default:
return nil, c.Error(fmt.Sprintf("unexpected token: %v", token.Type))
}
}
// parseObject parses a map or array
func (c *Config) parseObject() (any, error) {
// Initialize stack with first state
stack := []*ParseState{{
object: make(map[string]any, 8),
arrayElements: make([]any, 0, 8),
isArray: true,
}}
for len(stack) > 0 {
// Get current state from top of stack
current := stack[len(stack)-1]
token, err := c.nextToken()
if err != nil {
return nil, err
}
// Handle closing brace - finish current object/array
if token.Type == TokenCloseBrace {
// Determine result based on what we've collected
var result any
if current.isArray && len(current.object) == 0 {
result = current.arrayElements
} else {
result = current.object
}
// Pop the stack
stack = stack[:len(stack)-1]
// If stack is empty, we're done with the root object
if len(stack) == 0 {
return result, nil
}
// Otherwise, add result to parent
parent := stack[len(stack)-1]
if parent.expectValue {
parent.object[parent.currentKey] = result
parent.expectValue = false
} else {
parent.arrayElements = append(parent.arrayElements, result)
}
continue
}
// Handle tokens based on type
switch token.Type {
case TokenName:
name := string(token.Value)
// Look ahead to determine context
nextToken, err := c.nextToken()
if err != nil {
return nil, err
}
if nextToken.Type == TokenEquals {
// Key-value pair
current.isArray = false
current.currentKey = name
current.expectValue = true
// Parse the value
valueToken, err := c.nextToken()
if err != nil {
return nil, err
}
if valueToken.Type == TokenOpenBrace {
// Push new state for nested object/array
newState := &ParseState{
object: make(map[string]any, 8),
arrayElements: make([]any, 0, 8),
isArray: true,
}
stack = append(stack, newState)
} else {
// Handle primitive value
value := c.tokenToValue(valueToken)
current.object[name] = value
}
} else if nextToken.Type == TokenOpenBrace {
// Nested object with name
current.isArray = false
current.currentKey = name
current.expectValue = true
// Push new state for nested object
newState := &ParseState{
object: make(map[string]any, 8),
arrayElements: make([]any, 0, 8),
isArray: true,
}
stack = append(stack, newState)
} else {
// Array element
c.scanner.UnreadToken(nextToken)
// Convert name to appropriate type
value := c.convertNameValue(name)
current.arrayElements = append(current.arrayElements, value)
}
case TokenString, TokenNumber, TokenBoolean:
value := c.tokenToValue(token)
if current.expectValue {
current.object[current.currentKey] = value
current.expectValue = false
} else {
current.arrayElements = append(current.arrayElements, value)
}
case TokenOpenBrace:
// New nested object/array
newState := &ParseState{
object: make(map[string]any, 8),
arrayElements: make([]any, 0, 8),
isArray: true,
}
stack = append(stack, newState)
default:
return nil, c.Error(fmt.Sprintf("unexpected token: %v", token.Type))
}
}
return nil, fmt.Errorf("unexpected end of parsing")
}
// Load parses a config from a reader
func Load(r io.Reader) (*Config, error) {
config := NewConfig()
err := config.Parse(r)
if err != nil {
return nil, err
}
return config, nil
}
// Helpers
func isLetter(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
}
func isDigit(b byte) bool {
return b >= '0' && b <= '9'
}
// ParseNumber converts a string to a number (int64 or float64)
func ParseNumber(s string) (any, error) {
// Check if it has a decimal point
for i := 0; i < len(s); i++ {
if s[i] == '.' {
// It's a float
return strconv.ParseFloat(s, 64)
}
}
// It's an integer
return strconv.ParseInt(s, 10, 64)
}
// bytesEqual compares a byte slice with either a string or byte slice
func bytesEqual(b []byte, s []byte) bool {
if len(b) != len(s) {
return false
}
for i := 0; i < len(b); i++ {
if b[i] != s[i] {
return false
}
}
return true
}
// isDigitOrMinus checks if a string starts with a digit or minus sign
func isDigitOrMinus(s string) bool {
if len(s) == 0 {
return false
}
return isDigit(s[0]) || (s[0] == '-' && len(s) > 1 && isDigit(s[1]))
}
// parseStringAsNumber tries to parse a string as a number (float or int)
func parseStringAsNumber(s string) (any, error) {
// Check if it has a decimal point
for i := 0; i < len(s); i++ {
if s[i] == '.' {
// It's a float
return strconv.ParseFloat(s, 64)
}
}
// It's an integer
return strconv.ParseInt(s, 10, 64)
}
func (c *Config) tokenToValue(token Token) any {
switch token.Type {
case TokenString:
return string(token.Value)
case TokenNumber:
val, _ := parseStringAsNumber(string(token.Value))
return val
case TokenBoolean:
return bytesEqual(token.Value, []byte("true"))
default:
return string(token.Value)
}
}
func (c *Config) convertNameValue(name string) any {
if name == "true" {
return true
} else if name == "false" {
return false
} else if isDigitOrMinus(name) {
val, err := parseStringAsNumber(name)
if err == nil {
return val
}
}
return name
}