Compare commits

..

No commits in common. "master" and "v1.1.0" have entirely different histories.

8 changed files with 638 additions and 546 deletions

View File

@ -110,54 +110,6 @@ firstIP, err := cfg.GetString("allowed_ips.0")
username, err := cfg.GetString("database.credentials.username") username, err := cfg.GetString("database.credentials.username")
``` ```
## Writing Data
Fin supports writing data back to its format, allowing you to create or modify data at runtime:
```go
package main
import (
"os"
"git.sharkk.net/Sharkk/Fin"
)
func main() {
// Create new data structure
data := fin.NewData()
// Set values
data.GetData()["server"] = map[string]any{
"host": "localhost",
"port": 8080,
}
data.GetData()["debug"] = true
data.GetData()["allowed_ips"] = []any{
"192.168.1.1",
"10.0.0.1",
}
// Write to file
file, err := os.Create("config.conf")
if err != nil {
panic(err)
}
defer file.Close()
// Use the Write method
err = data.Write(file)
if err != nil {
panic(err)
}
// Or use the standalone Save function
err = fin.Save(file, data)
if err != nil {
panic(err)
}
}
```
## Performance ## Performance
Fin goes blow-for-blow against Go's standard JSON library, and performs incredibly versus Fin goes blow-for-blow against Go's standard JSON library, and performs incredibly versus

View File

@ -20,9 +20,9 @@ func BenchmarkRetrieveSimpleValues(b *testing.B) {
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
reader := strings.NewReader(configData) reader := strings.NewReader(configData)
cfg := config.Load(reader) cfg, err := config.Load(reader)
if cfg.HasError() { if err != nil {
b.Fatalf("Failed to parse config: %v", cfg.GetError().Error()) b.Fatalf("Failed to parse config: %v", err)
} }
b.ResetTimer() b.ResetTimer()
@ -75,9 +75,9 @@ func BenchmarkRetrieveNestedValues(b *testing.B) {
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
reader := strings.NewReader(configData) reader := strings.NewReader(configData)
cfg := config.Load(reader) cfg, err := config.Load(reader)
if cfg.HasError() { if err != nil {
b.Fatalf("Failed to parse config: %v", cfg.GetError().Error()) b.Fatalf("Failed to parse config: %v", err)
} }
b.ResetTimer() b.ResetTimer()
@ -125,9 +125,9 @@ func BenchmarkRetrieveArrayValues(b *testing.B) {
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
reader := strings.NewReader(configData) reader := strings.NewReader(configData)
cfg := config.Load(reader) cfg, err := config.Load(reader)
if cfg.HasError() { if err != nil {
b.Fatalf("Failed to parse config: %v", cfg.GetError().Error()) b.Fatalf("Failed to parse config: %v", err)
} }
b.ResetTimer() b.ResetTimer()
@ -180,9 +180,9 @@ func BenchmarkRetrieveMixedValues(b *testing.B) {
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
reader := strings.NewReader(configData) reader := strings.NewReader(configData)
cfg := config.Load(reader) cfg, err := config.Load(reader)
if cfg.HasError() { if err != nil {
b.Fatalf("Failed to parse config: %v", cfg.GetError().Error()) b.Fatalf("Failed to parse config: %v", err)
} }
b.ResetTimer() b.ResetTimer()
@ -531,14 +531,14 @@ write = 10
// Custom config // Custom config
customReader := strings.NewReader(customConfig) customReader := strings.NewReader(customConfig)
customCfg := config.Load(customReader) customCfg, err := config.Load(customReader)
if customCfg.HasError() { if err != nil {
b.Fatalf("Failed to parse custom config: %v", customCfg.GetError().Error()) b.Fatalf("Failed to parse custom config: %v", err)
} }
// JSON config // JSON config
var jsonResult map[string]any var jsonResult map[string]any
err := json.Unmarshal([]byte(jsonConfig), &jsonResult) err = json.Unmarshal([]byte(jsonConfig), &jsonResult)
if err != nil { if err != nil {
b.Fatalf("Failed to parse JSON: %v", err) b.Fatalf("Failed to parse JSON: %v", err)
} }
@ -605,8 +605,8 @@ write = 10
b.Run("Direct-ArrayGet", func(b *testing.B) { b.Run("Direct-ArrayGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := directData["app"].(map[string]any) app := directData["app"].(map[string]any)
environments := app["environments"].([]string) environments := app["environments"].([]any)
_ = environments[1] _ = environments[1].(string)
} }
}) })

View File

@ -23,9 +23,9 @@ func BenchmarkSmallConfig(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
reader := strings.NewReader(smallConfig) reader := strings.NewReader(smallConfig)
cfg := config.Load(reader) _, err := config.Load(reader)
if cfg.HasError() { if err != nil {
b.Fatalf("Failed to parse small config: %v", cfg.GetError().Error()) b.Fatalf("Failed to parse small config: %v", err)
} }
} }
} }
@ -74,9 +74,9 @@ func BenchmarkMediumConfig(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
reader := strings.NewReader(mediumConfig) reader := strings.NewReader(mediumConfig)
cfg := config.Load(reader) _, err := config.Load(reader)
if cfg.HasError() { if err != nil {
b.Fatalf("Failed to parse small config: %v", cfg.GetError().Error()) b.Fatalf("Failed to parse medium config: %v", err)
} }
} }
} }
@ -170,9 +170,9 @@ func BenchmarkLargeConfig(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
reader := strings.NewReader(largeConfig) reader := strings.NewReader(largeConfig)
cfg := config.Load(reader) _, err := config.Load(reader)
if cfg.HasError() { if err != nil {
b.Fatalf("Failed to parse small config: %v", cfg.GetError().Error()) b.Fatalf("Failed to parse large config: %v", err)
} }
} }
} }

467
data.go
View File

@ -1,27 +1,32 @@
package fin package fin
/*
data.go
Copyright 2025 Sharkk, sharkk.net
Authors: Sky Johnson
*/
import ( import (
"fmt" "fmt"
"io" "io"
"maps"
"os"
"slices"
"strconv" "strconv"
) )
// Data holds a single hierarchical structure and handles parsing
type Data struct { type Data struct {
data map[string]any data map[string]any
dataRef *map[string]any dataRef *map[string]any // Reference to pooled map
scanner *Scanner scanner *Scanner
currentObject map[string]any currentObject map[string]any
stack []map[string]any stack []map[string]any
currentToken Token currentToken Token
lastError error
} }
// NewData creates a new empty data structure
func NewData() *Data { func NewData() *Data {
dataRef := GetMap() dataRef := GetMap()
data := *dataRef data := *dataRef
d := &Data{ d := &Data{
data: data, data: data,
dataRef: dataRef, dataRef: dataRef,
@ -31,11 +36,13 @@ func NewData() *Data {
return d return d
} }
// Release frees any resources and returns them to pools
func (d *Data) Release() { func (d *Data) Release() {
if d.scanner != nil { if d.scanner != nil {
ReleaseScanner(d.scanner) ReleaseScanner(d.scanner)
d.scanner = nil d.scanner = nil
} }
if d.dataRef != nil { if d.dataRef != nil {
PutMap(d.dataRef) PutMap(d.dataRef)
d.data = nil d.data = nil
@ -45,22 +52,28 @@ func (d *Data) Release() {
d.stack = nil d.stack = nil
} }
func (d *Data) GetData() map[string]any { return d.data } // 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) { func (d *Data) Get(key string) (any, error) {
if key == "" { if key == "" {
return d.data, nil return d.data, nil
} }
var current any = d.data var current any = d.data
start := 0 start := 0
keyLen := len(key) keyLen := len(key)
for i := range keyLen { for i := 0; i < keyLen; i++ {
if key[i] == '.' || i == keyLen-1 { if key[i] == '.' || i == keyLen-1 {
end := i end := i
if i == keyLen-1 && key[i] != '.' { if i == keyLen-1 && key[i] != '.' {
end = i + 1 end = i + 1
} }
part := key[start:end] part := key[start:end]
switch node := current.(type) { switch node := current.(type) {
@ -79,36 +92,21 @@ func (d *Data) Get(key string) (any, error) {
return nil, fmt.Errorf("array index out of bounds: %d", index) return nil, fmt.Errorf("array index out of bounds: %d", index)
} }
current = node[index] current = node[index]
case []string:
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]
case []int:
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: default:
return nil, fmt.Errorf("cannot access %s in non-container value", part) return nil, fmt.Errorf("cannot access %s in non-container value", part)
} }
if i == keyLen-1 { if i == keyLen-1 {
return current, nil return current, nil
} }
start = i + 1 start = i + 1
} }
} }
return current, nil return current, nil
} }
// GetOr retrieves a value or returns a default if not found
func (d *Data) GetOr(key string, defaultValue any) any { func (d *Data) GetOr(key string, defaultValue any) any {
val, err := d.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
@ -117,214 +115,139 @@ func (d *Data) GetOr(key string, defaultValue any) any {
return val return val
} }
func convertTo[T any](val any, key string, converter func(any) (T, bool)) (T, error) { // GetString gets a value as string
var zero T
result, ok := converter(val)
if ok {
return result, nil
}
return zero, fmt.Errorf("value for key %s cannot be converted to %T", key, zero)
}
func toString(val any) (string, bool) {
switch v := val.(type) {
case string:
return v, true
case bool:
return strconv.FormatBool(v), true
case int:
return strconv.Itoa(v), true
case float64:
return strconv.FormatFloat(v, 'f', -1, 64), true
}
return "", false
}
func toBool(val any) (bool, bool) {
switch v := val.(type) {
case bool:
return v, true
case string:
if result, err := strconv.ParseBool(v); err == nil {
return result, true
}
}
return false, false
}
func toInt(val any) (int, bool) {
switch v := val.(type) {
case int:
return v, true
case float64:
return int(v), true
case string:
if result, err := strconv.Atoi(v); err == nil {
return result, true
}
}
return 0, false
}
func toFloat(val any) (float64, bool) {
switch v := val.(type) {
case float64:
return v, true
case int:
return float64(v), true
case string:
if result, err := strconv.ParseFloat(v, 64); err == nil {
return result, true
}
}
return 0, false
}
func (d *Data) GetString(key string) (string, error) { func (d *Data) GetString(key string) (string, error) {
val, err := d.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return "", err return "", err
} }
return convertTo(val, key, toString)
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) { func (d *Data) GetBool(key string) (bool, error) {
val, err := d.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return false, err return false, err
} }
return convertTo(val, key, toBool)
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) { func (d *Data) GetInt(key string) (int, error) {
val, err := d.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return convertTo(val, key, toInt)
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) { func (d *Data) GetFloat(key string) (float64, error) {
val, err := d.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return 0, err return 0, err
} }
return convertTo(val, key, toFloat)
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) { func (d *Data) GetArray(key string) ([]any, error) {
val, err := d.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch v := val.(type) { if arr, ok := val.([]any); ok {
case []any: return arr, nil
return v, nil
case []string:
result := make([]any, len(v))
for i, s := range v {
result[i] = s
}
return result, nil
case []int:
result := make([]any, len(v))
for i, n := range v {
result[i] = n
}
return result, nil
default:
return nil, fmt.Errorf("value for key %s is not an array", key)
} }
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) { func (d *Data) GetMap(key string) (map[string]any, error) {
val, err := d.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if m, ok := val.(map[string]any); ok { if m, ok := val.(map[string]any); ok {
return m, nil return m, nil
} }
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)
} }
func getTypedArray[T any](d *Data, key string, converter func(any) (T, bool)) ([]T, error) { // Error creates an error with line information from the current token
arr, err := d.GetArray(key)
if err != nil {
return nil, err
}
result := make([]T, len(arr))
for i, v := range arr {
converted, ok := converter(v)
if !ok {
return nil, fmt.Errorf("array element at index %d cannot be converted", i)
}
result[i] = converted
}
return result, nil
}
func (d *Data) GetStringArray(key string) ([]string, error) {
return getTypedArray(d, key, func(v any) (string, bool) {
s, ok := v.(string)
return s, ok
})
}
func (d *Data) GetIntArray(key string) ([]int, error) {
return getTypedArray(d, key, func(v any) (int, bool) {
switch val := v.(type) {
case int:
return val, true
case float64:
return int(val), true
}
return 0, false
})
}
func (d *Data) GetFloatArray(key string) ([]float64, error) {
return getTypedArray(d, key, func(v any) (float64, bool) {
switch val := v.(type) {
case float64:
return val, true
case int:
return float64(val), true
}
return 0, false
})
}
func (d *Data) Error(msg string) error { func (d *Data) Error(msg string) error {
return fmt.Errorf("line %d, column %d: %s", d.currentToken.Line, d.currentToken.Column, msg) return fmt.Errorf("line %d, column %d: %s",
} d.currentToken.Line, d.currentToken.Column, msg)
func (d *Data) makeStringKey(tokenValue []byte) string {
keyBytes := GetByteSlice()
*keyBytes = append((*keyBytes)[:0], tokenValue...)
result := string(*keyBytes)
PutByteSlice(keyBytes)
return result
} }
// Parse parses the data from a reader
func (d *Data) Parse(r io.Reader) error { func (d *Data) Parse(r io.Reader) error {
d.scanner = NewScanner(r) d.scanner = NewScanner(r)
d.currentObject = d.data d.currentObject = d.data
err := d.parseContent() err := d.parseContent()
// Clean up scanner resources even on success
if d.scanner != nil { if d.scanner != nil {
ReleaseScanner(d.scanner) ReleaseScanner(d.scanner)
d.scanner = nil d.scanner = nil
} }
return err return err
} }
// nextToken gets the next meaningful token (skipping comments)
func (d *Data) nextToken() (Token, error) { func (d *Data) nextToken() (Token, error) {
for { for {
token, err := d.scanner.NextToken() token, err := d.scanner.NextToken()
if err != nil { if err != nil {
return token, err return token, err
} }
// Skip comment tokens
if token.Type != TokenComment { if token.Type != TokenComment {
d.currentToken = token d.currentToken = token
return token, nil return token, nil
@ -332,24 +255,35 @@ func (d *Data) nextToken() (Token, error) {
} }
} }
// parseContent is the main parsing function
func (d *Data) parseContent() error { func (d *Data) parseContent() error {
for { for {
token, err := d.nextToken() token, err := d.nextToken()
if err != nil { if err != nil {
return err return err
} }
// Check for end of file
if token.Type == TokenEOF { if token.Type == TokenEOF {
break break
} }
// We expect top level entries to be names
if token.Type != TokenName { if token.Type != TokenName {
return d.Error(fmt.Sprintf("expected name at top level, got token type %v", token.Type)) return d.Error(fmt.Sprintf("expected name at top level, got token type %v", token.Type))
} }
name := d.makeStringKey(token.Value) // 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() nextToken, err := d.nextToken()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
// EOF after name - store as empty string
d.currentObject[name] = "" d.currentObject[name] = ""
break break
} }
@ -357,47 +291,65 @@ func (d *Data) parseContent() error {
} }
var value any var value any
if nextToken.Type == TokenOpenBrace { if nextToken.Type == TokenOpenBrace {
// It's a nested object/array
value, err = d.parseObject() value, err = d.parseObject()
if err != nil { if err != nil {
return err return err
} }
} else { } else {
// It's a simple value
value = d.tokenToValue(nextToken) value = d.tokenToValue(nextToken)
// Check for potential nested object - look ahead
lookAhead, nextErr := d.nextToken() lookAhead, nextErr := d.nextToken()
if nextErr == nil && lookAhead.Type == TokenOpenBrace { if nextErr == nil && lookAhead.Type == TokenOpenBrace {
// It's a complex object that follows a value
nestedValue, err := d.parseObject() nestedValue, err := d.parseObject()
if err != nil { if err != nil {
return err return err
} }
// Store the previous simple value in a map to add to the object
if mapValue, ok := nestedValue.(map[string]any); ok { if mapValue, ok := nestedValue.(map[string]any); ok {
// Create a new map value with both the simple value and the map
mapRef := GetMap() mapRef := GetMap()
newMap := *mapRef newMap := *mapRef
maps.Copy(newMap, mapValue) for k, v := range mapValue {
newMap["value"] = value newMap[k] = v
}
newMap["value"] = value // Store simple value under "value" key
value = newMap value = newMap
} }
} else if nextErr == nil && lookAhead.Type != TokenEOF { } else if nextErr == nil && lookAhead.Type != TokenEOF {
// Put the token back if it's not EOF
d.scanner.UnreadToken(lookAhead) d.scanner.UnreadToken(lookAhead)
} }
} }
// Store the value in the data
d.currentObject[name] = value d.currentObject[name] = value
} }
return nil return nil
} }
// parseObject parses a map or array
func (d *Data) parseObject() (any, error) { func (d *Data) parseObject() (any, error) {
// Default to treating as an array until we see a name
isArray := true isArray := true
arrayRef := GetArray() arrayRef := GetArray()
arrayElements := *arrayRef arrayElements := *arrayRef
mapRef := GetMap() mapRef := GetMap()
objectElements := *mapRef objectElements := *mapRef
defer func() { defer func() {
if !isArray { if !isArray {
PutArray(arrayRef) PutArray(arrayRef) // We didn't use the array
} else { } else {
PutMap(mapRef) PutMap(mapRef) // We didn't use the map
} }
}() }()
@ -410,23 +362,34 @@ func (d *Data) parseObject() (any, error) {
return nil, err return nil, err
} }
// Handle closing brace - finish current object/array
if token.Type == TokenCloseBrace { if token.Type == TokenCloseBrace {
if isArray && len(arrayElements) > 0 { if isArray && len(arrayElements) > 0 {
result := optimizeArray(arrayElements) result := arrayElements
*arrayRef = nil // Don't release the array, transfer ownership
*arrayRef = nil // Detach from pool reference
return result, nil return result, nil
} }
result := objectElements result := objectElements
*mapRef = nil // Don't release the map, transfer ownership
*mapRef = nil // Detach from pool reference
return result, nil return result, nil
} }
// Handle tokens based on type
switch token.Type { switch token.Type {
case TokenName: case TokenName:
key := d.makeStringKey(token.Value) // 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() nextToken, err := d.nextToken()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
// EOF after key - store as empty value
objectElements[key] = "" objectElements[key] = ""
isArray = false isArray = false
return objectElements, nil return objectElements, nil
@ -435,49 +398,63 @@ func (d *Data) parseObject() (any, error) {
} }
if nextToken.Type == TokenOpenBrace { if nextToken.Type == TokenOpenBrace {
isArray = false // Nested object
isArray = false // If we see a key, it's a map
nestedValue, err := d.parseObject() nestedValue, err := d.parseObject()
if err != nil { if err != nil {
return nil, err return nil, err
} }
objectElements[key] = nestedValue objectElements[key] = nestedValue
} else { } else {
isArray = false // Key-value pair
isArray = false // If we see a key, it's a map
value := d.tokenToValue(nextToken) value := d.tokenToValue(nextToken)
objectElements[key] = value objectElements[key] = value
// Check if there's an object following
lookAhead, nextErr := d.nextToken() lookAhead, nextErr := d.nextToken()
if nextErr == nil && lookAhead.Type == TokenOpenBrace { if nextErr == nil && lookAhead.Type == TokenOpenBrace {
// Nested object after value
nestedValue, err := d.parseObject() nestedValue, err := d.parseObject()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Check if we need to convert the value to a map
if mapValue, ok := nestedValue.(map[string]any); ok { if mapValue, ok := nestedValue.(map[string]any); ok {
// Create a combined map
combinedMapRef := GetMap() combinedMapRef := GetMap()
combinedMap := *combinedMapRef combinedMap := *combinedMapRef
maps.Copy(combinedMap, mapValue) for k, v := range mapValue {
combinedMap[k] = v
}
combinedMap["value"] = value combinedMap["value"] = value
objectElements[key] = combinedMap objectElements[key] = combinedMap
} }
} else if nextErr == nil && lookAhead.Type != TokenEOF && lookAhead.Type != TokenCloseBrace { } else if nextErr == nil && lookAhead.Type != TokenEOF && lookAhead.Type != TokenCloseBrace {
d.scanner.UnreadToken(lookAhead) d.scanner.UnreadToken(lookAhead)
} else if nextErr == nil && lookAhead.Type == TokenCloseBrace { } 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) d.scanner.UnreadToken(lookAhead)
} }
} }
case TokenString, TokenNumber, TokenBoolean: case TokenString, TokenNumber, TokenBoolean:
// Array element
value := d.tokenToValue(token) value := d.tokenToValue(token)
arrayElements = append(arrayElements, value) arrayElements = append(arrayElements, value)
case TokenOpenBrace: case TokenOpenBrace:
// Nested object/array
nestedValue, err := d.parseObject() nestedValue, err := d.parseObject()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if isArray { if isArray {
arrayElements = append(arrayElements, nestedValue) arrayElements = append(arrayElements, nestedValue)
} else { } else {
// If we're in an object context, this is an error
return nil, d.Error("unexpected nested object without a key") return nil, d.Error("unexpected nested object without a key")
} }
@ -487,83 +464,104 @@ func (d *Data) parseObject() (any, error) {
} }
} }
func Load(r io.Reader) *Data { // Load parses data from a reader
func Load(r io.Reader) (*Data, error) {
data := NewData() data := NewData()
err := data.Parse(r) err := data.Parse(r)
if err != nil { if err != nil {
data.Release() data.Release()
emptyData := NewData() return nil, err
emptyData.lastError = err
return emptyData
} }
return data
} return data, nil
func LoadFromFile(filepath string) *Data {
file, err := os.Open(filepath)
if err != nil {
emptyData := NewData()
emptyData.lastError = err
return emptyData
}
defer file.Close()
return Load(file)
}
func (d *Data) HasError() bool {
return d.lastError != nil
}
func (d *Data) GetError() error {
return d.lastError
} }
// tokenToValue converts a token to a Go value, preserving byte slices until final conversion
func (d *Data) tokenToValue(token Token) any { func (d *Data) tokenToValue(token Token) any {
switch token.Type { switch token.Type {
case TokenString: case TokenString:
return d.makeStringKey(token.Value) // Convert to string using pooled buffer
valueBytes := GetByteSlice()
*valueBytes = append((*valueBytes)[:0], token.Value...)
result := string(*valueBytes)
PutByteSlice(valueBytes)
return result
case TokenNumber: case TokenNumber:
// Parse number
valueStr := string(token.Value) valueStr := string(token.Value)
if slices.Contains(token.Value, '.') { if containsChar(token.Value, '.') {
// Float
val, _ := strconv.ParseFloat(valueStr, 64) val, _ := strconv.ParseFloat(valueStr, 64)
return val return val
} }
// Integer
val, _ := strconv.Atoi(valueStr) val, _ := strconv.Atoi(valueStr)
return val return val
case TokenBoolean: case TokenBoolean:
return bytesEqual(token.Value, []byte("true")) return bytesEqual(token.Value, []byte("true"))
case TokenName: case TokenName:
if bytesEqual(token.Value, []byte("true")) || bytesEqual(token.Value, []byte("false")) { // Check if name is a special value
return bytesEqual(token.Value, []byte("true")) valueBytes := GetByteSlice()
} *valueBytes = append((*valueBytes)[:0], token.Value...)
if len(token.Value) > 0 && isDigitOrMinus(token.Value[0]) {
valueStr := string(token.Value) if bytesEqual(*valueBytes, []byte("true")) {
if slices.Contains(token.Value, '.') { PutByteSlice(valueBytes)
if val, err := strconv.ParseFloat(valueStr, 64); err == nil { 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 return val
} }
} else { } else {
if val, err := strconv.Atoi(valueStr); err == nil { val, err := strconv.Atoi(valueStr)
if err == nil {
return val return val
} }
} }
return valueStr return valueStr
} }
return string(token.Value)
// Default to string
result := string(*valueBytes)
PutByteSlice(valueBytes)
return result
default:
return nil
} }
return nil
} }
func isLetter(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') } // Helper functions
func isDigit(b byte) bool { return b >= '0' && b <= '9' }
func isDigitOrMinus(b byte) bool { return isDigit(b) || b == '-' } 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 { func bytesEqual(b1, b2 []byte) bool {
if len(b1) != len(b2) { if len(b1) != len(b2) {
return false return false
} }
for i := range b1 { for i := 0; i < len(b1); i++ {
if b1[i] != b2[i] { if b1[i] != b2[i] {
return false return false
} }
@ -571,34 +569,11 @@ func bytesEqual(b1, b2 []byte) bool {
return true return true
} }
func optimizeArray(arr []any) any { func containsChar(b []byte, c byte) bool {
if len(arr) == 0 { for _, v := range b {
return arr if v == c {
} return true
// Check all strings
if allType[string](arr) {
result := make([]string, len(arr))
for i, v := range arr {
result[i] = v.(string)
} }
return result
} }
// Check all ints return false
if allType[int](arr) {
result := make([]int, len(arr))
for i, v := range arr {
result[i] = v.(int)
}
return result
}
return arr
}
func allType[T any](arr []any) bool {
for _, v := range arr {
if _, ok := v.(T); !ok {
return false
}
}
return true
} }

View File

@ -1,5 +1,11 @@
package fin package fin
/*
scanner.go
Copyright 2025 Sharkk, sharkk.net
Authors: Sky Johnson
*/
import ( import (
"bufio" "bufio"
"errors" "errors"
@ -8,6 +14,7 @@ import (
"sync" "sync"
) )
// Pre-declared errors to reduce allocations
var ( var (
ErrUnterminatedString = errors.New("unterminated string") ErrUnterminatedString = errors.New("unterminated string")
ErrUnterminatedEscape = errors.New("unterminated escape sequence") ErrUnterminatedEscape = errors.New("unterminated escape sequence")
@ -16,15 +23,17 @@ var (
ErrNameStartWithLetter = errors.New("name must start with letter") ErrNameStartWithLetter = errors.New("name must start with letter")
) )
// Scanner handles the low-level parsing of the configuration format
type Scanner struct { type Scanner struct {
reader *bufio.Reader reader *bufio.Reader
line int line int
col int col int
buffer []byte buffer []byte // Slice to the pooled buffer
bufferRef *[]byte bufferRef *[]byte // Reference to the pooled buffer
token Token token Token // Current token for unread
} }
// scannerPool helps reuse scanner objects
var scannerPool = sync.Pool{ var scannerPool = sync.Pool{
New: func() any { New: func() any {
bufferRef := GetByteSlice() bufferRef := GetByteSlice()
@ -37,35 +46,28 @@ var scannerPool = sync.Pool{
}, },
} }
// NewScanner creates a new scanner from a pool
func NewScanner(r io.Reader) *Scanner { func NewScanner(r io.Reader) *Scanner {
s := scannerPool.Get().(*Scanner) s := scannerPool.Get().(*Scanner)
s.reader = bufio.NewReaderSize(r, 1024) s.reader = bufio.NewReaderSize(r, 1024)
s.line = 1 s.line = 1
s.col = 0 s.col = 0
s.resetBuffer() s.buffer = (*s.bufferRef)[:0]
s.token = Token{Type: TokenError} s.token = Token{Type: TokenError}
return s return s
} }
// ReleaseScanner returns a scanner to the pool
func ReleaseScanner(s *Scanner) { func ReleaseScanner(s *Scanner) {
if s != nil { if s != nil {
// Clear references but keep allocated memory
s.reader = nil s.reader = nil
s.resetBuffer() s.buffer = (*s.bufferRef)[:0]
scannerPool.Put(s) scannerPool.Put(s)
} }
} }
// Helper to reset buffer consistently // ReadByte reads a single byte from the input
func (s *Scanner) resetBuffer() {
s.buffer = (*s.bufferRef)[:0]
}
// Helper for creating error tokens
func (s *Scanner) errorToken(msg string, line, col int) (Token, error) {
err := fmt.Errorf("line %d, column %d: %s", line, col, msg)
return Token{Type: TokenError, Value: []byte(msg), Line: line, Column: col}, err
}
func (s *Scanner) ReadByte() (byte, error) { func (s *Scanner) ReadByte() (byte, error) {
b, err := s.reader.ReadByte() b, err := s.reader.ReadByte()
if err == nil { if err == nil {
@ -79,6 +81,7 @@ func (s *Scanner) ReadByte() (byte, error) {
return b, err return b, err
} }
// PeekByte looks at the next byte without consuming it
func (s *Scanner) PeekByte() (byte, error) { func (s *Scanner) PeekByte() (byte, error) {
b, err := s.reader.Peek(1) b, err := s.reader.Peek(1)
if err != nil { if err != nil {
@ -87,10 +90,12 @@ func (s *Scanner) PeekByte() (byte, error) {
return b[0], nil return b[0], nil
} }
// PeekBytes looks at the next n bytes without consuming them
func (s *Scanner) PeekBytes(n int) ([]byte, error) { func (s *Scanner) PeekBytes(n int) ([]byte, error) {
return s.reader.Peek(n) return s.reader.Peek(n)
} }
// UnreadByte pushes back a byte to the reader
func (s *Scanner) UnreadByte() error { func (s *Scanner) UnreadByte() error {
err := s.reader.UnreadByte() err := s.reader.UnreadByte()
if err == nil && s.col > 0 { if err == nil && s.col > 0 {
@ -99,10 +104,12 @@ func (s *Scanner) UnreadByte() error {
return err return err
} }
// Error creates an error with line and column information
func (s *Scanner) Error(msg string) error { func (s *Scanner) Error(msg string) error {
return fmt.Errorf("line %d, column %d: %s", s.line, s.col, msg) return fmt.Errorf("line %d, column %d: %s", s.line, s.col, msg)
} }
// SkipWhitespace skips whitespace characters
func (s *Scanner) SkipWhitespace() error { func (s *Scanner) SkipWhitespace() error {
for { for {
b, err := s.PeekByte() b, err := s.PeekByte()
@ -112,9 +119,12 @@ func (s *Scanner) SkipWhitespace() error {
if err != nil { if err != nil {
return err return err
} }
// Fast check for common whitespace bytes
if b != ' ' && b != '\t' && b != '\n' && b != '\r' { if b != ' ' && b != '\t' && b != '\n' && b != '\r' {
return nil return nil
} }
_, err = s.ReadByte() _, err = s.ReadByte()
if err != nil { if err != nil {
return err return err
@ -122,22 +132,27 @@ func (s *Scanner) SkipWhitespace() error {
} }
} }
// UnreadToken stores a token to be returned by the next call to NextToken
func (s *Scanner) UnreadToken(token Token) { func (s *Scanner) UnreadToken(token Token) {
s.token = token s.token = token
} }
// NextToken scans and returns the next token
func (s *Scanner) NextToken() (Token, error) { func (s *Scanner) NextToken() (Token, error) {
if s.token.Type != TokenError { if s.token.Type != TokenError {
// We have a stored token
token := s.token token := s.token
s.token = Token{Type: TokenError} s.token = Token{Type: TokenError} // Reset
return token, nil return token, nil
} }
if err := s.SkipWhitespace(); err != nil { // Skip whitespace
err := s.SkipWhitespace()
if err != nil {
if err == io.EOF { if err == io.EOF {
return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil
} }
return s.errorToken(err.Error(), s.line, s.col) return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err
} }
b, err := s.PeekByte() b, err := s.PeekByte()
@ -145,50 +160,69 @@ func (s *Scanner) NextToken() (Token, error) {
if err == io.EOF { if err == io.EOF {
return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil
} }
return s.errorToken(err.Error(), s.line, s.col) return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err
} }
// Record start position for error reporting
startLine, startColumn := s.line, s.col startLine, startColumn := s.line, s.col
// Process based on first character
switch { switch {
case b == '{': case b == '{':
_, _ = s.ReadByte() _, _ = s.ReadByte() // consume open brace
return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil
case b == '}': case b == '}':
_, _ = s.ReadByte() _, _ = s.ReadByte() // consume close brace
return Token{Type: TokenCloseBrace, Line: startLine, Column: startColumn}, nil return Token{Type: TokenCloseBrace, Line: startLine, Column: startColumn}, nil
case b == '-': case b == '-':
// Could be a comment or a negative number
peekBytes, err := s.PeekBytes(2) peekBytes, err := s.PeekBytes(2)
if err == nil && len(peekBytes) == 2 && peekBytes[1] == '-' { if err == nil && len(peekBytes) == 2 && peekBytes[1] == '-' {
if err := s.scanComment(); err != nil { err = s.scanComment()
return s.errorToken(err.Error(), startLine, startColumn) if err != nil {
return Token{Type: TokenError, Value: []byte(err.Error())}, err
} }
return Token{Type: TokenComment, Line: startLine, Column: startColumn}, nil return Token{Type: TokenComment, Line: startLine, Column: startColumn}, nil
} }
// Check if it's a negative number
if err == nil && len(peekBytes) == 2 && isDigit(peekBytes[1]) { if err == nil && len(peekBytes) == 2 && isDigit(peekBytes[1]) {
return s.scanNumber(startLine, startColumn) return s.scanNumber(startLine, startColumn)
} }
_, _ = s.ReadByte()
return s.errorToken("unexpected '-'", startLine, startColumn) // Just a single dash
_, _ = s.ReadByte() // consume dash
return Token{Type: TokenError, Value: []byte("unexpected '-'")},
s.Error("unexpected '-'")
case b == '"': case b == '"':
return s.scanString(startLine, startColumn) return s.scanString(startLine, startColumn)
case isLetter(b): case isLetter(b):
return s.scanName(startLine, startColumn) return s.scanName(startLine, startColumn)
case isDigit(b): case isDigit(b):
return s.scanNumber(startLine, startColumn) return s.scanNumber(startLine, startColumn)
default: default:
_, _ = s.ReadByte() _, _ = s.ReadByte() // consume the unexpected character
msg := fmt.Sprintf("unexpected character: %c", b) return Token{Type: TokenError, Value: []byte(fmt.Sprintf("unexpected character: %c", b)), Line: startLine, Column: startColumn},
return s.errorToken(msg, startLine, startColumn) s.Error(fmt.Sprintf("unexpected character: %c", b))
} }
} }
// scanComment processes a comment
func (s *Scanner) scanComment() error { func (s *Scanner) scanComment() error {
_, err := s.ReadByte() // consume first dash // Consume the first dash
_, err := s.ReadByte()
if err != nil { if err != nil {
return err return err
} }
b, err := s.ReadByte() // consume second dash
// Check for second dash
b, err := s.ReadByte()
if err != nil { if err != nil {
return err return err
} }
@ -196,11 +230,12 @@ func (s *Scanner) scanComment() error {
return ErrInvalidComment return ErrInvalidComment
} }
// Check for block comment // Check for block comment [[
if b1, err := s.PeekByte(); err == nil && b1 == '[' { if b1, err := s.PeekByte(); err == nil && b1 == '[' {
_, _ = s.ReadByte() _, _ = s.ReadByte() // consume first [
if b2, err := s.PeekByte(); err == nil && b2 == '[' { if b2, err := s.PeekByte(); err == nil && b2 == '[' {
_, _ = s.ReadByte() _, _ = s.ReadByte() // consume second [
// Process block comment
for { for {
b, err := s.ReadByte() b, err := s.ReadByte()
if err != nil { if err != nil {
@ -208,7 +243,7 @@ func (s *Scanner) scanComment() error {
} }
if b == ']' { if b == ']' {
if n, err := s.PeekByte(); err == nil && n == ']' { if n, err := s.PeekByte(); err == nil && n == ']' {
_, _ = s.ReadByte() _, _ = s.ReadByte() // consume second ]
return nil return nil
} }
} }
@ -216,7 +251,7 @@ func (s *Scanner) scanComment() error {
} }
} }
// Line comment // Line comment - consume until newline or EOF
for { for {
b, err := s.ReadByte() b, err := s.ReadByte()
if err == io.EOF { if err == io.EOF {
@ -231,29 +266,38 @@ func (s *Scanner) scanComment() error {
} }
} }
// scanString scans a quoted string
func (s *Scanner) scanString(startLine, startColumn int) (Token, error) { func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
s.resetBuffer() // Reset buffer
_, err := s.ReadByte() // consume opening quote s.buffer = (*s.bufferRef)[:0]
// Consume opening quote
_, err := s.ReadByte()
if err != nil { if err != nil {
return s.errorToken(err.Error(), startLine, startColumn) return Token{Type: TokenError, Value: []byte(err.Error())}, err
} }
for { for {
b, err := s.ReadByte() b, err := s.ReadByte()
if err != nil { if err != nil {
return s.errorToken(ErrUnterminatedString.Error(), startLine, startColumn) return Token{Type: TokenError, Value: []byte(ErrUnterminatedString.Error())}, ErrUnterminatedString
} }
if b == '"' { if b == '"' {
break break
} }
// Handle escape sequences
if b == '\\' { if b == '\\' {
escaped, err := s.ReadByte() escaped, err := s.ReadByte()
if err != nil { if err != nil {
return s.errorToken(ErrUnterminatedEscape.Error(), startLine, startColumn) return Token{Type: TokenError, Value: []byte(ErrUnterminatedEscape.Error())}, ErrUnterminatedEscape
} }
switch escaped { switch escaped {
case '"', '\\': case '"':
s.buffer = append(s.buffer, escaped) s.buffer = append(s.buffer, '"')
case '\\':
s.buffer = append(s.buffer, '\\')
case 'n': case 'n':
s.buffer = append(s.buffer, '\n') s.buffer = append(s.buffer, '\n')
case 't': case 't':
@ -266,27 +310,39 @@ func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
} }
} }
return Token{Type: TokenString, Value: s.buffer, Line: startLine, Column: startColumn}, nil // Return token with buffer value - important: consumer must copy if needed
return Token{
Type: TokenString,
Value: s.buffer,
Line: startLine,
Column: startColumn,
}, nil
} }
// scanName scans an identifier
func (s *Scanner) scanName(startLine, startColumn int) (Token, error) { func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
s.resetBuffer() // Reset buffer
s.buffer = (*s.bufferRef)[:0]
// Read first character
b, err := s.ReadByte() b, err := s.ReadByte()
if err != nil { if err != nil {
return s.errorToken(err.Error(), startLine, startColumn) return Token{Type: TokenError, Value: []byte(err.Error())}, err
} }
if !isLetter(b) { if !isLetter(b) {
return s.errorToken(ErrNameStartWithLetter.Error(), startLine, startColumn) return Token{Type: TokenError, Value: []byte(ErrNameStartWithLetter.Error())}, ErrNameStartWithLetter
} }
s.buffer = append(s.buffer, b) s.buffer = append(s.buffer, b)
// Read rest of name
for { for {
b, err := s.PeekByte() b, err := s.PeekByte()
if err == io.EOF { if err == io.EOF {
break break
} }
if err != nil { if err != nil {
return s.errorToken(err.Error(), startLine, startColumn) return Token{Type: TokenError, Value: []byte(err.Error())}, err
} }
if !isLetter(b) && !isDigit(b) && b != '_' { if !isLetter(b) && !isDigit(b) && b != '_' {
break break
@ -295,22 +351,33 @@ func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
_, _ = s.ReadByte() _, _ = s.ReadByte()
} }
// Check if it's a boolean - use direct byte comparison
tokenType := TokenName tokenType := TokenName
if bytesEqual(s.buffer, []byte("true")) || bytesEqual(s.buffer, []byte("false")) { if bytesEqual(s.buffer, []byte("true")) || bytesEqual(s.buffer, []byte("false")) {
tokenType = TokenBoolean tokenType = TokenBoolean
} }
return Token{Type: tokenType, Value: s.buffer, Line: startLine, Column: startColumn}, nil return Token{
Type: tokenType,
Value: s.buffer, // Direct buffer reference - consumer must copy!
Line: startLine,
Column: startColumn,
}, nil
} }
// scanNumber scans a numeric value
func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) { func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
s.resetBuffer() // Reset buffer
s.buffer = (*s.bufferRef)[:0]
// Read first character (might be a minus sign or digit)
b, err := s.ReadByte() b, err := s.ReadByte()
if err != nil { if err != nil {
return s.errorToken(err.Error(), startLine, startColumn) return Token{Type: TokenError, Value: []byte(err.Error())}, err
} }
s.buffer = append(s.buffer, b) s.buffer = append(s.buffer, b)
// Scan the rest of the number
hasDot := false hasDot := false
for { for {
b, err := s.PeekByte() b, err := s.PeekByte()
@ -318,8 +385,9 @@ func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
if err == io.EOF { if err == io.EOF {
break break
} }
return s.errorToken(err.Error(), startLine, startColumn) return Token{Type: TokenError, Value: []byte(err.Error())}, err
} }
if b == '.' && !hasDot { if b == '.' && !hasDot {
hasDot = true hasDot = true
_, _ = s.ReadByte() _, _ = s.ReadByte()
@ -332,5 +400,10 @@ func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
} }
} }
return Token{Type: TokenNumber, Value: s.buffer, Line: startLine, Column: startColumn}, nil return Token{
Type: TokenNumber,
Value: s.buffer, // Direct buffer reference - consumer must copy!
Line: startLine,
Column: startColumn,
}, nil
} }

View File

@ -18,9 +18,9 @@ func TestBasicKeyValuePairs(t *testing.T) {
negativeFloat -2.5 negativeFloat -2.5
stringValue "hello world" stringValue "hello world"
` `
config := fin.Load(strings.NewReader(input)) config, err := fin.Load(strings.NewReader(input))
if config.HasError() { if err != nil {
t.Fatalf("unexpected error: %v", config.GetError().Error()) t.Fatalf("unexpected error: %v", err)
} }
// Verify boolean values // Verify boolean values
@ -98,9 +98,9 @@ func TestComments(t *testing.T) {
} }
` `
config := fin.Load(strings.NewReader(input)) config, err := fin.Load(strings.NewReader(input))
if config.HasError() { if err != nil {
t.Fatalf("unexpected error: %v", config.GetError().Error()) t.Fatalf("unexpected error: %v", err)
} }
// Verify that comments are properly ignored and values are parsed // Verify that comments are properly ignored and values are parsed
@ -144,9 +144,9 @@ func TestArrays(t *testing.T) {
} }
` `
config := fin.Load(strings.NewReader(input)) config, err := fin.Load(strings.NewReader(input))
if config.HasError() { if err != nil {
t.Fatalf("unexpected error: %v", config.GetError().Error()) t.Fatalf("unexpected error: %v", err)
} }
// Verify simple array // Verify simple array
@ -221,9 +221,9 @@ func TestMaps(t *testing.T) {
} }
` `
config := fin.Load(strings.NewReader(input)) config, err := fin.Load(strings.NewReader(input))
if config.HasError() { if err != nil {
t.Fatalf("unexpected error: %v", config.GetError().Error()) t.Fatalf("unexpected error: %v", err)
} }
// Verify simple map // Verify simple map

View File

@ -28,9 +28,9 @@ func TestBasicWrite(t *testing.T) {
} }
// Read back // Read back
readData := fin.Load(bytes.NewReader(buf.Bytes())) readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if readData.HasError() { if err != nil {
t.Fatalf("unexpected error loading written data: %v", readData.GetError().Error()) t.Fatalf("unexpected error loading written data: %v", err)
} }
// Verify values // Verify values
@ -72,9 +72,9 @@ func TestArrayWrite(t *testing.T) {
} }
// Read back // Read back
readData := fin.Load(bytes.NewReader(buf.Bytes())) readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if readData.HasError() { if err != nil {
t.Fatalf("unexpected error loading written data: %v", readData.GetError().Error()) t.Fatalf("unexpected error loading written data: %v", err)
} }
// Verify arrays // Verify arrays
@ -149,9 +149,9 @@ func TestMapWrite(t *testing.T) {
} }
// Read back // Read back
readData := fin.Load(bytes.NewReader(buf.Bytes())) readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if readData.HasError() { if err != nil {
t.Fatalf("unexpected error loading written data: %v", readData.GetError().Error()) t.Fatalf("unexpected error loading written data: %v", err)
} }
// Verify simple map // Verify simple map
@ -198,9 +198,9 @@ func TestSpecialCasesWrite(t *testing.T) {
} }
// Read back // Read back
readData := fin.Load(bytes.NewReader(buf.Bytes())) readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if readData.HasError() { if err != nil {
t.Fatalf("unexpected error loading written data: %v", readData.GetError().Error()) t.Fatalf("unexpected error loading written data: %v", err)
} }
// Verify combined value+object // Verify combined value+object
@ -231,9 +231,9 @@ func TestStringEscapingWrite(t *testing.T) {
} }
// Read back // Read back
readData := fin.Load(bytes.NewReader(buf.Bytes())) readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if readData.HasError() { if err != nil {
t.Fatalf("unexpected error loading written data: %v", readData.GetError().Error()) t.Fatalf("unexpected error loading written data: %v", err)
} }
// Verify escaped strings // Verify escaped strings

382
writer.go
View File

@ -7,7 +7,6 @@ package fin
*/ */
import ( import (
"fmt"
"io" "io"
"strconv" "strconv"
) )
@ -17,196 +16,289 @@ func (d *Data) Write(w io.Writer) error {
return writeMap(w, d.data, 0) return writeMap(w, d.data, 0)
} }
// Save writes data to a writer (standalone function)
func Save(w io.Writer, d *Data) error { func Save(w io.Writer, d *Data) error {
return d.Write(w) return d.Write(w)
} }
// Helper for writing indentation // writeMap writes a map at the given indent level
func writeIndent(w io.Writer, level int) error {
for range level {
if _, err := w.Write([]byte{'\t'}); err != nil {
return err
}
}
return nil
}
// Helper for writing numbers using pooled buffers
func writeNumber(w io.Writer, val any) error {
buffer := GetByteSlice()
defer PutByteSlice(buffer)
switch v := val.(type) {
case int:
*buffer = strconv.AppendInt((*buffer)[:0], int64(v), 10)
case float64:
*buffer = strconv.AppendFloat((*buffer)[:0], v, 'g', -1, 64)
default:
return fmt.Errorf("not a number type")
}
_, err := w.Write(*buffer)
return err
}
// Consolidated string writing with escaping
func writeString(w io.Writer, s string, quote bool) error {
if quote {
if _, err := io.WriteString(w, "\""); err != nil {
return err
}
}
for i := range len(s) {
c := s[i]
var escaped string
switch c {
case '"':
escaped = "\\\""
case '\\':
escaped = "\\\\"
case '\n':
escaped = "\\n"
case '\t':
escaped = "\\t"
default:
if _, err := w.Write([]byte{c}); err != nil {
return err
}
continue
}
if _, err := io.WriteString(w, escaped); err != nil {
return err
}
}
if quote {
if _, err := io.WriteString(w, "\""); err != nil {
return err
}
}
return nil
}
// Unified value writing logic
func writeValue(w io.Writer, value any, level int, addSpace, addNewline bool) error {
if addSpace {
if _, err := w.Write([]byte{' '}); err != nil {
return err
}
}
switch v := value.(type) {
case nil:
// Do nothing for nil
case string:
if err := writeString(w, v, true); err != nil {
return err
}
case int, float64:
if err := writeNumber(w, v); err != nil {
return err
}
case bool:
val := "false"
if v {
val = "true"
}
if _, err := io.WriteString(w, val); err != nil {
return err
}
case map[string]any:
if _, err := io.WriteString(w, "{\n"); err != nil {
return err
}
if err := writeMap(w, v, level+1); err != nil {
return err
}
if err := writeIndent(w, level); err != nil {
return err
}
if _, err := io.WriteString(w, "}"); err != nil {
return err
}
case []any:
if _, err := io.WriteString(w, "{\n"); err != nil {
return err
}
if err := writeArray(w, v, level+1); err != nil {
return err
}
if err := writeIndent(w, level); err != nil {
return err
}
if _, err := io.WriteString(w, "}"); err != nil {
return err
}
}
if addNewline {
if _, err := io.WriteString(w, "\n"); err != nil {
return err
}
}
return nil
}
func writeMap(w io.Writer, data map[string]any, level int) error { func writeMap(w io.Writer, data map[string]any, level int) error {
for key, value := range data { for key, value := range data {
if err := writeIndent(w, level); err != nil { // Write indentation
return err for i := 0; i < level; i++ {
if _, err := w.Write([]byte{'\t'}); err != nil {
return err
}
} }
// Write key
if _, err := io.WriteString(w, key); err != nil { if _, err := io.WriteString(w, key); err != nil {
return err return err
} }
// Handle combined value+object case // Special case: combined value+object
if m, ok := value.(map[string]any); ok && len(m) > 1 { if m, ok := value.(map[string]any); ok && len(m) > 1 {
if simpleValue, hasValue := m["value"]; hasValue { if simpleValue, hasValue := m["value"]; hasValue {
if err := writeValue(w, simpleValue, level, true, false); err != nil { // Write simple value first
if err := writeSimpleValue(w, simpleValue); err != nil {
return err return err
} }
// Write object portion
if _, err := io.WriteString(w, " {\n"); err != nil { if _, err := io.WriteString(w, " {\n"); err != nil {
return err return err
} }
for k, v := range m { for k, v := range m {
if k != "value" { if k != "value" {
if err := writeIndent(w, level+1); err != nil { for i := 0; i < level+1; i++ {
return err if _, err := w.Write([]byte{'\t'}); err != nil {
return err
}
} }
if _, err := io.WriteString(w, k); err != nil { if _, err := io.WriteString(w, k); err != nil {
return err return err
} }
if err := writeValue(w, v, level+1, true, true); err != nil {
if err := writeValueWithNewline(w, v, level+1); err != nil {
return err return err
} }
} }
} }
if err := writeIndent(w, level); err != nil {
return err for i := 0; i < level; i++ {
if _, err := w.Write([]byte{'\t'}); err != nil {
return err
}
} }
if _, err := io.WriteString(w, "}\n"); err != nil { if _, err := io.WriteString(w, "}\n"); err != nil {
return err return err
} }
continue continue
} }
} }
if err := writeValue(w, value, level, true, true); err != nil { // Regular value handling
if err := writeValueWithNewline(w, value, level); err != nil {
return err return err
} }
} }
return nil return nil
} }
func writeArray(w io.Writer, array []any, level int) error { // writeValueWithNewline writes a value and adds a newline
for _, item := range array { func writeValueWithNewline(w io.Writer, value any, level int) error {
if err := writeIndent(w, level); err != nil { if err := writeValue(w, value, level); err != nil {
return err
}
if _, err := io.WriteString(w, "\n"); err != nil {
return err
}
return nil
}
// writeValue writes a value with appropriate formatting
func writeValue(w io.Writer, value any, level int) error {
// Space after key
if _, err := w.Write([]byte{' '}); err != nil {
return err
}
switch v := value.(type) {
case nil:
return nil
case string:
// Quote strings
if _, err := io.WriteString(w, "\""); err != nil {
return err return err
} }
if err := writeValue(w, item, level, true, true); err != nil {
if err := writeEscapedString(w, v); err != nil {
return err
}
if _, err := io.WriteString(w, "\""); err != nil {
return err
}
return nil
case int:
// Use byte slice pool for better performance
buffer := GetByteSlice()
*buffer = strconv.AppendInt((*buffer)[:0], int64(v), 10)
_, err := w.Write(*buffer)
PutByteSlice(buffer)
return err
case float64:
buffer := GetByteSlice()
*buffer = strconv.AppendFloat((*buffer)[:0], v, 'g', -1, 64)
_, err := w.Write(*buffer)
PutByteSlice(buffer)
return err
case bool:
if v {
_, err := io.WriteString(w, "true")
return err
}
_, err := io.WriteString(w, "false")
return err
case map[string]any:
if _, err := io.WriteString(w, "{\n"); err != nil {
return err
}
if err := writeMap(w, v, level+1); err != nil {
return err
}
for i := 0; i < level; i++ {
if _, err := w.Write([]byte{'\t'}); err != nil {
return err
}
}
if _, err := io.WriteString(w, "}"); err != nil {
return err
}
return nil
case []any:
if _, err := io.WriteString(w, "{\n"); err != nil {
return err
}
if err := writeArray(w, v, level+1); err != nil {
return err
}
for i := 0; i < level; i++ {
if _, err := w.Write([]byte{'\t'}); err != nil {
return err
}
}
if _, err := io.WriteString(w, "}"); err != nil {
return err
}
return nil
default:
// Fall back for any other types
buffer := GetByteSlice()
*buffer = append((*buffer)[:0], []byte(strconv.FormatInt(int64(v.(int)), 10))...)
_, err := w.Write(*buffer)
PutByteSlice(buffer)
return err
}
}
// writeArray writes array elements
func writeArray(w io.Writer, array []any, level int) error {
for _, item := range array {
for i := 0; i < level; i++ {
if _, err := w.Write([]byte{'\t'}); err != nil {
return err
}
}
if err := writeValue(w, item, level); err != nil {
return err
}
if _, err := io.WriteString(w, "\n"); err != nil {
return err return err
} }
} }
return nil
}
// writeEscapedString writes a string with escape sequences
func writeEscapedString(w io.Writer, s string) error {
for i := 0; i < len(s); i++ {
c := s[i]
switch c {
case '"':
if _, err := io.WriteString(w, "\\\""); err != nil {
return err
}
case '\\':
if _, err := io.WriteString(w, "\\\\"); err != nil {
return err
}
case '\n':
if _, err := io.WriteString(w, "\\n"); err != nil {
return err
}
case '\t':
if _, err := io.WriteString(w, "\\t"); err != nil {
return err
}
default:
if _, err := w.Write([]byte{c}); err != nil {
return err
}
}
}
return nil
}
// writeSimpleValue writes a simple value without newline
func writeSimpleValue(w io.Writer, value any) error {
if _, err := w.Write([]byte{' '}); err != nil {
return err
}
switch v := value.(type) {
case string:
if _, err := io.WriteString(w, "\""); err != nil {
return err
}
if err := writeEscapedString(w, v); err != nil {
return err
}
if _, err := io.WriteString(w, "\""); err != nil {
return err
}
case int:
buffer := GetByteSlice()
*buffer = strconv.AppendInt((*buffer)[:0], int64(v), 10)
_, err := w.Write(*buffer)
PutByteSlice(buffer)
return err
case float64:
buffer := GetByteSlice()
*buffer = strconv.AppendFloat((*buffer)[:0], v, 'g', -1, 64)
_, err := w.Write(*buffer)
PutByteSlice(buffer)
return err
case bool:
if v {
_, err := io.WriteString(w, "true")
return err
}
_, err := io.WriteString(w, "false")
return err
}
return nil return nil
} }