562 lines
12 KiB
Go
562 lines
12 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"sync"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// valuePool to reuse maps and slices for common value types
|
|
var valuePool = sync.Pool{
|
|
New: func() interface{} {
|
|
return make(map[string]any, 8)
|
|
},
|
|
}
|
|
|
|
// 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) {
|
|
// Get a map from the pool
|
|
contents := valuePool.Get().(map[string]any)
|
|
// Clear the map to reuse it
|
|
for k := range contents {
|
|
delete(contents, k)
|
|
}
|
|
|
|
// Ensure map is returned to pool on function exit
|
|
defer func() {
|
|
// Only return to pool if we're using array (contents becomes unused)
|
|
// If we're returning contents directly, don't return to pool
|
|
if contents != nil {
|
|
valuePool.Put(contents)
|
|
}
|
|
}()
|
|
|
|
// Use pre-allocated capacity for array elements to avoid reallocations
|
|
arrayElements := make([]any, 0, 8)
|
|
isArray := true
|
|
|
|
for {
|
|
token, err := c.nextToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check for end of object
|
|
if token.Type == TokenCloseBrace {
|
|
if isArray && len(contents) == 0 {
|
|
// Using array, set contents to nil to signal in defer that it should be returned to pool
|
|
contentsToReturn := contents
|
|
contents = nil
|
|
valuePool.Put(contentsToReturn)
|
|
return arrayElements, nil
|
|
}
|
|
|
|
// We're returning contents directly, set to nil to signal in defer not to return to pool
|
|
result := contents
|
|
contents = nil
|
|
return result, nil
|
|
}
|
|
|
|
// Handle based on token type
|
|
switch token.Type {
|
|
case TokenName:
|
|
// Get the name value - must copy for stability
|
|
name := string(token.Value)
|
|
|
|
// Get the next token to determine if it's a map entry or array element
|
|
nextToken, err := c.nextToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if nextToken.Type == TokenEquals {
|
|
// It's a key-value pair
|
|
value, err := c.parseValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isArray = false
|
|
contents[name] = value
|
|
} else if nextToken.Type == TokenOpenBrace {
|
|
// It's a nested object
|
|
objValue, err := c.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
|
|
c.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 = bytesEqual(token.Value, []byte("true"))
|
|
}
|
|
|
|
arrayElements = append(arrayElements, value)
|
|
|
|
case TokenOpenBrace:
|
|
// Nested object in array
|
|
nestedObj, err := c.parseObject()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
arrayElements = append(arrayElements, nestedObj)
|
|
|
|
default:
|
|
return nil, c.Error(fmt.Sprintf("unexpected token in object: %v", token.Type))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|