Fin/config.go

574 lines
13 KiB
Go

package scf
import (
"fmt"
"io"
"strconv"
)
// Config holds a single hierarchical structure 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
}
// GetData retrieves the entirety of the internal data map
func (c *Config) GetData() map[string]any {
return c.data
}
// Get retrieves a value from the config using dot notation
func (c *Config) Get(key string) (any, error) {
if key == "" {
return c.data, nil
}
var current any = c.data
start := 0
keyLen := len(key)
for i := 0; i < keyLen; i++ {
if key[i] == '.' || i == keyLen-1 {
end := i
if i == keyLen-1 && key[i] != '.' {
end = i + 1
}
part := key[start:end]
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 == keyLen-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 int:
return strconv.Itoa(v), 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) (int, error) {
val, err := c.Get(key)
if err != nil {
return 0, err
}
switch v := val.(type) {
case int:
return v, nil
case float64:
return int(v), nil
case string:
parsed, err := strconv.Atoi(v)
return parsed, err
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 int:
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.Atoi(valueStr)
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.Atoi(valueStr)
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
}