package fin /* data.go Copyright 2025 Sharkk, sharkk.net Authors: Sky Johnson */ import ( "fmt" "io" "strconv" ) // Data holds a single hierarchical structure and handles parsing type Data 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 } // NewData creates a new empty data structure func NewData() *Data { dataRef := GetMap() data := *dataRef d := &Data{ data: data, dataRef: dataRef, stack: make([]map[string]any, 0, 8), } d.currentObject = d.data return d } // Release frees any resources and returns them to pools func (d *Data) Release() { if d.scanner != nil { ReleaseScanner(d.scanner) d.scanner = nil } if d.dataRef != nil { PutMap(d.dataRef) d.data = nil d.dataRef = nil } d.currentObject = nil d.stack = nil } // GetData retrieves the entirety of the internal data map func (d *Data) GetData() map[string]any { return d.data } // Get retrieves a value from the data using dot notation func (d *Data) Get(key string) (any, error) { if key == "" { return d.data, nil } var current any = d.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 (d *Data) GetOr(key string, defaultValue any) any { val, err := d.Get(key) if err != nil { return defaultValue } return val } // GetString gets a value as string func (d *Data) GetString(key string) (string, error) { val, err := d.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 (d *Data) GetBool(key string) (bool, error) { val, err := d.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 int func (d *Data) GetInt(key string) (int, error) { val, err := d.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 (d *Data) GetFloat(key string) (float64, error) { val, err := d.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 (d *Data) GetArray(key string) ([]any, error) { val, err := d.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 (d *Data) GetMap(key string) (map[string]any, error) { val, err := d.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 (d *Data) Error(msg string) error { return fmt.Errorf("line %d, column %d: %s", d.currentToken.Line, d.currentToken.Column, msg) } // Parse parses the data from a reader func (d *Data) Parse(r io.Reader) error { d.scanner = NewScanner(r) d.currentObject = d.data err := d.parseContent() // Clean up scanner resources even on success if d.scanner != nil { ReleaseScanner(d.scanner) d.scanner = nil } return err } // nextToken gets the next meaningful token (skipping comments) func (d *Data) nextToken() (Token, error) { for { token, err := d.scanner.NextToken() if err != nil { return token, err } // Skip comment tokens if token.Type != TokenComment { d.currentToken = token return token, nil } } } // parseContent is the main parsing function func (d *Data) parseContent() error { for { token, err := d.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 d.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 := d.nextToken() if err != nil { if err == io.EOF { // EOF after name - store as empty string d.currentObject[name] = "" break } return err } var value any if nextToken.Type == TokenOpenBrace { // It's a nested object/array value, err = d.parseObject() if err != nil { return err } } else { // It's a simple value value = d.tokenToValue(nextToken) // Check for potential nested object - look ahead lookAhead, nextErr := d.nextToken() if nextErr == nil && lookAhead.Type == TokenOpenBrace { // It's a complex object that follows a value nestedValue, err := d.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 d.scanner.UnreadToken(lookAhead) } } // Store the value in the data d.currentObject[name] = value } return nil } // parseObject parses a map or array func (d *Data) 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 := d.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 := d.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 := d.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 := d.tokenToValue(nextToken) objectElements[key] = value // Check if there's an object following lookAhead, nextErr := d.nextToken() if nextErr == nil && lookAhead.Type == TokenOpenBrace { // Nested object after value nestedValue, err := d.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 { d.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 d.scanner.UnreadToken(lookAhead) } } case TokenString, TokenNumber, TokenBoolean: // Array element value := d.tokenToValue(token) arrayElements = append(arrayElements, value) case TokenOpenBrace: // Nested object/array nestedValue, err := d.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, d.Error("unexpected nested object without a key") } default: return nil, d.Error(fmt.Sprintf("unexpected token type: %v", token.Type)) } } } // Load parses data from a reader func Load(r io.Reader) (*Data, error) { data := NewData() err := data.Parse(r) if err != nil { data.Release() return nil, err } return data, nil } // tokenToValue converts a token to a Go value, preserving byte slices until final conversion func (d *Data) 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 }