rewrite, add array support
This commit is contained in:
parent
1ff96038c4
commit
c67cd9e269
@ -605,8 +605,8 @@ write = 10
|
|||||||
b.Run("Direct-ArrayGet", func(b *testing.B) {
|
b.Run("Direct-ArrayGet", func(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
app := directData["app"].(map[string]any)
|
app := directData["app"].(map[string]any)
|
||||||
environments := app["environments"].([]any)
|
environments := app["environments"].([]string)
|
||||||
_ = environments[1].(string)
|
_ = environments[1]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
439
data.go
439
data.go
@ -1,32 +1,25 @@
|
|||||||
package fin
|
package fin
|
||||||
|
|
||||||
/*
|
|
||||||
data.go
|
|
||||||
Copyright 2025 Sharkk, sharkk.net
|
|
||||||
Authors: Sky Johnson
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Data holds a single hierarchical structure and handles parsing
|
|
||||||
type Data struct {
|
type Data struct {
|
||||||
data map[string]any
|
data map[string]any
|
||||||
dataRef *map[string]any // Reference to pooled map
|
dataRef *map[string]any
|
||||||
scanner *Scanner
|
scanner *Scanner
|
||||||
currentObject map[string]any
|
currentObject map[string]any
|
||||||
stack []map[string]any
|
stack []map[string]any
|
||||||
currentToken Token
|
currentToken Token
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewData creates a new empty data structure
|
|
||||||
func NewData() *Data {
|
func NewData() *Data {
|
||||||
dataRef := GetMap()
|
dataRef := GetMap()
|
||||||
data := *dataRef
|
data := *dataRef
|
||||||
|
|
||||||
d := &Data{
|
d := &Data{
|
||||||
data: data,
|
data: data,
|
||||||
dataRef: dataRef,
|
dataRef: dataRef,
|
||||||
@ -36,13 +29,11 @@ func NewData() *Data {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release frees any resources and returns them to pools
|
|
||||||
func (d *Data) Release() {
|
func (d *Data) Release() {
|
||||||
if d.scanner != nil {
|
if d.scanner != nil {
|
||||||
ReleaseScanner(d.scanner)
|
ReleaseScanner(d.scanner)
|
||||||
d.scanner = nil
|
d.scanner = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.dataRef != nil {
|
if d.dataRef != nil {
|
||||||
PutMap(d.dataRef)
|
PutMap(d.dataRef)
|
||||||
d.data = nil
|
d.data = nil
|
||||||
@ -52,28 +43,22 @@ func (d *Data) Release() {
|
|||||||
d.stack = nil
|
d.stack = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetData retrieves the entirety of the internal data map
|
func (d *Data) GetData() map[string]any { return d.data }
|
||||||
func (d *Data) GetData() map[string]any {
|
|
||||||
return d.data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves a value from the data using dot notation
|
|
||||||
func (d *Data) Get(key string) (any, error) {
|
func (d *Data) Get(key string) (any, error) {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return d.data, nil
|
return d.data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var current any = d.data
|
var current any = d.data
|
||||||
start := 0
|
start := 0
|
||||||
keyLen := len(key)
|
keyLen := len(key)
|
||||||
|
|
||||||
for i := 0; i < keyLen; i++ {
|
for i := range keyLen {
|
||||||
if key[i] == '.' || i == keyLen-1 {
|
if key[i] == '.' || i == keyLen-1 {
|
||||||
end := i
|
end := i
|
||||||
if i == keyLen-1 && key[i] != '.' {
|
if i == keyLen-1 && key[i] != '.' {
|
||||||
end = i + 1
|
end = i + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
part := key[start:end]
|
part := key[start:end]
|
||||||
|
|
||||||
switch node := current.(type) {
|
switch node := current.(type) {
|
||||||
@ -92,21 +77,36 @@ func (d *Data) Get(key string) (any, error) {
|
|||||||
return nil, fmt.Errorf("array index out of bounds: %d", index)
|
return nil, fmt.Errorf("array index out of bounds: %d", index)
|
||||||
}
|
}
|
||||||
current = node[index]
|
current = node[index]
|
||||||
|
case []string:
|
||||||
|
index, err := strconv.Atoi(part)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid array index: %s", part)
|
||||||
|
}
|
||||||
|
if index < 0 || index >= len(node) {
|
||||||
|
return nil, fmt.Errorf("array index out of bounds: %d", index)
|
||||||
|
}
|
||||||
|
current = node[index]
|
||||||
|
case []int:
|
||||||
|
index, err := strconv.Atoi(part)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid array index: %s", part)
|
||||||
|
}
|
||||||
|
if index < 0 || index >= len(node) {
|
||||||
|
return nil, fmt.Errorf("array index out of bounds: %d", index)
|
||||||
|
}
|
||||||
|
current = node[index]
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("cannot access %s in non-container value", part)
|
return nil, fmt.Errorf("cannot access %s in non-container value", part)
|
||||||
}
|
}
|
||||||
|
|
||||||
if i == keyLen-1 {
|
if i == keyLen-1 {
|
||||||
return current, nil
|
return current, nil
|
||||||
}
|
}
|
||||||
start = i + 1
|
start = i + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return current, nil
|
return current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOr retrieves a value or returns a default if not found
|
|
||||||
func (d *Data) GetOr(key string, defaultValue any) any {
|
func (d *Data) GetOr(key string, defaultValue any) any {
|
||||||
val, err := d.Get(key)
|
val, err := d.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -115,139 +115,214 @@ func (d *Data) GetOr(key string, defaultValue any) any {
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetString gets a value as string
|
func convertTo[T any](val any, key string, converter func(any) (T, bool)) (T, error) {
|
||||||
|
var zero T
|
||||||
|
result, ok := converter(val)
|
||||||
|
if ok {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
return zero, fmt.Errorf("value for key %s cannot be converted to %T", key, zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toString(val any) (string, bool) {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
return v, true
|
||||||
|
case bool:
|
||||||
|
return strconv.FormatBool(v), true
|
||||||
|
case int:
|
||||||
|
return strconv.Itoa(v), true
|
||||||
|
case float64:
|
||||||
|
return strconv.FormatFloat(v, 'f', -1, 64), true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBool(val any) (bool, bool) {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case bool:
|
||||||
|
return v, true
|
||||||
|
case string:
|
||||||
|
if result, err := strconv.ParseBool(v); err == nil {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt(val any) (int, bool) {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case int:
|
||||||
|
return v, true
|
||||||
|
case float64:
|
||||||
|
return int(v), true
|
||||||
|
case string:
|
||||||
|
if result, err := strconv.Atoi(v); err == nil {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFloat(val any) (float64, bool) {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case float64:
|
||||||
|
return v, true
|
||||||
|
case int:
|
||||||
|
return float64(v), true
|
||||||
|
case string:
|
||||||
|
if result, err := strconv.ParseFloat(v, 64); err == nil {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Data) GetString(key string) (string, error) {
|
func (d *Data) GetString(key string) (string, error) {
|
||||||
val, err := d.Get(key)
|
val, err := d.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
return convertTo(val, key, toString)
|
||||||
switch v := val.(type) {
|
|
||||||
case string:
|
|
||||||
return v, nil
|
|
||||||
case bool:
|
|
||||||
return strconv.FormatBool(v), nil
|
|
||||||
case int:
|
|
||||||
return strconv.Itoa(v), nil
|
|
||||||
case float64:
|
|
||||||
return strconv.FormatFloat(v, 'f', -1, 64), nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("value for key %s cannot be converted to string", key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBool gets a value as boolean
|
|
||||||
func (d *Data) GetBool(key string) (bool, error) {
|
func (d *Data) GetBool(key string) (bool, error) {
|
||||||
val, err := d.Get(key)
|
val, err := d.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
return convertTo(val, key, toBool)
|
||||||
switch v := val.(type) {
|
|
||||||
case bool:
|
|
||||||
return v, nil
|
|
||||||
case string:
|
|
||||||
return strconv.ParseBool(v)
|
|
||||||
default:
|
|
||||||
return false, fmt.Errorf("value for key %s cannot be converted to bool", key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt gets a value as int
|
|
||||||
func (d *Data) GetInt(key string) (int, error) {
|
func (d *Data) GetInt(key string) (int, error) {
|
||||||
val, err := d.Get(key)
|
val, err := d.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
return convertTo(val, key, toInt)
|
||||||
switch v := val.(type) {
|
|
||||||
case int:
|
|
||||||
return v, nil
|
|
||||||
case float64:
|
|
||||||
return int(v), nil
|
|
||||||
case string:
|
|
||||||
parsed, err := strconv.Atoi(v)
|
|
||||||
return parsed, err
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("value for key %s cannot be converted to int", key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFloat gets a value as float64
|
|
||||||
func (d *Data) GetFloat(key string) (float64, error) {
|
func (d *Data) GetFloat(key string) (float64, error) {
|
||||||
val, err := d.Get(key)
|
val, err := d.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
return convertTo(val, key, toFloat)
|
||||||
switch v := val.(type) {
|
|
||||||
case float64:
|
|
||||||
return v, nil
|
|
||||||
case int:
|
|
||||||
return float64(v), nil
|
|
||||||
case string:
|
|
||||||
return strconv.ParseFloat(v, 64)
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("value for key %s cannot be converted to float", key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetArray gets a value as []any
|
|
||||||
func (d *Data) GetArray(key string) ([]any, error) {
|
func (d *Data) GetArray(key string) ([]any, error) {
|
||||||
val, err := d.Get(key)
|
val, err := d.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if arr, ok := val.([]any); ok {
|
switch v := val.(type) {
|
||||||
return arr, nil
|
case []any:
|
||||||
|
return v, nil
|
||||||
|
case []string:
|
||||||
|
result := make([]any, len(v))
|
||||||
|
for i, s := range v {
|
||||||
|
result[i] = s
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
case []int:
|
||||||
|
result := make([]any, len(v))
|
||||||
|
for i, n := range v {
|
||||||
|
result[i] = n
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("value for key %s is not an array", key)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("value for key %s is not an array", key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMap gets a value as map[string]any
|
|
||||||
func (d *Data) GetMap(key string) (map[string]any, error) {
|
func (d *Data) GetMap(key string) (map[string]any, error) {
|
||||||
val, err := d.Get(key)
|
val, err := d.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if m, ok := val.(map[string]any); ok {
|
if m, ok := val.(map[string]any); ok {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("value for key %s is not a map", key)
|
return nil, fmt.Errorf("value for key %s is not a map", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error creates an error with line information from the current token
|
func getTypedArray[T any](d *Data, key string, converter func(any) (T, bool)) ([]T, error) {
|
||||||
func (d *Data) Error(msg string) error {
|
arr, err := d.GetArray(key)
|
||||||
return fmt.Errorf("line %d, column %d: %s",
|
if err != nil {
|
||||||
d.currentToken.Line, d.currentToken.Column, msg)
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make([]T, len(arr))
|
||||||
|
for i, v := range arr {
|
||||||
|
converted, ok := converter(v)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("array element at index %d cannot be converted", i)
|
||||||
|
}
|
||||||
|
result[i] = converted
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Data) GetStringArray(key string) ([]string, error) {
|
||||||
|
return getTypedArray(d, key, func(v any) (string, bool) {
|
||||||
|
s, ok := v.(string)
|
||||||
|
return s, ok
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Data) GetIntArray(key string) ([]int, error) {
|
||||||
|
return getTypedArray(d, key, func(v any) (int, bool) {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case int:
|
||||||
|
return val, true
|
||||||
|
case float64:
|
||||||
|
return int(val), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Data) GetFloatArray(key string) ([]float64, error) {
|
||||||
|
return getTypedArray(d, key, func(v any) (float64, bool) {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return val, true
|
||||||
|
case int:
|
||||||
|
return float64(val), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Data) Error(msg string) error {
|
||||||
|
return fmt.Errorf("line %d, column %d: %s", d.currentToken.Line, d.currentToken.Column, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Data) makeStringKey(tokenValue []byte) string {
|
||||||
|
keyBytes := GetByteSlice()
|
||||||
|
*keyBytes = append((*keyBytes)[:0], tokenValue...)
|
||||||
|
result := string(*keyBytes)
|
||||||
|
PutByteSlice(keyBytes)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse parses the data from a reader
|
|
||||||
func (d *Data) Parse(r io.Reader) error {
|
func (d *Data) Parse(r io.Reader) error {
|
||||||
d.scanner = NewScanner(r)
|
d.scanner = NewScanner(r)
|
||||||
d.currentObject = d.data
|
d.currentObject = d.data
|
||||||
err := d.parseContent()
|
err := d.parseContent()
|
||||||
|
|
||||||
// Clean up scanner resources even on success
|
|
||||||
if d.scanner != nil {
|
if d.scanner != nil {
|
||||||
ReleaseScanner(d.scanner)
|
ReleaseScanner(d.scanner)
|
||||||
d.scanner = nil
|
d.scanner = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// nextToken gets the next meaningful token (skipping comments)
|
|
||||||
func (d *Data) nextToken() (Token, error) {
|
func (d *Data) nextToken() (Token, error) {
|
||||||
for {
|
for {
|
||||||
token, err := d.scanner.NextToken()
|
token, err := d.scanner.NextToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return token, err
|
return token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip comment tokens
|
|
||||||
if token.Type != TokenComment {
|
if token.Type != TokenComment {
|
||||||
d.currentToken = token
|
d.currentToken = token
|
||||||
return token, nil
|
return token, nil
|
||||||
@ -255,35 +330,24 @@ func (d *Data) nextToken() (Token, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseContent is the main parsing function
|
|
||||||
func (d *Data) parseContent() error {
|
func (d *Data) parseContent() error {
|
||||||
for {
|
for {
|
||||||
token, err := d.nextToken()
|
token, err := d.nextToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for end of file
|
|
||||||
if token.Type == TokenEOF {
|
if token.Type == TokenEOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// We expect top level entries to be names
|
|
||||||
if token.Type != TokenName {
|
if token.Type != TokenName {
|
||||||
return d.Error(fmt.Sprintf("expected name at top level, got token type %v", token.Type))
|
return d.Error(fmt.Sprintf("expected name at top level, got token type %v", token.Type))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the property name - copy to create a stable key
|
name := d.makeStringKey(token.Value)
|
||||||
nameBytes := GetByteSlice()
|
|
||||||
*nameBytes = append((*nameBytes)[:0], token.Value...)
|
|
||||||
name := string(*nameBytes)
|
|
||||||
PutByteSlice(nameBytes)
|
|
||||||
|
|
||||||
// Get the next token
|
|
||||||
nextToken, err := d.nextToken()
|
nextToken, err := d.nextToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
// EOF after name - store as empty string
|
|
||||||
d.currentObject[name] = ""
|
d.currentObject[name] = ""
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -291,65 +355,47 @@ func (d *Data) parseContent() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var value any
|
var value any
|
||||||
|
|
||||||
if nextToken.Type == TokenOpenBrace {
|
if nextToken.Type == TokenOpenBrace {
|
||||||
// It's a nested object/array
|
|
||||||
value, err = d.parseObject()
|
value, err = d.parseObject()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// It's a simple value
|
|
||||||
value = d.tokenToValue(nextToken)
|
value = d.tokenToValue(nextToken)
|
||||||
|
|
||||||
// Check for potential nested object - look ahead
|
|
||||||
lookAhead, nextErr := d.nextToken()
|
lookAhead, nextErr := d.nextToken()
|
||||||
if nextErr == nil && lookAhead.Type == TokenOpenBrace {
|
if nextErr == nil && lookAhead.Type == TokenOpenBrace {
|
||||||
// It's a complex object that follows a value
|
|
||||||
nestedValue, err := d.parseObject()
|
nestedValue, err := d.parseObject()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the previous simple value in a map to add to the object
|
|
||||||
if mapValue, ok := nestedValue.(map[string]any); ok {
|
if mapValue, ok := nestedValue.(map[string]any); ok {
|
||||||
// Create a new map value with both the simple value and the map
|
|
||||||
mapRef := GetMap()
|
mapRef := GetMap()
|
||||||
newMap := *mapRef
|
newMap := *mapRef
|
||||||
for k, v := range mapValue {
|
maps.Copy(newMap, mapValue)
|
||||||
newMap[k] = v
|
newMap["value"] = value
|
||||||
}
|
|
||||||
newMap["value"] = value // Store simple value under "value" key
|
|
||||||
value = newMap
|
value = newMap
|
||||||
}
|
}
|
||||||
} else if nextErr == nil && lookAhead.Type != TokenEOF {
|
} else if nextErr == nil && lookAhead.Type != TokenEOF {
|
||||||
// Put the token back if it's not EOF
|
|
||||||
d.scanner.UnreadToken(lookAhead)
|
d.scanner.UnreadToken(lookAhead)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the value in the data
|
|
||||||
d.currentObject[name] = value
|
d.currentObject[name] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseObject parses a map or array
|
|
||||||
func (d *Data) parseObject() (any, error) {
|
func (d *Data) parseObject() (any, error) {
|
||||||
// Default to treating as an array until we see a name
|
|
||||||
isArray := true
|
isArray := true
|
||||||
arrayRef := GetArray()
|
arrayRef := GetArray()
|
||||||
arrayElements := *arrayRef
|
arrayElements := *arrayRef
|
||||||
|
|
||||||
mapRef := GetMap()
|
mapRef := GetMap()
|
||||||
objectElements := *mapRef
|
objectElements := *mapRef
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if !isArray {
|
if !isArray {
|
||||||
PutArray(arrayRef) // We didn't use the array
|
PutArray(arrayRef)
|
||||||
} else {
|
} else {
|
||||||
PutMap(mapRef) // We didn't use the map
|
PutMap(mapRef)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -362,34 +408,23 @@ func (d *Data) parseObject() (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle closing brace - finish current object/array
|
|
||||||
if token.Type == TokenCloseBrace {
|
if token.Type == TokenCloseBrace {
|
||||||
if isArray && len(arrayElements) > 0 {
|
if isArray && len(arrayElements) > 0 {
|
||||||
result := arrayElements
|
result := optimizeArray(arrayElements)
|
||||||
// Don't release the array, transfer ownership
|
*arrayRef = nil
|
||||||
*arrayRef = nil // Detach from pool reference
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
result := objectElements
|
result := objectElements
|
||||||
// Don't release the map, transfer ownership
|
*mapRef = nil
|
||||||
*mapRef = nil // Detach from pool reference
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tokens based on type
|
|
||||||
switch token.Type {
|
switch token.Type {
|
||||||
case TokenName:
|
case TokenName:
|
||||||
// Copy token value to create a stable key
|
key := d.makeStringKey(token.Value)
|
||||||
keyBytes := GetByteSlice()
|
|
||||||
*keyBytes = append((*keyBytes)[:0], token.Value...)
|
|
||||||
key := string(*keyBytes)
|
|
||||||
PutByteSlice(keyBytes)
|
|
||||||
|
|
||||||
// Look ahead to see what follows
|
|
||||||
nextToken, err := d.nextToken()
|
nextToken, err := d.nextToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
// EOF after key - store as empty value
|
|
||||||
objectElements[key] = ""
|
objectElements[key] = ""
|
||||||
isArray = false
|
isArray = false
|
||||||
return objectElements, nil
|
return objectElements, nil
|
||||||
@ -398,63 +433,49 @@ func (d *Data) parseObject() (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if nextToken.Type == TokenOpenBrace {
|
if nextToken.Type == TokenOpenBrace {
|
||||||
// Nested object
|
isArray = false
|
||||||
isArray = false // If we see a key, it's a map
|
|
||||||
nestedValue, err := d.parseObject()
|
nestedValue, err := d.parseObject()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
objectElements[key] = nestedValue
|
objectElements[key] = nestedValue
|
||||||
} else {
|
} else {
|
||||||
// Key-value pair
|
isArray = false
|
||||||
isArray = false // If we see a key, it's a map
|
|
||||||
value := d.tokenToValue(nextToken)
|
value := d.tokenToValue(nextToken)
|
||||||
objectElements[key] = value
|
objectElements[key] = value
|
||||||
|
|
||||||
// Check if there's an object following
|
|
||||||
lookAhead, nextErr := d.nextToken()
|
lookAhead, nextErr := d.nextToken()
|
||||||
if nextErr == nil && lookAhead.Type == TokenOpenBrace {
|
if nextErr == nil && lookAhead.Type == TokenOpenBrace {
|
||||||
// Nested object after value
|
|
||||||
nestedValue, err := d.parseObject()
|
nestedValue, err := d.parseObject()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to convert the value to a map
|
|
||||||
if mapValue, ok := nestedValue.(map[string]any); ok {
|
if mapValue, ok := nestedValue.(map[string]any); ok {
|
||||||
// Create a combined map
|
|
||||||
combinedMapRef := GetMap()
|
combinedMapRef := GetMap()
|
||||||
combinedMap := *combinedMapRef
|
combinedMap := *combinedMapRef
|
||||||
for k, v := range mapValue {
|
maps.Copy(combinedMap, mapValue)
|
||||||
combinedMap[k] = v
|
|
||||||
}
|
|
||||||
combinedMap["value"] = value
|
combinedMap["value"] = value
|
||||||
objectElements[key] = combinedMap
|
objectElements[key] = combinedMap
|
||||||
}
|
}
|
||||||
} else if nextErr == nil && lookAhead.Type != TokenEOF && lookAhead.Type != TokenCloseBrace {
|
} else if nextErr == nil && lookAhead.Type != TokenEOF && lookAhead.Type != TokenCloseBrace {
|
||||||
d.scanner.UnreadToken(lookAhead)
|
d.scanner.UnreadToken(lookAhead)
|
||||||
} else if nextErr == nil && lookAhead.Type == TokenCloseBrace {
|
} else if nextErr == nil && lookAhead.Type == TokenCloseBrace {
|
||||||
// We found the closing brace - unread it so it's handled by the main loop
|
|
||||||
d.scanner.UnreadToken(lookAhead)
|
d.scanner.UnreadToken(lookAhead)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case TokenString, TokenNumber, TokenBoolean:
|
case TokenString, TokenNumber, TokenBoolean:
|
||||||
// Array element
|
|
||||||
value := d.tokenToValue(token)
|
value := d.tokenToValue(token)
|
||||||
arrayElements = append(arrayElements, value)
|
arrayElements = append(arrayElements, value)
|
||||||
|
|
||||||
case TokenOpenBrace:
|
case TokenOpenBrace:
|
||||||
// Nested object/array
|
|
||||||
nestedValue, err := d.parseObject()
|
nestedValue, err := d.parseObject()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if isArray {
|
if isArray {
|
||||||
arrayElements = append(arrayElements, nestedValue)
|
arrayElements = append(arrayElements, nestedValue)
|
||||||
} else {
|
} else {
|
||||||
// If we're in an object context, this is an error
|
|
||||||
return nil, d.Error("unexpected nested object without a key")
|
return nil, d.Error("unexpected nested object without a key")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,104 +485,61 @@ func (d *Data) parseObject() (any, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load parses data from a reader
|
|
||||||
func Load(r io.Reader) (*Data, error) {
|
func Load(r io.Reader) (*Data, error) {
|
||||||
data := NewData()
|
data := NewData()
|
||||||
err := data.Parse(r)
|
err := data.Parse(r)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
data.Release()
|
data.Release()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// tokenToValue converts a token to a Go value, preserving byte slices until final conversion
|
|
||||||
func (d *Data) tokenToValue(token Token) any {
|
func (d *Data) tokenToValue(token Token) any {
|
||||||
switch token.Type {
|
switch token.Type {
|
||||||
case TokenString:
|
case TokenString:
|
||||||
// Convert to string using pooled buffer
|
return d.makeStringKey(token.Value)
|
||||||
valueBytes := GetByteSlice()
|
|
||||||
*valueBytes = append((*valueBytes)[:0], token.Value...)
|
|
||||||
result := string(*valueBytes)
|
|
||||||
PutByteSlice(valueBytes)
|
|
||||||
return result
|
|
||||||
|
|
||||||
case TokenNumber:
|
case TokenNumber:
|
||||||
// Parse number
|
|
||||||
valueStr := string(token.Value)
|
valueStr := string(token.Value)
|
||||||
if containsChar(token.Value, '.') {
|
if slices.Contains(token.Value, '.') {
|
||||||
// Float
|
|
||||||
val, _ := strconv.ParseFloat(valueStr, 64)
|
val, _ := strconv.ParseFloat(valueStr, 64)
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
// Integer
|
|
||||||
val, _ := strconv.Atoi(valueStr)
|
val, _ := strconv.Atoi(valueStr)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
case TokenBoolean:
|
case TokenBoolean:
|
||||||
return bytesEqual(token.Value, []byte("true"))
|
return bytesEqual(token.Value, []byte("true"))
|
||||||
|
|
||||||
case TokenName:
|
case TokenName:
|
||||||
// Check if name is a special value
|
if bytesEqual(token.Value, []byte("true")) || bytesEqual(token.Value, []byte("false")) {
|
||||||
valueBytes := GetByteSlice()
|
return bytesEqual(token.Value, []byte("true"))
|
||||||
*valueBytes = append((*valueBytes)[:0], token.Value...)
|
}
|
||||||
|
if len(token.Value) > 0 && isDigitOrMinus(token.Value[0]) {
|
||||||
if bytesEqual(*valueBytes, []byte("true")) {
|
valueStr := string(token.Value)
|
||||||
PutByteSlice(valueBytes)
|
if slices.Contains(token.Value, '.') {
|
||||||
return true
|
if val, err := strconv.ParseFloat(valueStr, 64); err == nil {
|
||||||
} else if bytesEqual(*valueBytes, []byte("false")) {
|
|
||||||
PutByteSlice(valueBytes)
|
|
||||||
return false
|
|
||||||
} else if isDigitOrMinus((*valueBytes)[0]) {
|
|
||||||
// Try to convert to number
|
|
||||||
valueStr := string(*valueBytes)
|
|
||||||
PutByteSlice(valueBytes)
|
|
||||||
|
|
||||||
if containsChar(token.Value, '.') {
|
|
||||||
val, err := strconv.ParseFloat(valueStr, 64)
|
|
||||||
if err == nil {
|
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val, err := strconv.Atoi(valueStr)
|
if val, err := strconv.Atoi(valueStr); err == nil {
|
||||||
if err == nil {
|
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return valueStr
|
return valueStr
|
||||||
}
|
}
|
||||||
|
return string(token.Value)
|
||||||
// Default to string
|
|
||||||
result := string(*valueBytes)
|
|
||||||
PutByteSlice(valueBytes)
|
|
||||||
return result
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
func isLetter(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') }
|
||||||
|
func isDigit(b byte) bool { return b >= '0' && b <= '9' }
|
||||||
func isLetter(b byte) bool {
|
func isDigitOrMinus(b byte) bool { return isDigit(b) || b == '-' }
|
||||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDigit(b byte) bool {
|
|
||||||
return b >= '0' && b <= '9'
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDigitOrMinus(b byte) bool {
|
|
||||||
return isDigit(b) || b == '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytesEqual(b1, b2 []byte) bool {
|
func bytesEqual(b1, b2 []byte) bool {
|
||||||
if len(b1) != len(b2) {
|
if len(b1) != len(b2) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for i := 0; i < len(b1); i++ {
|
for i := range b1 {
|
||||||
if b1[i] != b2[i] {
|
if b1[i] != b2[i] {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -569,11 +547,34 @@ func bytesEqual(b1, b2 []byte) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsChar(b []byte, c byte) bool {
|
func optimizeArray(arr []any) any {
|
||||||
for _, v := range b {
|
if len(arr) == 0 {
|
||||||
if v == c {
|
return arr
|
||||||
return true
|
}
|
||||||
|
// Check all strings
|
||||||
|
if allType[string](arr) {
|
||||||
|
result := make([]string, len(arr))
|
||||||
|
for i, v := range arr {
|
||||||
|
result[i] = v.(string)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
// Check all ints
|
||||||
|
if allType[int](arr) {
|
||||||
|
result := make([]int, len(arr))
|
||||||
|
for i, v := range arr {
|
||||||
|
result[i] = v.(int)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
func allType[T any](arr []any) bool {
|
||||||
|
for _, v := range arr {
|
||||||
|
if _, ok := v.(T); !ok {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
|
179
scanner.go
179
scanner.go
@ -1,11 +1,5 @@
|
|||||||
package fin
|
package fin
|
||||||
|
|
||||||
/*
|
|
||||||
scanner.go
|
|
||||||
Copyright 2025 Sharkk, sharkk.net
|
|
||||||
Authors: Sky Johnson
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
@ -14,7 +8,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pre-declared errors to reduce allocations
|
|
||||||
var (
|
var (
|
||||||
ErrUnterminatedString = errors.New("unterminated string")
|
ErrUnterminatedString = errors.New("unterminated string")
|
||||||
ErrUnterminatedEscape = errors.New("unterminated escape sequence")
|
ErrUnterminatedEscape = errors.New("unterminated escape sequence")
|
||||||
@ -23,17 +16,15 @@ var (
|
|||||||
ErrNameStartWithLetter = errors.New("name must start with letter")
|
ErrNameStartWithLetter = errors.New("name must start with letter")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scanner handles the low-level parsing of the configuration format
|
|
||||||
type Scanner struct {
|
type Scanner struct {
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
line int
|
line int
|
||||||
col int
|
col int
|
||||||
buffer []byte // Slice to the pooled buffer
|
buffer []byte
|
||||||
bufferRef *[]byte // Reference to the pooled buffer
|
bufferRef *[]byte
|
||||||
token Token // Current token for unread
|
token Token
|
||||||
}
|
}
|
||||||
|
|
||||||
// scannerPool helps reuse scanner objects
|
|
||||||
var scannerPool = sync.Pool{
|
var scannerPool = sync.Pool{
|
||||||
New: func() any {
|
New: func() any {
|
||||||
bufferRef := GetByteSlice()
|
bufferRef := GetByteSlice()
|
||||||
@ -46,28 +37,35 @@ var scannerPool = sync.Pool{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScanner creates a new scanner from a pool
|
|
||||||
func NewScanner(r io.Reader) *Scanner {
|
func NewScanner(r io.Reader) *Scanner {
|
||||||
s := scannerPool.Get().(*Scanner)
|
s := scannerPool.Get().(*Scanner)
|
||||||
s.reader = bufio.NewReaderSize(r, 1024)
|
s.reader = bufio.NewReaderSize(r, 1024)
|
||||||
s.line = 1
|
s.line = 1
|
||||||
s.col = 0
|
s.col = 0
|
||||||
s.buffer = (*s.bufferRef)[:0]
|
s.resetBuffer()
|
||||||
s.token = Token{Type: TokenError}
|
s.token = Token{Type: TokenError}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReleaseScanner returns a scanner to the pool
|
|
||||||
func ReleaseScanner(s *Scanner) {
|
func ReleaseScanner(s *Scanner) {
|
||||||
if s != nil {
|
if s != nil {
|
||||||
// Clear references but keep allocated memory
|
|
||||||
s.reader = nil
|
s.reader = nil
|
||||||
s.buffer = (*s.bufferRef)[:0]
|
s.resetBuffer()
|
||||||
scannerPool.Put(s)
|
scannerPool.Put(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadByte reads a single byte from the input
|
// Helper to reset buffer consistently
|
||||||
|
func (s *Scanner) resetBuffer() {
|
||||||
|
s.buffer = (*s.bufferRef)[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for creating error tokens
|
||||||
|
func (s *Scanner) errorToken(msg string, line, col int) (Token, error) {
|
||||||
|
err := fmt.Errorf("line %d, column %d: %s", line, col, msg)
|
||||||
|
return Token{Type: TokenError, Value: []byte(msg), Line: line, Column: col}, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Scanner) ReadByte() (byte, error) {
|
func (s *Scanner) ReadByte() (byte, error) {
|
||||||
b, err := s.reader.ReadByte()
|
b, err := s.reader.ReadByte()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -81,7 +79,6 @@ func (s *Scanner) ReadByte() (byte, error) {
|
|||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeekByte looks at the next byte without consuming it
|
|
||||||
func (s *Scanner) PeekByte() (byte, error) {
|
func (s *Scanner) PeekByte() (byte, error) {
|
||||||
b, err := s.reader.Peek(1)
|
b, err := s.reader.Peek(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -90,12 +87,10 @@ func (s *Scanner) PeekByte() (byte, error) {
|
|||||||
return b[0], nil
|
return b[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeekBytes looks at the next n bytes without consuming them
|
|
||||||
func (s *Scanner) PeekBytes(n int) ([]byte, error) {
|
func (s *Scanner) PeekBytes(n int) ([]byte, error) {
|
||||||
return s.reader.Peek(n)
|
return s.reader.Peek(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnreadByte pushes back a byte to the reader
|
|
||||||
func (s *Scanner) UnreadByte() error {
|
func (s *Scanner) UnreadByte() error {
|
||||||
err := s.reader.UnreadByte()
|
err := s.reader.UnreadByte()
|
||||||
if err == nil && s.col > 0 {
|
if err == nil && s.col > 0 {
|
||||||
@ -104,12 +99,10 @@ func (s *Scanner) UnreadByte() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error creates an error with line and column information
|
|
||||||
func (s *Scanner) Error(msg string) error {
|
func (s *Scanner) Error(msg string) error {
|
||||||
return fmt.Errorf("line %d, column %d: %s", s.line, s.col, msg)
|
return fmt.Errorf("line %d, column %d: %s", s.line, s.col, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SkipWhitespace skips whitespace characters
|
|
||||||
func (s *Scanner) SkipWhitespace() error {
|
func (s *Scanner) SkipWhitespace() error {
|
||||||
for {
|
for {
|
||||||
b, err := s.PeekByte()
|
b, err := s.PeekByte()
|
||||||
@ -119,12 +112,9 @@ func (s *Scanner) SkipWhitespace() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast check for common whitespace bytes
|
|
||||||
if b != ' ' && b != '\t' && b != '\n' && b != '\r' {
|
if b != ' ' && b != '\t' && b != '\n' && b != '\r' {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.ReadByte()
|
_, err = s.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -132,27 +122,22 @@ func (s *Scanner) SkipWhitespace() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnreadToken stores a token to be returned by the next call to NextToken
|
|
||||||
func (s *Scanner) UnreadToken(token Token) {
|
func (s *Scanner) UnreadToken(token Token) {
|
||||||
s.token = token
|
s.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextToken scans and returns the next token
|
|
||||||
func (s *Scanner) NextToken() (Token, error) {
|
func (s *Scanner) NextToken() (Token, error) {
|
||||||
if s.token.Type != TokenError {
|
if s.token.Type != TokenError {
|
||||||
// We have a stored token
|
|
||||||
token := s.token
|
token := s.token
|
||||||
s.token = Token{Type: TokenError} // Reset
|
s.token = Token{Type: TokenError}
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip whitespace
|
if err := s.SkipWhitespace(); err != nil {
|
||||||
err := s.SkipWhitespace()
|
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil
|
return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil
|
||||||
}
|
}
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err
|
return s.errorToken(err.Error(), s.line, s.col)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := s.PeekByte()
|
b, err := s.PeekByte()
|
||||||
@ -160,69 +145,50 @@ func (s *Scanner) NextToken() (Token, error) {
|
|||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil
|
return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil
|
||||||
}
|
}
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err
|
return s.errorToken(err.Error(), s.line, s.col)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record start position for error reporting
|
|
||||||
startLine, startColumn := s.line, s.col
|
startLine, startColumn := s.line, s.col
|
||||||
|
|
||||||
// Process based on first character
|
|
||||||
switch {
|
switch {
|
||||||
case b == '{':
|
case b == '{':
|
||||||
_, _ = s.ReadByte() // consume open brace
|
_, _ = s.ReadByte()
|
||||||
return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil
|
return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil
|
||||||
|
|
||||||
case b == '}':
|
case b == '}':
|
||||||
_, _ = s.ReadByte() // consume close brace
|
_, _ = s.ReadByte()
|
||||||
return Token{Type: TokenCloseBrace, Line: startLine, Column: startColumn}, nil
|
return Token{Type: TokenCloseBrace, Line: startLine, Column: startColumn}, nil
|
||||||
|
|
||||||
case b == '-':
|
case b == '-':
|
||||||
// Could be a comment or a negative number
|
|
||||||
peekBytes, err := s.PeekBytes(2)
|
peekBytes, err := s.PeekBytes(2)
|
||||||
if err == nil && len(peekBytes) == 2 && peekBytes[1] == '-' {
|
if err == nil && len(peekBytes) == 2 && peekBytes[1] == '-' {
|
||||||
err = s.scanComment()
|
if err := s.scanComment(); err != nil {
|
||||||
if err != nil {
|
return s.errorToken(err.Error(), startLine, startColumn)
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error())}, err
|
|
||||||
}
|
}
|
||||||
return Token{Type: TokenComment, Line: startLine, Column: startColumn}, nil
|
return Token{Type: TokenComment, Line: startLine, Column: startColumn}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a negative number
|
|
||||||
if err == nil && len(peekBytes) == 2 && isDigit(peekBytes[1]) {
|
if err == nil && len(peekBytes) == 2 && isDigit(peekBytes[1]) {
|
||||||
return s.scanNumber(startLine, startColumn)
|
return s.scanNumber(startLine, startColumn)
|
||||||
}
|
}
|
||||||
|
_, _ = s.ReadByte()
|
||||||
// Just a single dash
|
return s.errorToken("unexpected '-'", startLine, startColumn)
|
||||||
_, _ = s.ReadByte() // consume dash
|
|
||||||
return Token{Type: TokenError, Value: []byte("unexpected '-'")},
|
|
||||||
s.Error("unexpected '-'")
|
|
||||||
|
|
||||||
case b == '"':
|
case b == '"':
|
||||||
return s.scanString(startLine, startColumn)
|
return s.scanString(startLine, startColumn)
|
||||||
|
|
||||||
case isLetter(b):
|
case isLetter(b):
|
||||||
return s.scanName(startLine, startColumn)
|
return s.scanName(startLine, startColumn)
|
||||||
|
|
||||||
case isDigit(b):
|
case isDigit(b):
|
||||||
return s.scanNumber(startLine, startColumn)
|
return s.scanNumber(startLine, startColumn)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
_, _ = s.ReadByte() // consume the unexpected character
|
_, _ = s.ReadByte()
|
||||||
return Token{Type: TokenError, Value: []byte(fmt.Sprintf("unexpected character: %c", b)), Line: startLine, Column: startColumn},
|
msg := fmt.Sprintf("unexpected character: %c", b)
|
||||||
s.Error(fmt.Sprintf("unexpected character: %c", b))
|
return s.errorToken(msg, startLine, startColumn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanComment processes a comment
|
|
||||||
func (s *Scanner) scanComment() error {
|
func (s *Scanner) scanComment() error {
|
||||||
// Consume the first dash
|
_, err := s.ReadByte() // consume first dash
|
||||||
_, err := s.ReadByte()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
b, err := s.ReadByte() // consume second dash
|
||||||
// Check for second dash
|
|
||||||
b, err := s.ReadByte()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -230,12 +196,11 @@ func (s *Scanner) scanComment() error {
|
|||||||
return ErrInvalidComment
|
return ErrInvalidComment
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for block comment [[
|
// Check for block comment
|
||||||
if b1, err := s.PeekByte(); err == nil && b1 == '[' {
|
if b1, err := s.PeekByte(); err == nil && b1 == '[' {
|
||||||
_, _ = s.ReadByte() // consume first [
|
_, _ = s.ReadByte()
|
||||||
if b2, err := s.PeekByte(); err == nil && b2 == '[' {
|
if b2, err := s.PeekByte(); err == nil && b2 == '[' {
|
||||||
_, _ = s.ReadByte() // consume second [
|
_, _ = s.ReadByte()
|
||||||
// Process block comment
|
|
||||||
for {
|
for {
|
||||||
b, err := s.ReadByte()
|
b, err := s.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -243,7 +208,7 @@ func (s *Scanner) scanComment() error {
|
|||||||
}
|
}
|
||||||
if b == ']' {
|
if b == ']' {
|
||||||
if n, err := s.PeekByte(); err == nil && n == ']' {
|
if n, err := s.PeekByte(); err == nil && n == ']' {
|
||||||
_, _ = s.ReadByte() // consume second ]
|
_, _ = s.ReadByte()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,7 +216,7 @@ func (s *Scanner) scanComment() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Line comment - consume until newline or EOF
|
// Line comment
|
||||||
for {
|
for {
|
||||||
b, err := s.ReadByte()
|
b, err := s.ReadByte()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@ -266,38 +231,29 @@ func (s *Scanner) scanComment() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanString scans a quoted string
|
|
||||||
func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
|
func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
|
||||||
// Reset buffer
|
s.resetBuffer()
|
||||||
s.buffer = (*s.bufferRef)[:0]
|
_, err := s.ReadByte() // consume opening quote
|
||||||
|
|
||||||
// Consume opening quote
|
|
||||||
_, err := s.ReadByte()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error())}, err
|
return s.errorToken(err.Error(), startLine, startColumn)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
b, err := s.ReadByte()
|
b, err := s.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Token{Type: TokenError, Value: []byte(ErrUnterminatedString.Error())}, ErrUnterminatedString
|
return s.errorToken(ErrUnterminatedString.Error(), startLine, startColumn)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b == '"' {
|
if b == '"' {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle escape sequences
|
|
||||||
if b == '\\' {
|
if b == '\\' {
|
||||||
escaped, err := s.ReadByte()
|
escaped, err := s.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Token{Type: TokenError, Value: []byte(ErrUnterminatedEscape.Error())}, ErrUnterminatedEscape
|
return s.errorToken(ErrUnterminatedEscape.Error(), startLine, startColumn)
|
||||||
}
|
}
|
||||||
switch escaped {
|
switch escaped {
|
||||||
case '"':
|
case '"', '\\':
|
||||||
s.buffer = append(s.buffer, '"')
|
s.buffer = append(s.buffer, escaped)
|
||||||
case '\\':
|
|
||||||
s.buffer = append(s.buffer, '\\')
|
|
||||||
case 'n':
|
case 'n':
|
||||||
s.buffer = append(s.buffer, '\n')
|
s.buffer = append(s.buffer, '\n')
|
||||||
case 't':
|
case 't':
|
||||||
@ -310,39 +266,27 @@ func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return token with buffer value - important: consumer must copy if needed
|
return Token{Type: TokenString, Value: s.buffer, Line: startLine, Column: startColumn}, nil
|
||||||
return Token{
|
|
||||||
Type: TokenString,
|
|
||||||
Value: s.buffer,
|
|
||||||
Line: startLine,
|
|
||||||
Column: startColumn,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanName scans an identifier
|
|
||||||
func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
|
func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
|
||||||
// Reset buffer
|
s.resetBuffer()
|
||||||
s.buffer = (*s.bufferRef)[:0]
|
|
||||||
|
|
||||||
// Read first character
|
|
||||||
b, err := s.ReadByte()
|
b, err := s.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error())}, err
|
return s.errorToken(err.Error(), startLine, startColumn)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isLetter(b) {
|
if !isLetter(b) {
|
||||||
return Token{Type: TokenError, Value: []byte(ErrNameStartWithLetter.Error())}, ErrNameStartWithLetter
|
return s.errorToken(ErrNameStartWithLetter.Error(), startLine, startColumn)
|
||||||
}
|
}
|
||||||
s.buffer = append(s.buffer, b)
|
s.buffer = append(s.buffer, b)
|
||||||
|
|
||||||
// Read rest of name
|
|
||||||
for {
|
for {
|
||||||
b, err := s.PeekByte()
|
b, err := s.PeekByte()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error())}, err
|
return s.errorToken(err.Error(), startLine, startColumn)
|
||||||
}
|
}
|
||||||
if !isLetter(b) && !isDigit(b) && b != '_' {
|
if !isLetter(b) && !isDigit(b) && b != '_' {
|
||||||
break
|
break
|
||||||
@ -351,33 +295,22 @@ func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
|
|||||||
_, _ = s.ReadByte()
|
_, _ = s.ReadByte()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a boolean - use direct byte comparison
|
|
||||||
tokenType := TokenName
|
tokenType := TokenName
|
||||||
if bytesEqual(s.buffer, []byte("true")) || bytesEqual(s.buffer, []byte("false")) {
|
if bytesEqual(s.buffer, []byte("true")) || bytesEqual(s.buffer, []byte("false")) {
|
||||||
tokenType = TokenBoolean
|
tokenType = TokenBoolean
|
||||||
}
|
}
|
||||||
|
|
||||||
return Token{
|
return Token{Type: tokenType, Value: s.buffer, Line: startLine, Column: startColumn}, nil
|
||||||
Type: tokenType,
|
|
||||||
Value: s.buffer, // Direct buffer reference - consumer must copy!
|
|
||||||
Line: startLine,
|
|
||||||
Column: startColumn,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanNumber scans a numeric value
|
|
||||||
func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
|
func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
|
||||||
// Reset buffer
|
s.resetBuffer()
|
||||||
s.buffer = (*s.bufferRef)[:0]
|
|
||||||
|
|
||||||
// Read first character (might be a minus sign or digit)
|
|
||||||
b, err := s.ReadByte()
|
b, err := s.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error())}, err
|
return s.errorToken(err.Error(), startLine, startColumn)
|
||||||
}
|
}
|
||||||
s.buffer = append(s.buffer, b)
|
s.buffer = append(s.buffer, b)
|
||||||
|
|
||||||
// Scan the rest of the number
|
|
||||||
hasDot := false
|
hasDot := false
|
||||||
for {
|
for {
|
||||||
b, err := s.PeekByte()
|
b, err := s.PeekByte()
|
||||||
@ -385,9 +318,8 @@ func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
|
|||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error())}, err
|
return s.errorToken(err.Error(), startLine, startColumn)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b == '.' && !hasDot {
|
if b == '.' && !hasDot {
|
||||||
hasDot = true
|
hasDot = true
|
||||||
_, _ = s.ReadByte()
|
_, _ = s.ReadByte()
|
||||||
@ -400,10 +332,5 @@ func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Token{
|
return Token{Type: TokenNumber, Value: s.buffer, Line: startLine, Column: startColumn}, nil
|
||||||
Type: TokenNumber,
|
|
||||||
Value: s.buffer, // Direct buffer reference - consumer must copy!
|
|
||||||
Line: startLine,
|
|
||||||
Column: startColumn,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
370
writer.go
370
writer.go
@ -7,6 +7,7 @@ package fin
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
@ -16,289 +17,196 @@ func (d *Data) Write(w io.Writer) error {
|
|||||||
return writeMap(w, d.data, 0)
|
return writeMap(w, d.data, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save writes data to a writer (standalone function)
|
|
||||||
func Save(w io.Writer, d *Data) error {
|
func Save(w io.Writer, d *Data) error {
|
||||||
return d.Write(w)
|
return d.Write(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeMap writes a map at the given indent level
|
// Helper for writing indentation
|
||||||
func writeMap(w io.Writer, data map[string]any, level int) error {
|
func writeIndent(w io.Writer, level int) error {
|
||||||
for key, value := range data {
|
for range level {
|
||||||
// Write indentation
|
if _, err := w.Write([]byte{'\t'}); err != nil {
|
||||||
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 err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeValueWithNewline writes a value and adds a newline
|
// Helper for writing numbers using pooled buffers
|
||||||
func writeValueWithNewline(w io.Writer, value any, level int) error {
|
func writeNumber(w io.Writer, val any) error {
|
||||||
if err := writeValue(w, value, level); err != nil {
|
buffer := GetByteSlice()
|
||||||
return err
|
defer PutByteSlice(buffer)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
switch v := val.(type) {
|
||||||
case int:
|
case int:
|
||||||
// Use byte slice pool for better performance
|
|
||||||
buffer := GetByteSlice()
|
|
||||||
*buffer = strconv.AppendInt((*buffer)[:0], int64(v), 10)
|
*buffer = strconv.AppendInt((*buffer)[:0], int64(v), 10)
|
||||||
_, err := w.Write(*buffer)
|
|
||||||
PutByteSlice(buffer)
|
|
||||||
return err
|
|
||||||
|
|
||||||
case float64:
|
case float64:
|
||||||
buffer := GetByteSlice()
|
|
||||||
*buffer = strconv.AppendFloat((*buffer)[:0], v, 'g', -1, 64)
|
*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:
|
default:
|
||||||
// Fall back for any other types
|
return fmt.Errorf("not a number type")
|
||||||
buffer := GetByteSlice()
|
|
||||||
*buffer = append((*buffer)[:0], []byte(strconv.FormatInt(int64(v.(int)), 10))...)
|
|
||||||
_, err := w.Write(*buffer)
|
|
||||||
PutByteSlice(buffer)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err := w.Write(*buffer)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeArray writes array elements
|
// Consolidated string writing with escaping
|
||||||
func writeArray(w io.Writer, array []any, level int) error {
|
func writeString(w io.Writer, s string, quote bool) error {
|
||||||
for _, item := range array {
|
if quote {
|
||||||
for i := 0; i < level; i++ {
|
if _, err := io.WriteString(w, "\""); err != nil {
|
||||||
if _, err := w.Write([]byte{'\t'}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writeValue(w, item, level); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := io.WriteString(w, "\n"); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
for i := range len(s) {
|
||||||
}
|
|
||||||
|
|
||||||
// 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]
|
c := s[i]
|
||||||
|
var escaped string
|
||||||
switch c {
|
switch c {
|
||||||
case '"':
|
case '"':
|
||||||
if _, err := io.WriteString(w, "\\\""); err != nil {
|
escaped = "\\\""
|
||||||
return err
|
|
||||||
}
|
|
||||||
case '\\':
|
case '\\':
|
||||||
if _, err := io.WriteString(w, "\\\\"); err != nil {
|
escaped = "\\\\"
|
||||||
return err
|
|
||||||
}
|
|
||||||
case '\n':
|
case '\n':
|
||||||
if _, err := io.WriteString(w, "\\n"); err != nil {
|
escaped = "\\n"
|
||||||
return err
|
|
||||||
}
|
|
||||||
case '\t':
|
case '\t':
|
||||||
if _, err := io.WriteString(w, "\\t"); err != nil {
|
escaped = "\\t"
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
if _, err := w.Write([]byte{c}); err != nil {
|
if _, err := w.Write([]byte{c}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, escaped); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if quote {
|
||||||
|
if _, err := io.WriteString(w, "\""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeSimpleValue writes a simple value without newline
|
// Unified value writing logic
|
||||||
func writeSimpleValue(w io.Writer, value any) error {
|
func writeValue(w io.Writer, value any, level int, addSpace, addNewline bool) error {
|
||||||
if _, err := w.Write([]byte{' '}); err != nil {
|
if addSpace {
|
||||||
return err
|
if _, err := w.Write([]byte{' '}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
|
case nil:
|
||||||
|
// Do nothing for nil
|
||||||
case string:
|
case string:
|
||||||
if _, err := io.WriteString(w, "\""); err != nil {
|
if err := writeString(w, v, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case int, float64:
|
||||||
if err := writeEscapedString(w, v); err != nil {
|
if err := writeNumber(w, v); err != nil {
|
||||||
return err
|
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:
|
case bool:
|
||||||
|
val := "false"
|
||||||
if v {
|
if v {
|
||||||
_, err := io.WriteString(w, "true")
|
val = "true"
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case map[string]any:
|
||||||
|
if _, err := io.WriteString(w, "{\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeMap(w, v, level+1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeIndent(w, level); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, "}"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
if _, err := io.WriteString(w, "{\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeArray(w, v, level+1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeIndent(w, level); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, "}"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := io.WriteString(w, "false")
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if addNewline {
|
||||||
|
if _, err := io.WriteString(w, "\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeMap(w io.Writer, data map[string]any, level int) error {
|
||||||
|
for key, value := range data {
|
||||||
|
if err := writeIndent(w, level); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle combined value+object case
|
||||||
|
if m, ok := value.(map[string]any); ok && len(m) > 1 {
|
||||||
|
if simpleValue, hasValue := m["value"]; hasValue {
|
||||||
|
if err := writeValue(w, simpleValue, level, true, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, " {\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for k, v := range m {
|
||||||
|
if k != "value" {
|
||||||
|
if err := writeIndent(w, level+1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeValue(w, v, level+1, true, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := writeIndent(w, level); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, "}\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeValue(w, value, level, true, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeArray(w io.Writer, array []any, level int) error {
|
||||||
|
for _, item := range array {
|
||||||
|
if err := writeIndent(w, level); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeValue(w, item, level, true, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user