package config import ( "fmt" "io" "strconv" ) // Config holds a single hierarchical structure like JSON and handles parsing type Config struct { data map[string]any dataRef *map[string]any // Reference to pooled map scanner *Scanner currentObject map[string]any stack []map[string]any currentToken Token } // NewConfig creates a new empty config func NewConfig() *Config { dataRef := GetMap() data := *dataRef cfg := &Config{ data: data, dataRef: dataRef, stack: make([]map[string]any, 0, 8), } cfg.currentObject = cfg.data return cfg } // Release frees any resources and returns them to pools func (c *Config) Release() { if c.scanner != nil { ReleaseScanner(c.scanner) c.scanner = nil } if c.dataRef != nil { PutMap(c.dataRef) c.data = nil c.dataRef = nil } c.currentObject = nil c.stack = nil } // 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: val, ok := node[part] if !ok { return nil, fmt.Errorf("key %s not found", part) } current = val case []any: 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 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) } // 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(fmt.Sprintf("expected name at top level, got token type %v", token.Type)) } // Get the property name - copy to create a stable key nameBytes := GetByteSlice() *nameBytes = append((*nameBytes)[:0], token.Value...) name := string(*nameBytes) PutByteSlice(nameBytes) // Get the next token nextToken, err := c.nextToken() if err != nil { if err == io.EOF { // EOF after name - store as empty string c.currentObject[name] = "" break } return err } var value any if nextToken.Type == TokenOpenBrace { // It's a nested object/array value, err = c.parseObject() if err != nil { return err } } else { // It's a simple value value = c.tokenToValue(nextToken) // Check for potential nested object - look ahead lookAhead, nextErr := c.nextToken() if nextErr == nil && lookAhead.Type == TokenOpenBrace { // It's a complex object that follows a value nestedValue, err := c.parseObject() if err != nil { return err } // Store the previous simple value in a map to add to the object if mapValue, ok := nestedValue.(map[string]any); ok { // Create a new map value with both the simple value and the map mapRef := GetMap() newMap := *mapRef for k, v := range mapValue { newMap[k] = v } newMap["value"] = value // Store simple value under "value" key value = newMap } } else if nextErr == nil && lookAhead.Type != TokenEOF { // Put the token back if it's not EOF c.scanner.UnreadToken(lookAhead) } } // Store the value in the config c.currentObject[name] = value } return nil } // parseObject parses a map or array func (c *Config) parseObject() (any, error) { // Default to treating as an array until we see a name isArray := true arrayRef := GetArray() arrayElements := *arrayRef mapRef := GetMap() objectElements := *mapRef defer func() { if !isArray { PutArray(arrayRef) // We didn't use the array } else { PutMap(mapRef) // We didn't use the map } }() for { token, err := c.nextToken() if err != nil { if err == io.EOF { return nil, fmt.Errorf("unexpected EOF in object/array") } return nil, err } // Handle closing brace - finish current object/array if token.Type == TokenCloseBrace { if isArray && len(arrayElements) > 0 { result := arrayElements // Don't release the array, transfer ownership *arrayRef = nil // Detach from pool reference return result, nil } result := objectElements // Don't release the map, transfer ownership *mapRef = nil // Detach from pool reference return result, nil } // Handle tokens based on type switch token.Type { case TokenName: // Copy token value to create a stable key keyBytes := GetByteSlice() *keyBytes = append((*keyBytes)[:0], token.Value...) key := string(*keyBytes) PutByteSlice(keyBytes) // Look ahead to see what follows nextToken, err := c.nextToken() if err != nil { if err == io.EOF { // EOF after key - store as empty value objectElements[key] = "" isArray = false return objectElements, nil } return nil, err } if nextToken.Type == TokenOpenBrace { // Nested object isArray = false // If we see a key, it's a map nestedValue, err := c.parseObject() if err != nil { return nil, err } objectElements[key] = nestedValue } else { // Key-value pair isArray = false // If we see a key, it's a map value := c.tokenToValue(nextToken) objectElements[key] = value // Check if there's an object following lookAhead, nextErr := c.nextToken() if nextErr == nil && lookAhead.Type == TokenOpenBrace { // Nested object after value nestedValue, err := c.parseObject() if err != nil { return nil, err } // Check if we need to convert the value to a map if mapValue, ok := nestedValue.(map[string]any); ok { // Create a combined map combinedMapRef := GetMap() combinedMap := *combinedMapRef for k, v := range mapValue { combinedMap[k] = v } combinedMap["value"] = value objectElements[key] = combinedMap } } else if nextErr == nil && lookAhead.Type != TokenEOF && lookAhead.Type != TokenCloseBrace { c.scanner.UnreadToken(lookAhead) } else if nextErr == nil && lookAhead.Type == TokenCloseBrace { // We found the closing brace - unread it so it's handled by the main loop c.scanner.UnreadToken(lookAhead) } } case TokenString, TokenNumber, TokenBoolean: // Array element value := c.tokenToValue(token) arrayElements = append(arrayElements, value) case TokenOpenBrace: // Nested object/array nestedValue, err := c.parseObject() if err != nil { return nil, err } if isArray { arrayElements = append(arrayElements, nestedValue) } else { // If we're in an object context, this is an error return nil, c.Error("unexpected nested object without a key") } default: return nil, c.Error(fmt.Sprintf("unexpected token type: %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 { config.Release() return nil, err } return config, nil } // tokenToValue converts a token to a Go value, preserving byte slices until final conversion func (c *Config) tokenToValue(token Token) any { switch token.Type { case TokenString: // Convert to string using pooled buffer valueBytes := GetByteSlice() *valueBytes = append((*valueBytes)[:0], token.Value...) result := string(*valueBytes) PutByteSlice(valueBytes) return result case TokenNumber: // Parse number valueStr := string(token.Value) if containsChar(token.Value, '.') { // Float val, _ := strconv.ParseFloat(valueStr, 64) return val } // Integer val, _ := strconv.ParseInt(valueStr, 10, 64) return val case TokenBoolean: return bytesEqual(token.Value, []byte("true")) case TokenName: // Check if name is a special value valueBytes := GetByteSlice() *valueBytes = append((*valueBytes)[:0], token.Value...) if bytesEqual(*valueBytes, []byte("true")) { PutByteSlice(valueBytes) return true } else if bytesEqual(*valueBytes, []byte("false")) { PutByteSlice(valueBytes) return false } else if isDigitOrMinus((*valueBytes)[0]) { // Try to convert to number valueStr := string(*valueBytes) PutByteSlice(valueBytes) if containsChar(token.Value, '.') { val, err := strconv.ParseFloat(valueStr, 64) if err == nil { return val } } else { val, err := strconv.ParseInt(valueStr, 10, 64) if err == nil { return val } } return valueStr } // Default to string result := string(*valueBytes) PutByteSlice(valueBytes) return result default: return nil } } // Helper functions func isLetter(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') } func isDigit(b byte) bool { return b >= '0' && b <= '9' } func isDigitOrMinus(b byte) bool { return isDigit(b) || b == '-' } func bytesEqual(b1, b2 []byte) bool { if len(b1) != len(b2) { return false } for i := 0; i < len(b1); i++ { if b1[i] != b2[i] { return false } } return true } func containsChar(b []byte, c byte) bool { for _, v := range b { if v == c { return true } } return false }