574 lines
13 KiB
Go
574 lines
13 KiB
Go
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
|
|
}
|