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 }