ref 6
This commit is contained in:
parent
e7584879a3
commit
8258a967a2
335
config.go
335
config.go
|
@ -4,18 +4,26 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds a single hierarchical structure like JSON
|
// 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
|
||||||
|
scanner *Scanner
|
||||||
|
currentObject map[string]any
|
||||||
|
stack []map[string]any
|
||||||
|
currentToken Token
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig creates a new empty config
|
// NewConfig creates a new empty config
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
return &Config{
|
cfg := &Config{
|
||||||
data: make(map[string]any),
|
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
|
// Get retrieves a value from the config using dot notation
|
||||||
|
@ -187,10 +195,312 @@ func (c *Config) GetMap(key string) (map[string]any, error) {
|
||||||
return nil, fmt.Errorf("value for key %s is not a map", key)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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) {
|
||||||
|
// Get a map from the pool
|
||||||
|
contents := valuePool.Get().(map[string]any)
|
||||||
|
// Clear the map to reuse it
|
||||||
|
for k := range contents {
|
||||||
|
delete(contents, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure map is returned to pool on function exit
|
||||||
|
defer func() {
|
||||||
|
// Only return to pool if we're using array (contents becomes unused)
|
||||||
|
// 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()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for end of object
|
||||||
|
if token.Type == TokenCloseBrace {
|
||||||
|
if isArray && len(contents) == 0 {
|
||||||
|
// Using array, set contents to nil to signal in defer that it should be returned to pool
|
||||||
|
contentsToReturn := contents
|
||||||
|
contents = nil
|
||||||
|
valuePool.Put(contentsToReturn)
|
||||||
|
return arrayElements, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're returning contents directly, set to nil to signal in defer not to return to pool
|
||||||
|
result := contents
|
||||||
|
contents = nil
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle based on token type
|
||||||
|
switch token.Type {
|
||||||
|
case TokenName:
|
||||||
|
// Get the name value - must copy for stability
|
||||||
|
name := string(token.Value)
|
||||||
|
|
||||||
|
// Get the next token to determine if it's a map entry or array element
|
||||||
|
nextToken, err := c.nextToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if nextToken.Type == TokenEquals {
|
||||||
|
// It's a key-value pair
|
||||||
|
value, err := c.parseValue()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isArray = false
|
||||||
|
contents[name] = value
|
||||||
|
} else if nextToken.Type == TokenOpenBrace {
|
||||||
|
// It's a nested object
|
||||||
|
objValue, err := c.parseObject()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isArray = false
|
||||||
|
contents[name] = objValue
|
||||||
|
} else {
|
||||||
|
// Put the token back and treat the name as an array element
|
||||||
|
c.scanner.UnreadToken(nextToken)
|
||||||
|
|
||||||
|
// Convert to appropriate type if possible
|
||||||
|
var value any = name
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
// Direct array element
|
||||||
|
var value any
|
||||||
|
|
||||||
|
switch token.Type {
|
||||||
|
case TokenString:
|
||||||
|
value = string(token.Value)
|
||||||
|
case TokenNumber:
|
||||||
|
strVal := string(token.Value)
|
||||||
|
value, _ = parseStringAsNumber(strVal)
|
||||||
|
case TokenBoolean:
|
||||||
|
value = bytesEqual(token.Value, []byte("true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayElements = append(arrayElements, value)
|
||||||
|
|
||||||
|
case TokenOpenBrace:
|
||||||
|
// Nested object in array
|
||||||
|
nestedObj, err := c.parseObject()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
arrayElements = append(arrayElements, nestedObj)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, c.Error(fmt.Sprintf("unexpected token in object: %v", token.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load parses a config from a reader
|
// Load parses a config from a reader
|
||||||
func Load(r io.Reader) (*Config, error) {
|
func Load(r io.Reader) (*Config, error) {
|
||||||
parser := NewParser(r)
|
config := NewConfig()
|
||||||
return parser.Parse()
|
err := config.Parse(r)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
@ -216,6 +526,19 @@ func ParseNumber(s string) (any, error) {
|
||||||
return strconv.ParseInt(s, 10, 64)
|
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
|
// isDigitOrMinus checks if a string starts with a digit or minus sign
|
||||||
func isDigitOrMinus(s string) bool {
|
func isDigitOrMinus(s string) bool {
|
||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
|
|
282
parser.go
282
parser.go
|
@ -1,282 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parser parses configuration files
|
|
||||||
type Parser struct {
|
|
||||||
scanner *Scanner
|
|
||||||
config *Config
|
|
||||||
currentObject map[string]any
|
|
||||||
stack []map[string]any
|
|
||||||
currentToken Token
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewParser creates a new parser with a reader and empty config
|
|
||||||
func NewParser(r io.Reader) *Parser {
|
|
||||||
config := NewConfig()
|
|
||||||
return &Parser{
|
|
||||||
scanner: NewScanner(r),
|
|
||||||
config: config,
|
|
||||||
currentObject: config.data,
|
|
||||||
stack: make([]map[string]any, 0, 8), // Pre-allocate stack with reasonable capacity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error creates an error with line information from the current token
|
|
||||||
func (p *Parser) Error(msg string) error {
|
|
||||||
return fmt.Errorf("line %d, column %d: %s",
|
|
||||||
p.currentToken.Line, p.currentToken.Column, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses the config file and returns a Config
|
|
||||||
func (p *Parser) Parse() (*Config, error) {
|
|
||||||
err := p.parseContent()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return p.config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// nextToken gets the next meaningful token (skipping comments)
|
|
||||||
func (p *Parser) nextToken() (Token, error) {
|
|
||||||
for {
|
|
||||||
token, err := p.scanner.NextToken()
|
|
||||||
if err != nil {
|
|
||||||
return token, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip comment tokens
|
|
||||||
if token.Type != TokenComment {
|
|
||||||
p.currentToken = token
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseContent is the main parsing function
|
|
||||||
func (p *Parser) parseContent() error {
|
|
||||||
for {
|
|
||||||
token, err := p.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 p.Error("expected name at top level")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the property name
|
|
||||||
name := string(token.Value)
|
|
||||||
|
|
||||||
// Get the next token (should be = or {)
|
|
||||||
token, err = p.nextToken()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var value any
|
|
||||||
|
|
||||||
if token.Type == TokenEquals {
|
|
||||||
// It's a standard key=value assignment
|
|
||||||
value, err = p.parseValue()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if token.Type == TokenOpenBrace {
|
|
||||||
// It's a map/array without '='
|
|
||||||
value, err = p.parseObject()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return p.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
|
|
||||||
p.currentObject[name] = newMap
|
|
||||||
|
|
||||||
// Process the map contents
|
|
||||||
p.stack = append(p.stack, p.currentObject)
|
|
||||||
p.currentObject = newMap
|
|
||||||
|
|
||||||
// Copy values from scanned map to our object
|
|
||||||
for k, v := range mapValue {
|
|
||||||
p.currentObject[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore parent object
|
|
||||||
n := len(p.stack)
|
|
||||||
if n > 0 {
|
|
||||||
p.currentObject = p.stack[n-1]
|
|
||||||
p.stack = p.stack[:n-1]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Direct storage for primitives and arrays
|
|
||||||
p.currentObject[name] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseValue parses a value after an equals sign
|
|
||||||
func (p *Parser) parseValue() (any, error) {
|
|
||||||
token, err := p.nextToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch token.Type {
|
|
||||||
case TokenString:
|
|
||||||
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, p.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, p.Error(fmt.Sprintf("invalid integer: %s", strValue))
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
|
|
||||||
case TokenBoolean:
|
|
||||||
return string(token.Value) == "true", nil
|
|
||||||
|
|
||||||
case TokenOpenBrace:
|
|
||||||
// It's a map or array
|
|
||||||
return p.parseObject()
|
|
||||||
|
|
||||||
case TokenName:
|
|
||||||
// Treat as a string value
|
|
||||||
return string(token.Value), nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, p.Error(fmt.Sprintf("unexpected token: %v", token.Type))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseObject parses a map or array
|
|
||||||
func (p *Parser) parseObject() (any, error) {
|
|
||||||
contents := make(map[string]any)
|
|
||||||
var arrayElements []any
|
|
||||||
isArray := true
|
|
||||||
|
|
||||||
for {
|
|
||||||
token, err := p.nextToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for end of object
|
|
||||||
if token.Type == TokenCloseBrace {
|
|
||||||
if isArray && len(contents) == 0 {
|
|
||||||
return arrayElements, nil
|
|
||||||
}
|
|
||||||
return contents, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle based on token type
|
|
||||||
switch token.Type {
|
|
||||||
case TokenName:
|
|
||||||
// Get the name value
|
|
||||||
name := string(token.Value)
|
|
||||||
|
|
||||||
// Get the next token to determine if it's a map entry or array element
|
|
||||||
nextToken, err := p.nextToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if nextToken.Type == TokenEquals {
|
|
||||||
// It's a key-value pair
|
|
||||||
value, err := p.parseValue()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
isArray = false
|
|
||||||
contents[name] = value
|
|
||||||
} else if nextToken.Type == TokenOpenBrace {
|
|
||||||
// It's a nested object
|
|
||||||
objValue, err := p.parseObject()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
isArray = false
|
|
||||||
contents[name] = objValue
|
|
||||||
} else {
|
|
||||||
// Put the token back and treat the name as an array element
|
|
||||||
p.scanner.UnreadToken(nextToken)
|
|
||||||
|
|
||||||
// Convert to appropriate type if possible
|
|
||||||
var value any = name
|
|
||||||
|
|
||||||
// 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:
|
|
||||||
// Direct array element
|
|
||||||
var value any
|
|
||||||
|
|
||||||
switch token.Type {
|
|
||||||
case TokenString:
|
|
||||||
value = string(token.Value)
|
|
||||||
case TokenNumber:
|
|
||||||
strVal := string(token.Value)
|
|
||||||
value, _ = parseStringAsNumber(strVal)
|
|
||||||
case TokenBoolean:
|
|
||||||
value = string(token.Value) == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
arrayElements = append(arrayElements, value)
|
|
||||||
|
|
||||||
case TokenOpenBrace:
|
|
||||||
// Nested object in array
|
|
||||||
nestedObj, err := p.parseObject()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
arrayElements = append(arrayElements, nestedObj)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, p.Error(fmt.Sprintf("unexpected token in object: %v", token.Type))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
51
scanner.go
51
scanner.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pre-declared errors to reduce allocations
|
// Pre-declared errors to reduce allocations
|
||||||
|
@ -19,19 +20,41 @@ var (
|
||||||
// Scanner handles the low-level parsing of the configuration format
|
// Scanner handles the low-level parsing of the configuration format
|
||||||
type Scanner struct {
|
type Scanner struct {
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
line int // Current line number
|
line int
|
||||||
col int // Current column position
|
col int
|
||||||
buffer []byte
|
buffer []byte
|
||||||
token Token // Current token
|
token Token // Current token for unread
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScanner creates a new scanner with the given reader
|
// scannerPool helps reuse scanner objects
|
||||||
|
var scannerPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return &Scanner{
|
||||||
|
line: 1,
|
||||||
|
col: 0,
|
||||||
|
buffer: make([]byte, 0, 128),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScanner creates a new scanner from a pool
|
||||||
func NewScanner(r io.Reader) *Scanner {
|
func NewScanner(r io.Reader) *Scanner {
|
||||||
return &Scanner{
|
s := scannerPool.Get().(*Scanner)
|
||||||
reader: bufio.NewReader(r),
|
s.reader = bufio.NewReader(r)
|
||||||
line: 1, // Start at line 1
|
s.line = 1
|
||||||
col: 0,
|
s.col = 0
|
||||||
buffer: make([]byte, 0, 128), // Pre-allocate with reasonable capacity
|
s.buffer = s.buffer[:0]
|
||||||
|
s.token = Token{Type: TokenError}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseScanner returns a scanner to the pool
|
||||||
|
func ReleaseScanner(s *Scanner) {
|
||||||
|
if s != nil {
|
||||||
|
// Clear references but keep allocated memory
|
||||||
|
s.reader = nil
|
||||||
|
s.buffer = s.buffer[:0]
|
||||||
|
scannerPool.Put(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +305,7 @@ func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the buffer directly - consumer is responsible for copying if needed
|
// Return token with buffer value - important: consumer must copy if needed
|
||||||
return Token{
|
return Token{
|
||||||
Type: TokenString,
|
Type: TokenString,
|
||||||
Value: s.buffer,
|
Value: s.buffer,
|
||||||
|
@ -323,15 +346,15 @@ func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
|
||||||
_, _ = s.ReadByte()
|
_, _ = s.ReadByte()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a boolean - fixed comparison
|
// Check if it's a boolean - use direct byte comparison
|
||||||
tokenType := TokenName
|
tokenType := TokenName
|
||||||
if string(s.buffer) == "true" || string(s.buffer) == "false" {
|
if bytesEqual(s.buffer, []byte("true")) || bytesEqual(s.buffer, []byte("false")) {
|
||||||
tokenType = TokenBoolean
|
tokenType = TokenBoolean
|
||||||
}
|
}
|
||||||
|
|
||||||
return Token{
|
return Token{
|
||||||
Type: tokenType,
|
Type: tokenType,
|
||||||
Value: s.buffer,
|
Value: s.buffer, // Direct buffer reference - consumer must copy!
|
||||||
Line: startLine,
|
Line: startLine,
|
||||||
Column: startColumn,
|
Column: startColumn,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -374,7 +397,7 @@ func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
|
||||||
|
|
||||||
return Token{
|
return Token{
|
||||||
Type: TokenNumber,
|
Type: TokenNumber,
|
||||||
Value: s.buffer,
|
Value: s.buffer, // Direct buffer reference - consumer must copy!
|
||||||
Line: startLine,
|
Line: startLine,
|
||||||
Column: startColumn,
|
Column: startColumn,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
2
token.go
2
token.go
|
@ -19,7 +19,7 @@ const (
|
||||||
// Token represents a lexical token
|
// Token represents a lexical token
|
||||||
type Token struct {
|
type Token struct {
|
||||||
Type TokenType
|
Type TokenType
|
||||||
Value []byte
|
Value []byte // Not modified after returning - caller must copy if needed
|
||||||
Line int
|
Line int
|
||||||
Column int
|
Column int
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user