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) }