diff --git a/bench/get_test.go b/bench/get_test.go index 4b8dc7e..753953f 100644 --- a/bench/get_test.go +++ b/bench/get_test.go @@ -1,4 +1,4 @@ -package scf_test +package fin_test import ( "encoding/json" diff --git a/bench/parse_test.go b/bench/parse_test.go index 4c4971a..f1a56b1 100644 --- a/bench/parse_test.go +++ b/bench/parse_test.go @@ -1,4 +1,4 @@ -package scf_test +package fin_test import ( "encoding/json" diff --git a/config.go b/data.go similarity index 77% rename from config.go rename to data.go index 49a2059..33c15c5 100644 --- a/config.go +++ b/data.go @@ -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 diff --git a/tests/config_test.go b/tests/read_test.go similarity index 96% rename from tests/config_test.go rename to tests/read_test.go index 948e087..f803375 100644 --- a/tests/config_test.go +++ b/tests/read_test.go @@ -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) } diff --git a/tests/write_test.go b/tests/write_test.go new file mode 100644 index 0000000..26b7aad --- /dev/null +++ b/tests/write_test.go @@ -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()) + } +} diff --git a/writer.go b/writer.go new file mode 100644 index 0000000..730c215 --- /dev/null +++ b/writer.go @@ -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 +}