This commit is contained in:
Sky Johnson 2025-03-03 07:47:41 -06:00
parent 8258a967a2
commit bb3478eb47

210
config.go
View File

@ -4,9 +4,17 @@ import (
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"sync"
) )
// 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 // Config holds a single hierarchical structure like JSON and handles parsing
type Config struct { type Config struct {
data map[string]any data map[string]any
@ -310,13 +318,6 @@ func (c *Config) parseContent() error {
return nil 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 // parseValue parses a value after an equals sign
func (c *Config) parseValue() (any, error) { func (c *Config) parseValue() (any, error) {
token, err := c.nextToken() token, err := c.nextToken()
@ -366,129 +367,134 @@ func (c *Config) parseValue() (any, error) {
// parseObject parses a map or array // parseObject parses a map or array
func (c *Config) parseObject() (any, error) { func (c *Config) parseObject() (any, error) {
// Get a map from the pool // Initialize stack with first state
contents := valuePool.Get().(map[string]any) stack := []*ParseState{{
// Clear the map to reuse it object: make(map[string]any, 8),
for k := range contents { arrayElements: make([]any, 0, 8),
delete(contents, k) isArray: true,
} }}
// Ensure map is returned to pool on function exit for len(stack) > 0 {
defer func() { // Get current state from top of stack
// Only return to pool if we're using array (contents becomes unused) current := stack[len(stack)-1]
// 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() token, err := c.nextToken()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Check for end of object // Handle closing brace - finish current object/array
if token.Type == TokenCloseBrace { if token.Type == TokenCloseBrace {
if isArray && len(contents) == 0 { // Determine result based on what we've collected
// Using array, set contents to nil to signal in defer that it should be returned to pool var result any
contentsToReturn := contents if current.isArray && len(current.object) == 0 {
contents = nil result = current.arrayElements
valuePool.Put(contentsToReturn) } else {
return arrayElements, nil result = current.object
} }
// We're returning contents directly, set to nil to signal in defer not to return to pool // Pop the stack
result := contents stack = stack[:len(stack)-1]
contents = nil
// If stack is empty, we're done with the root object
if len(stack) == 0 {
return result, nil return result, nil
} }
// Handle based on token type // 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 { switch token.Type {
case TokenName: case TokenName:
// Get the name value - must copy for stability
name := string(token.Value) name := string(token.Value)
// Get the next token to determine if it's a map entry or array element // Look ahead to determine context
nextToken, err := c.nextToken() nextToken, err := c.nextToken()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if nextToken.Type == TokenEquals { if nextToken.Type == TokenEquals {
// It's a key-value pair // Key-value pair
value, err := c.parseValue() current.isArray = false
current.currentKey = name
current.expectValue = true
// Parse the value
valueToken, err := c.nextToken()
if err != nil { if err != nil {
return nil, err return nil, err
} }
isArray = false if valueToken.Type == TokenOpenBrace {
contents[name] = value // Push new state for nested object/array
} else if nextToken.Type == TokenOpenBrace { newState := &ParseState{
// It's a nested object object: make(map[string]any, 8),
objValue, err := c.parseObject() arrayElements: make([]any, 0, 8),
if err != nil { isArray: true,
return nil, err
} }
stack = append(stack, newState)
isArray = false
contents[name] = objValue
} else { } else {
// Put the token back and treat the name as an array element // 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) c.scanner.UnreadToken(nextToken)
// Convert to appropriate type if possible // Convert name to appropriate type
var value any = name value := c.convertNameValue(name)
current.arrayElements = append(current.arrayElements, value)
// 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: case TokenString, TokenNumber, TokenBoolean:
// Direct array element value := c.tokenToValue(token)
var value any
switch token.Type { if current.expectValue {
case TokenString: current.object[current.currentKey] = value
value = string(token.Value) current.expectValue = false
case TokenNumber: } else {
strVal := string(token.Value) current.arrayElements = append(current.arrayElements, value)
value, _ = parseStringAsNumber(strVal)
case TokenBoolean:
value = bytesEqual(token.Value, []byte("true"))
} }
arrayElements = append(arrayElements, value)
case TokenOpenBrace: case TokenOpenBrace:
// Nested object in array // New nested object/array
nestedObj, err := c.parseObject() newState := &ParseState{
if err != nil { object: make(map[string]any, 8),
return nil, err arrayElements: make([]any, 0, 8),
isArray: true,
} }
arrayElements = append(arrayElements, nestedObj) stack = append(stack, newState)
default: default:
return nil, c.Error(fmt.Sprintf("unexpected token in object: %v", token.Type)) 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 // Load parses a config from a reader
@ -559,3 +565,31 @@ func parseStringAsNumber(s string) (any, error) {
// It's an integer // It's an integer
return strconv.ParseInt(s, 10, 64) 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
}