add writer
This commit is contained in:
parent
1190af7222
commit
eab89c086e
|
@ -1,4 +1,4 @@
|
|||
package scf_test
|
||||
package fin_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package scf_test
|
||||
package fin_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package fin
|
||||
|
||||
/*
|
||||
config.go
|
||||
data.go
|
||||
Copyright 2025 Sharkk, sharkk.net
|
||||
Authors: Sky Johnson
|
||||
*/
|
||||
|
@ -12,8 +12,8 @@ import (
|
|||
"strconv"
|
||||
)
|
||||
|
||||
// Config holds a single hierarchical structure and handles parsing
|
||||
type Config struct {
|
||||
// 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
|
||||
|
@ -22,48 +22,48 @@ type Config struct {
|
|||
currentToken Token
|
||||
}
|
||||
|
||||
// NewConfig creates a new empty config
|
||||
func NewConfig() *Config {
|
||||
// NewData creates a new empty data structure
|
||||
func NewData() *Data {
|
||||
dataRef := GetMap()
|
||||
data := *dataRef
|
||||
|
||||
cfg := &Config{
|
||||
d := &Data{
|
||||
data: data,
|
||||
dataRef: dataRef,
|
||||
stack: make([]map[string]any, 0, 8),
|
||||
}
|
||||
cfg.currentObject = cfg.data
|
||||
return cfg
|
||||
d.currentObject = d.data
|
||||
return d
|
||||
}
|
||||
|
||||
// Release frees any resources and returns them to pools
|
||||
func (c *Config) Release() {
|
||||
if c.scanner != nil {
|
||||
ReleaseScanner(c.scanner)
|
||||
c.scanner = nil
|
||||
func (d *Data) Release() {
|
||||
if d.scanner != nil {
|
||||
ReleaseScanner(d.scanner)
|
||||
d.scanner = nil
|
||||
}
|
||||
|
||||
if c.dataRef != nil {
|
||||
PutMap(c.dataRef)
|
||||
c.data = nil
|
||||
c.dataRef = nil
|
||||
if d.dataRef != nil {
|
||||
PutMap(d.dataRef)
|
||||
d.data = nil
|
||||
d.dataRef = nil
|
||||
}
|
||||
c.currentObject = nil
|
||||
c.stack = nil
|
||||
d.currentObject = nil
|
||||
d.stack = nil
|
||||
}
|
||||
|
||||
// GetData retrieves the entirety of the internal data map
|
||||
func (c *Config) GetData() map[string]any {
|
||||
return c.data
|
||||
func (d *Data) GetData() map[string]any {
|
||||
return d.data
|
||||
}
|
||||
|
||||
// Get retrieves a value from the config using dot notation
|
||||
func (c *Config) Get(key string) (any, error) {
|
||||
// Get retrieves a value from the data using dot notation
|
||||
func (d *Data) Get(key string) (any, error) {
|
||||
if key == "" {
|
||||
return c.data, nil
|
||||
return d.data, nil
|
||||
}
|
||||
|
||||
var current any = c.data
|
||||
var current any = d.data
|
||||
start := 0
|
||||
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
|
||||
func (c *Config) GetOr(key string, defaultValue any) any {
|
||||
val, err := c.Get(key)
|
||||
func (d *Data) GetOr(key string, defaultValue any) any {
|
||||
val, err := d.Get(key)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
@ -116,8 +116,8 @@ func (c *Config) GetOr(key string, defaultValue any) any {
|
|||
}
|
||||
|
||||
// GetString gets a value as string
|
||||
func (c *Config) GetString(key string) (string, error) {
|
||||
val, err := c.Get(key)
|
||||
func (d *Data) GetString(key string) (string, error) {
|
||||
val, err := d.Get(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -137,8 +137,8 @@ func (c *Config) GetString(key string) (string, error) {
|
|||
}
|
||||
|
||||
// GetBool gets a value as boolean
|
||||
func (c *Config) GetBool(key string) (bool, error) {
|
||||
val, err := c.Get(key)
|
||||
func (d *Data) GetBool(key string) (bool, error) {
|
||||
val, err := d.Get(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -153,9 +153,9 @@ func (c *Config) GetBool(key string) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetInt gets a value as int64
|
||||
func (c *Config) GetInt(key string) (int, error) {
|
||||
val, err := c.Get(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
|
||||
}
|
||||
|
@ -174,8 +174,8 @@ func (c *Config) GetInt(key string) (int, error) {
|
|||
}
|
||||
|
||||
// GetFloat gets a value as float64
|
||||
func (c *Config) GetFloat(key string) (float64, error) {
|
||||
val, err := c.Get(key)
|
||||
func (d *Data) GetFloat(key string) (float64, error) {
|
||||
val, err := d.Get(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -193,8 +193,8 @@ func (c *Config) GetFloat(key string) (float64, error) {
|
|||
}
|
||||
|
||||
// GetArray gets a value as []any
|
||||
func (c *Config) GetArray(key string) ([]any, error) {
|
||||
val, err := c.Get(key)
|
||||
func (d *Data) GetArray(key string) ([]any, error) {
|
||||
val, err := d.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -206,8 +206,8 @@ func (c *Config) GetArray(key string) ([]any, error) {
|
|||
}
|
||||
|
||||
// GetMap gets a value as map[string]any
|
||||
func (c *Config) GetMap(key string) (map[string]any, error) {
|
||||
val, err := c.Get(key)
|
||||
func (d *Data) GetMap(key string) (map[string]any, error) {
|
||||
val, err := d.Get(key)
|
||||
if err != nil {
|
||||
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
|
||||
func (c *Config) Error(msg string) error {
|
||||
func (d *Data) Error(msg string) error {
|
||||
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
|
||||
func (c *Config) Parse(r io.Reader) error {
|
||||
c.scanner = NewScanner(r)
|
||||
c.currentObject = c.data
|
||||
err := c.parseContent()
|
||||
// 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 c.scanner != nil {
|
||||
ReleaseScanner(c.scanner)
|
||||
c.scanner = nil
|
||||
if d.scanner != nil {
|
||||
ReleaseScanner(d.scanner)
|
||||
d.scanner = nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// nextToken gets the next meaningful token (skipping comments)
|
||||
func (c *Config) nextToken() (Token, error) {
|
||||
func (d *Data) nextToken() (Token, error) {
|
||||
for {
|
||||
token, err := c.scanner.NextToken()
|
||||
token, err := d.scanner.NextToken()
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
// Skip comment tokens
|
||||
if token.Type != TokenComment {
|
||||
c.currentToken = token
|
||||
d.currentToken = token
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseContent is the main parsing function
|
||||
func (c *Config) parseContent() error {
|
||||
func (d *Data) parseContent() error {
|
||||
for {
|
||||
token, err := c.nextToken()
|
||||
token, err := d.nextToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -270,7 +270,7 @@ func (c *Config) parseContent() error {
|
|||
|
||||
// We expect top level entries to be names
|
||||
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
|
||||
|
@ -280,11 +280,11 @@ func (c *Config) parseContent() error {
|
|||
PutByteSlice(nameBytes)
|
||||
|
||||
// Get the next token
|
||||
nextToken, err := c.nextToken()
|
||||
nextToken, err := d.nextToken()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// EOF after name - store as empty string
|
||||
c.currentObject[name] = ""
|
||||
d.currentObject[name] = ""
|
||||
break
|
||||
}
|
||||
return err
|
||||
|
@ -294,19 +294,19 @@ func (c *Config) parseContent() error {
|
|||
|
||||
if nextToken.Type == TokenOpenBrace {
|
||||
// It's a nested object/array
|
||||
value, err = c.parseObject()
|
||||
value, err = d.parseObject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// It's a simple value
|
||||
value = c.tokenToValue(nextToken)
|
||||
value = d.tokenToValue(nextToken)
|
||||
|
||||
// Check for potential nested object - look ahead
|
||||
lookAhead, nextErr := c.nextToken()
|
||||
lookAhead, nextErr := d.nextToken()
|
||||
if nextErr == nil && lookAhead.Type == TokenOpenBrace {
|
||||
// It's a complex object that follows a value
|
||||
nestedValue, err := c.parseObject()
|
||||
nestedValue, err := d.parseObject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -324,19 +324,19 @@ func (c *Config) parseContent() error {
|
|||
}
|
||||
} else if nextErr == nil && lookAhead.Type != TokenEOF {
|
||||
// Put the token back if it's not EOF
|
||||
c.scanner.UnreadToken(lookAhead)
|
||||
d.scanner.UnreadToken(lookAhead)
|
||||
}
|
||||
}
|
||||
|
||||
// Store the value in the config
|
||||
c.currentObject[name] = value
|
||||
// Store the value in the data
|
||||
d.currentObject[name] = value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
isArray := true
|
||||
arrayRef := GetArray()
|
||||
|
@ -354,7 +354,7 @@ func (c *Config) parseObject() (any, error) {
|
|||
}()
|
||||
|
||||
for {
|
||||
token, err := c.nextToken()
|
||||
token, err := d.nextToken()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, fmt.Errorf("unexpected EOF in object/array")
|
||||
|
@ -386,7 +386,7 @@ func (c *Config) parseObject() (any, error) {
|
|||
PutByteSlice(keyBytes)
|
||||
|
||||
// Look ahead to see what follows
|
||||
nextToken, err := c.nextToken()
|
||||
nextToken, err := d.nextToken()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// EOF after key - store as empty value
|
||||
|
@ -400,7 +400,7 @@ func (c *Config) parseObject() (any, error) {
|
|||
if nextToken.Type == TokenOpenBrace {
|
||||
// Nested object
|
||||
isArray = false // If we see a key, it's a map
|
||||
nestedValue, err := c.parseObject()
|
||||
nestedValue, err := d.parseObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -408,14 +408,14 @@ func (c *Config) parseObject() (any, error) {
|
|||
} else {
|
||||
// Key-value pair
|
||||
isArray = false // If we see a key, it's a map
|
||||
value := c.tokenToValue(nextToken)
|
||||
value := d.tokenToValue(nextToken)
|
||||
objectElements[key] = value
|
||||
|
||||
// Check if there's an object following
|
||||
lookAhead, nextErr := c.nextToken()
|
||||
lookAhead, nextErr := d.nextToken()
|
||||
if nextErr == nil && lookAhead.Type == TokenOpenBrace {
|
||||
// Nested object after value
|
||||
nestedValue, err := c.parseObject()
|
||||
nestedValue, err := d.parseObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -432,21 +432,21 @@ func (c *Config) parseObject() (any, error) {
|
|||
objectElements[key] = combinedMap
|
||||
}
|
||||
} 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 {
|
||||
// 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:
|
||||
// Array element
|
||||
value := c.tokenToValue(token)
|
||||
value := d.tokenToValue(token)
|
||||
arrayElements = append(arrayElements, value)
|
||||
|
||||
case TokenOpenBrace:
|
||||
// Nested object/array
|
||||
nestedValue, err := c.parseObject()
|
||||
nestedValue, err := d.parseObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -455,30 +455,30 @@ func (c *Config) parseObject() (any, error) {
|
|||
arrayElements = append(arrayElements, nestedValue)
|
||||
} else {
|
||||
// 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:
|
||||
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
|
||||
func Load(r io.Reader) (*Config, error) {
|
||||
config := NewConfig()
|
||||
err := config.Parse(r)
|
||||
// Load parses data from a reader
|
||||
func Load(r io.Reader) (*Data, error) {
|
||||
data := NewData()
|
||||
err := data.Parse(r)
|
||||
|
||||
if err != nil {
|
||||
config.Release()
|
||||
data.Release()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
case TokenString:
|
||||
// Convert to string using pooled buffer
|
|
@ -1,11 +1,11 @@
|
|||
package scf_test
|
||||
package fin_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
config "git.sharkk.net/Sharkk/Fin"
|
||||
fin "git.sharkk.net/Sharkk/Fin"
|
||||
)
|
||||
|
||||
func TestBasicKeyValuePairs(t *testing.T) {
|
||||
|
@ -18,7 +18,7 @@ func TestBasicKeyValuePairs(t *testing.T) {
|
|||
negativeFloat -2.5
|
||||
stringValue "hello world"
|
||||
`
|
||||
config, err := config.Load(strings.NewReader(input))
|
||||
config, err := fin.Load(strings.NewReader(input))
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
273
tests/write_test.go
Normal file
273
tests/write_test.go
Normal 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
304
writer.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user