Compare commits

..

2 Commits

Author SHA1 Message Date
1ff96038c4 update readme 2025-04-19 12:33:53 -05:00
eab89c086e add writer 2025-04-19 12:21:52 -05:00
7 changed files with 717 additions and 92 deletions

View File

@ -110,6 +110,54 @@ 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

@ -1,4 +1,4 @@
package scf_test package fin_test
import ( import (
"encoding/json" "encoding/json"

View File

@ -1,4 +1,4 @@
package scf_test package fin_test
import ( import (
"encoding/json" "encoding/json"

View File

@ -1,7 +1,7 @@
package fin package fin
/* /*
config.go data.go
Copyright 2025 Sharkk, sharkk.net Copyright 2025 Sharkk, sharkk.net
Authors: Sky Johnson Authors: Sky Johnson
*/ */
@ -12,8 +12,8 @@ import (
"strconv" "strconv"
) )
// Config holds a single hierarchical structure and handles parsing // Data holds a single hierarchical structure and handles parsing
type Config struct { type Data struct {
data map[string]any data map[string]any
dataRef *map[string]any // Reference to pooled map dataRef *map[string]any // Reference to pooled map
scanner *Scanner scanner *Scanner
@ -22,48 +22,48 @@ type Config struct {
currentToken Token currentToken Token
} }
// NewConfig creates a new empty config // NewData creates a new empty data structure
func NewConfig() *Config { func NewData() *Data {
dataRef := GetMap() dataRef := GetMap()
data := *dataRef data := *dataRef
cfg := &Config{ d := &Data{
data: data, data: data,
dataRef: dataRef, dataRef: dataRef,
stack: make([]map[string]any, 0, 8), stack: make([]map[string]any, 0, 8),
} }
cfg.currentObject = cfg.data d.currentObject = d.data
return cfg return d
} }
// Release frees any resources and returns them to pools // Release frees any resources and returns them to pools
func (c *Config) Release() { func (d *Data) Release() {
if c.scanner != nil { if d.scanner != nil {
ReleaseScanner(c.scanner) ReleaseScanner(d.scanner)
c.scanner = nil d.scanner = nil
} }
if c.dataRef != nil { if d.dataRef != nil {
PutMap(c.dataRef) PutMap(d.dataRef)
c.data = nil d.data = nil
c.dataRef = nil d.dataRef = nil
} }
c.currentObject = nil d.currentObject = nil
c.stack = nil d.stack = nil
} }
// GetData retrieves the entirety of the internal data map // GetData retrieves the entirety of the internal data map
func (c *Config) GetData() map[string]any { func (d *Data) GetData() map[string]any {
return c.data return d.data
} }
// Get retrieves a value from the config using dot notation // Get retrieves a value from the data using dot notation
func (c *Config) Get(key string) (any, error) { func (d *Data) Get(key string) (any, error) {
if key == "" { if key == "" {
return c.data, nil return d.data, nil
} }
var current any = c.data var current any = d.data
start := 0 start := 0
keyLen := len(key) keyLen := len(key)
@ -107,8 +107,8 @@ func (c *Config) Get(key string) (any, error) {
} }
// GetOr retrieves a value or returns a default if not found // GetOr retrieves a value or returns a default if not found
func (c *Config) GetOr(key string, defaultValue any) any { func (d *Data) GetOr(key string, defaultValue any) any {
val, err := c.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return defaultValue return defaultValue
} }
@ -116,8 +116,8 @@ func (c *Config) GetOr(key string, defaultValue any) any {
} }
// GetString gets a value as string // GetString gets a value as string
func (c *Config) GetString(key string) (string, error) { func (d *Data) GetString(key string) (string, error) {
val, err := c.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -137,8 +137,8 @@ func (c *Config) GetString(key string) (string, error) {
} }
// GetBool gets a value as boolean // GetBool gets a value as boolean
func (c *Config) GetBool(key string) (bool, error) { func (d *Data) GetBool(key string) (bool, error) {
val, err := c.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -153,9 +153,9 @@ func (c *Config) GetBool(key string) (bool, error) {
} }
} }
// GetInt gets a value as int64 // GetInt gets a value as int
func (c *Config) GetInt(key string) (int, error) { func (d *Data) GetInt(key string) (int, error) {
val, err := c.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -174,8 +174,8 @@ func (c *Config) GetInt(key string) (int, error) {
} }
// GetFloat gets a value as float64 // GetFloat gets a value as float64
func (c *Config) GetFloat(key string) (float64, error) { func (d *Data) GetFloat(key string) (float64, error) {
val, err := c.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -193,8 +193,8 @@ func (c *Config) GetFloat(key string) (float64, error) {
} }
// GetArray gets a value as []any // GetArray gets a value as []any
func (c *Config) GetArray(key string) ([]any, error) { func (d *Data) GetArray(key string) ([]any, error) {
val, err := c.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -206,8 +206,8 @@ func (c *Config) GetArray(key string) ([]any, error) {
} }
// GetMap gets a value as map[string]any // GetMap gets a value as map[string]any
func (c *Config) GetMap(key string) (map[string]any, error) { func (d *Data) GetMap(key string) (map[string]any, error) {
val, err := c.Get(key) val, err := d.Get(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -219,46 +219,46 @@ func (c *Config) GetMap(key string) (map[string]any, error) {
} }
// Error creates an error with line information from the current token // Error creates an error with line information from the current token
func (c *Config) Error(msg string) error { func (d *Data) Error(msg string) error {
return fmt.Errorf("line %d, column %d: %s", return fmt.Errorf("line %d, column %d: %s",
c.currentToken.Line, c.currentToken.Column, msg) d.currentToken.Line, d.currentToken.Column, msg)
} }
// Parse parses the config from a reader // Parse parses the data from a reader
func (c *Config) Parse(r io.Reader) error { func (d *Data) Parse(r io.Reader) error {
c.scanner = NewScanner(r) d.scanner = NewScanner(r)
c.currentObject = c.data d.currentObject = d.data
err := c.parseContent() err := d.parseContent()
// Clean up scanner resources even on success // Clean up scanner resources even on success
if c.scanner != nil { if d.scanner != nil {
ReleaseScanner(c.scanner) ReleaseScanner(d.scanner)
c.scanner = nil d.scanner = nil
} }
return err return err
} }
// nextToken gets the next meaningful token (skipping comments) // nextToken gets the next meaningful token (skipping comments)
func (c *Config) nextToken() (Token, error) { func (d *Data) nextToken() (Token, error) {
for { for {
token, err := c.scanner.NextToken() token, err := d.scanner.NextToken()
if err != nil { if err != nil {
return token, err return token, err
} }
// Skip comment tokens // Skip comment tokens
if token.Type != TokenComment { if token.Type != TokenComment {
c.currentToken = token d.currentToken = token
return token, nil return token, nil
} }
} }
} }
// parseContent is the main parsing function // parseContent is the main parsing function
func (c *Config) parseContent() error { func (d *Data) parseContent() error {
for { for {
token, err := c.nextToken() token, err := d.nextToken()
if err != nil { if err != nil {
return err return err
} }
@ -270,7 +270,7 @@ func (c *Config) parseContent() error {
// We expect top level entries to be names // We expect top level entries to be names
if token.Type != TokenName { if token.Type != TokenName {
return c.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))
} }
// Get the property name - copy to create a stable key // Get the property name - copy to create a stable key
@ -280,11 +280,11 @@ func (c *Config) parseContent() error {
PutByteSlice(nameBytes) PutByteSlice(nameBytes)
// Get the next token // Get the next token
nextToken, err := c.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 // EOF after name - store as empty string
c.currentObject[name] = "" d.currentObject[name] = ""
break break
} }
return err return err
@ -294,19 +294,19 @@ func (c *Config) parseContent() error {
if nextToken.Type == TokenOpenBrace { if nextToken.Type == TokenOpenBrace {
// It's a nested object/array // It's a nested object/array
value, err = c.parseObject() value, err = d.parseObject()
if err != nil { if err != nil {
return err return err
} }
} else { } else {
// It's a simple value // It's a simple value
value = c.tokenToValue(nextToken) value = d.tokenToValue(nextToken)
// Check for potential nested object - look ahead // Check for potential nested object - look ahead
lookAhead, nextErr := c.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 // It's a complex object that follows a value
nestedValue, err := c.parseObject() nestedValue, err := d.parseObject()
if err != nil { if err != nil {
return err return err
} }
@ -324,19 +324,19 @@ func (c *Config) parseContent() error {
} }
} else if nextErr == nil && lookAhead.Type != TokenEOF { } else if nextErr == nil && lookAhead.Type != TokenEOF {
// Put the token back if it's not EOF // Put the token back if it's not EOF
c.scanner.UnreadToken(lookAhead) d.scanner.UnreadToken(lookAhead)
} }
} }
// Store the value in the config // Store the value in the data
c.currentObject[name] = value d.currentObject[name] = value
} }
return nil return nil
} }
// parseObject parses a map or array // parseObject parses a map or array
func (c *Config) parseObject() (any, error) { func (d *Data) parseObject() (any, error) {
// Default to treating as an array until we see a name // Default to treating as an array until we see a name
isArray := true isArray := true
arrayRef := GetArray() arrayRef := GetArray()
@ -354,7 +354,7 @@ func (c *Config) parseObject() (any, error) {
}() }()
for { for {
token, err := c.nextToken() token, err := d.nextToken()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
return nil, fmt.Errorf("unexpected EOF in object/array") return nil, fmt.Errorf("unexpected EOF in object/array")
@ -386,7 +386,7 @@ func (c *Config) parseObject() (any, error) {
PutByteSlice(keyBytes) PutByteSlice(keyBytes)
// Look ahead to see what follows // Look ahead to see what follows
nextToken, err := c.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 // EOF after key - store as empty value
@ -400,7 +400,7 @@ func (c *Config) parseObject() (any, error) {
if nextToken.Type == TokenOpenBrace { if nextToken.Type == TokenOpenBrace {
// Nested object // Nested object
isArray = false // If we see a key, it's a map isArray = false // If we see a key, it's a map
nestedValue, err := c.parseObject() nestedValue, err := d.parseObject()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -408,14 +408,14 @@ func (c *Config) parseObject() (any, error) {
} else { } else {
// Key-value pair // Key-value pair
isArray = false // If we see a key, it's a map isArray = false // If we see a key, it's a map
value := c.tokenToValue(nextToken) value := d.tokenToValue(nextToken)
objectElements[key] = value objectElements[key] = value
// Check if there's an object following // Check if there's an object following
lookAhead, nextErr := c.nextToken() lookAhead, nextErr := d.nextToken()
if nextErr == nil && lookAhead.Type == TokenOpenBrace { if nextErr == nil && lookAhead.Type == TokenOpenBrace {
// Nested object after value // Nested object after value
nestedValue, err := c.parseObject() nestedValue, err := d.parseObject()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -432,21 +432,21 @@ func (c *Config) parseObject() (any, error) {
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 {
c.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 // We found the closing brace - unread it so it's handled by the main loop
c.scanner.UnreadToken(lookAhead) d.scanner.UnreadToken(lookAhead)
} }
} }
case TokenString, TokenNumber, TokenBoolean: case TokenString, TokenNumber, TokenBoolean:
// Array element // Array element
value := c.tokenToValue(token) value := d.tokenToValue(token)
arrayElements = append(arrayElements, value) arrayElements = append(arrayElements, value)
case TokenOpenBrace: case TokenOpenBrace:
// Nested object/array // Nested object/array
nestedValue, err := c.parseObject() nestedValue, err := d.parseObject()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -455,30 +455,30 @@ func (c *Config) parseObject() (any, error) {
arrayElements = append(arrayElements, nestedValue) arrayElements = append(arrayElements, nestedValue)
} else { } else {
// If we're in an object context, this is an error // If we're in an object context, this is an error
return nil, c.Error("unexpected nested object without a key") return nil, d.Error("unexpected nested object without a key")
} }
default: default:
return nil, c.Error(fmt.Sprintf("unexpected token type: %v", token.Type)) return nil, d.Error(fmt.Sprintf("unexpected token type: %v", token.Type))
} }
} }
} }
// Load parses a config from a reader // Load parses data from a reader
func Load(r io.Reader) (*Config, error) { func Load(r io.Reader) (*Data, error) {
config := NewConfig() data := NewData()
err := config.Parse(r) err := data.Parse(r)
if err != nil { if err != nil {
config.Release() data.Release()
return nil, err return nil, err
} }
return config, nil return data, nil
} }
// tokenToValue converts a token to a Go value, preserving byte slices until final conversion // tokenToValue converts a token to a Go value, preserving byte slices until final conversion
func (c *Config) tokenToValue(token Token) any { func (d *Data) tokenToValue(token Token) any {
switch token.Type { switch token.Type {
case TokenString: case TokenString:
// Convert to string using pooled buffer // Convert to string using pooled buffer

View File

@ -1,11 +1,11 @@
package scf_test package fin_test
import ( import (
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
config "git.sharkk.net/Sharkk/Fin" fin "git.sharkk.net/Sharkk/Fin"
) )
func TestBasicKeyValuePairs(t *testing.T) { func TestBasicKeyValuePairs(t *testing.T) {
@ -18,7 +18,7 @@ func TestBasicKeyValuePairs(t *testing.T) {
negativeFloat -2.5 negativeFloat -2.5
stringValue "hello world" stringValue "hello world"
` `
config, err := config.Load(strings.NewReader(input)) config, err := fin.Load(strings.NewReader(input))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
@ -98,7 +98,7 @@ func TestComments(t *testing.T) {
} }
` `
config, err := config.Load(strings.NewReader(input)) config, err := fin.Load(strings.NewReader(input))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
@ -144,7 +144,7 @@ func TestArrays(t *testing.T) {
} }
` `
config, err := config.Load(strings.NewReader(input)) config, err := fin.Load(strings.NewReader(input))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
@ -221,7 +221,7 @@ func TestMaps(t *testing.T) {
} }
` `
config, err := config.Load(strings.NewReader(input)) config, err := fin.Load(strings.NewReader(input))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }

273
tests/write_test.go Normal file
View File

@ -0,0 +1,273 @@
package fin_test
import (
"bytes"
"reflect"
"strings"
"testing"
fin "git.sharkk.net/Sharkk/Fin"
)
func TestBasicWrite(t *testing.T) {
// Create test data
data := fin.NewData()
data.GetData()["boolTrue"] = true
data.GetData()["boolFalse"] = false
data.GetData()["integer"] = 42
data.GetData()["negativeInt"] = -10
data.GetData()["floatValue"] = 3.14
data.GetData()["negativeFloat"] = -2.5
data.GetData()["stringValue"] = "hello world"
// Write to buffer
buf := new(bytes.Buffer)
err := data.Write(buf)
if err != nil {
t.Fatalf("unexpected error writing data: %v", err)
}
// Read back
readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("unexpected error loading written data: %v", err)
}
// Verify values
testCases := []struct {
key string
expected any
getter func(string) (any, error)
}{
{"boolTrue", true, func(k string) (any, error) { return readData.GetBool(k) }},
{"boolFalse", false, func(k string) (any, error) { return readData.GetBool(k) }},
{"integer", 42, func(k string) (any, error) { return readData.GetInt(k) }},
{"negativeInt", -10, func(k string) (any, error) { return readData.GetInt(k) }},
{"floatValue", 3.14, func(k string) (any, error) { return readData.GetFloat(k) }},
{"negativeFloat", -2.5, func(k string) (any, error) { return readData.GetFloat(k) }},
{"stringValue", "hello world", func(k string) (any, error) { return readData.GetString(k) }},
}
for _, tc := range testCases {
got, err := tc.getter(tc.key)
if err != nil {
t.Errorf("error getting %s: %v", tc.key, err)
} else if got != tc.expected {
t.Errorf("expected %s=%v, got %v", tc.key, tc.expected, got)
}
}
}
func TestArrayWrite(t *testing.T) {
// Create test data with arrays
data := fin.NewData()
data.GetData()["fruits"] = []any{"apple", "banana", "cherry"}
data.GetData()["mixed"] = []any{"string", 42, true, 3.14}
// Write to buffer
buf := new(bytes.Buffer)
err := data.Write(buf)
if err != nil {
t.Fatalf("unexpected error writing data: %v", err)
}
// Read back
readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("unexpected error loading written data: %v", err)
}
// Verify arrays
fruits, err := readData.GetArray("fruits")
if err != nil {
t.Fatalf("failed to get fruits array: %v", err)
}
expectedFruits := []any{"apple", "banana", "cherry"}
if !reflect.DeepEqual(fruits, expectedFruits) {
t.Errorf("expected fruits=%v, got %v", expectedFruits, fruits)
}
// Verify mixed array
mixed, err := readData.GetArray("mixed")
if err != nil {
t.Fatalf("failed to get mixed array: %v", err)
}
if len(mixed) != 4 {
t.Errorf("expected 4 items in mixed array, got %d", len(mixed))
}
// Check types in mixed array
stringVal, err := readData.GetString("mixed.0")
if err != nil || stringVal != "string" {
t.Errorf("expected mixed.0=\"string\", got %v, err: %v", stringVal, err)
}
intVal, err := readData.GetInt("mixed.1")
if err != nil || intVal != 42 {
t.Errorf("expected mixed.1=42, got %v, err: %v", intVal, err)
}
boolVal, err := readData.GetBool("mixed.2")
if err != nil || boolVal != true {
t.Errorf("expected mixed.2=true, got %v, err: %v", boolVal, err)
}
floatVal, err := readData.GetFloat("mixed.3")
if err != nil || floatVal != 3.14 {
t.Errorf("expected mixed.3=3.14, got %v, err: %v", floatVal, err)
}
}
func TestMapWrite(t *testing.T) {
// Create nested map structure
data := fin.NewData()
serverMap := map[string]any{
"host": "localhost",
"port": 8080,
}
data.GetData()["server"] = serverMap
loggingMap := map[string]any{
"level": "info",
"file": "app.log",
}
settingsMap := map[string]any{
"theme": "dark",
"notifications": true,
"logging": loggingMap,
}
appMap := map[string]any{
"name": "MyApp",
"version": "1.0.0",
"settings": settingsMap,
}
data.GetData()["application"] = appMap
// Write to buffer
buf := new(bytes.Buffer)
err := data.Write(buf)
if err != nil {
t.Fatalf("unexpected error writing data: %v", err)
}
// Read back
readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("unexpected error loading written data: %v", err)
}
// Verify simple map
host, err := readData.GetString("server.host")
if err != nil || host != "localhost" {
t.Errorf("expected server.host=\"localhost\", got %v, err: %v", host, err)
}
port, err := readData.GetInt("server.port")
if err != nil || port != 8080 {
t.Errorf("expected server.port=8080, got %v, err: %v", port, err)
}
// Verify deeply nested maps
appName, err := readData.GetString("application.name")
if err != nil || appName != "MyApp" {
t.Errorf("expected application.name=\"MyApp\", got %v, err: %v", appName, err)
}
theme, err := readData.GetString("application.settings.theme")
if err != nil || theme != "dark" {
t.Errorf("expected application.settings.theme=\"dark\", got %v, err: %v", theme, err)
}
logLevel, err := readData.GetString("application.settings.logging.level")
if err != nil || logLevel != "info" {
t.Errorf("expected application.settings.logging.level=\"info\", got %v, err: %v", logLevel, err)
}
}
func TestSpecialCasesWrite(t *testing.T) {
// Test combined value+object case
data := fin.NewData()
combinedMap := map[string]any{
"value": 8080,
"protocol": "http",
"secure": false,
}
data.GetData()["port"] = combinedMap
// Write to buffer
buf := new(bytes.Buffer)
err := data.Write(buf)
if err != nil {
t.Fatalf("unexpected error writing data: %v", err)
}
// Read back
readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("unexpected error loading written data: %v", err)
}
// Verify combined value+object
portVal, err := readData.GetInt("port.value")
if err != nil || portVal != 8080 {
t.Errorf("expected port.value=8080, got %v, err: %v", portVal, err)
}
protocol, err := readData.GetString("port.protocol")
if err != nil || protocol != "http" {
t.Errorf("expected port.protocol=\"http\", got %v, err: %v", protocol, err)
}
}
func TestStringEscapingWrite(t *testing.T) {
data := fin.NewData()
// Add strings with special characters
data.GetData()["quoted"] = "Text with \"quotes\""
data.GetData()["backslash"] = "Path with \\backslash"
data.GetData()["newlines"] = "Text with\nnew lines"
data.GetData()["tabs"] = "Text with\ttabs"
// Write to buffer
buf := new(bytes.Buffer)
err := data.Write(buf)
if err != nil {
t.Fatalf("unexpected error writing data: %v", err)
}
// Read back
readData, err := fin.Load(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("unexpected error loading written data: %v", err)
}
// Verify escaped strings
quoted, err := readData.GetString("quoted")
if err != nil || quoted != "Text with \"quotes\"" {
t.Errorf("expected correct handling of quotes, got %v, err: %v", quoted, err)
}
backslash, err := readData.GetString("backslash")
if err != nil || backslash != "Path with \\backslash" {
t.Errorf("expected correct handling of backslashes, got %v, err: %v", backslash, err)
}
newlines, err := readData.GetString("newlines")
if err != nil || newlines != "Text with\nnew lines" {
t.Errorf("expected correct handling of newlines, got %v, err: %v", newlines, err)
}
tabs, err := readData.GetString("tabs")
if err != nil || tabs != "Text with\ttabs" {
t.Errorf("expected correct handling of tabs, got %v, err: %v", tabs, err)
}
}
func TestSaveFunction(t *testing.T) {
// Test the standalone Save function
data := fin.NewData()
data.GetData()["key"] = "value"
buf := new(bytes.Buffer)
err := fin.Save(buf, data)
if err != nil {
t.Fatalf("unexpected error using Save function: %v", err)
}
// Verify content was written
if !strings.Contains(buf.String(), "key \"value\"") {
t.Errorf("expected Save function to write data, got: %s", buf.String())
}
}

304
writer.go Normal file
View File

@ -0,0 +1,304 @@
package fin
/*
writer.go
Copyright 2025 Sharkk, sharkk.net
Authors: Sky Johnson
*/
import (
"io"
"strconv"
)
// Write serializes the data to the provided writer
func (d *Data) Write(w io.Writer) error {
return writeMap(w, d.data, 0)
}
// Save writes data to a writer (standalone function)
func Save(w io.Writer, d *Data) error {
return d.Write(w)
}
// writeMap writes a map at the given indent level
func writeMap(w io.Writer, data map[string]any, level int) error {
for key, value := range data {
// Write indentation
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 {
return err
}
// Special case: combined value+object
if m, ok := value.(map[string]any); ok && len(m) > 1 {
if simpleValue, hasValue := m["value"]; hasValue {
// Write simple value first
if err := writeSimpleValue(w, simpleValue); err != nil {
return err
}
// Write object portion
if _, err := io.WriteString(w, " {\n"); err != nil {
return err
}
for k, v := range m {
if k != "value" {
for i := 0; i < level+1; i++ {
if _, err := w.Write([]byte{'\t'}); err != nil {
return err
}
}
if _, err := io.WriteString(w, k); err != nil {
return err
}
if err := writeValueWithNewline(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, "}\n"); err != nil {
return err
}
continue
}
}
// Regular value handling
if err := writeValueWithNewline(w, value, level); err != nil {
return err
}
}
return nil
}
// writeValueWithNewline writes a value and adds a newline
func writeValueWithNewline(w io.Writer, value any, level int) error {
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
}
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 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
}