Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
1ff96038c4 | |||
eab89c086e |
48
README.md
48
README.md
|
@ -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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package scf_test
|
package fin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package scf_test
|
package fin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
|
@ -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
|
|
@ -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
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