From 57d38fd82e3b5e448621d96781cb694098d57173 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 4 Mar 2025 22:33:00 -0600 Subject: [PATCH] ref 12 + readme --- README.md | 80 ++++++++++++++++++++--------------------- bench/get_test.go | 92 ++++++++++++++++++++++++++++++----------------- config.go | 46 ++++++++++++------------ 3 files changed, 120 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 35fdde0..44de0c1 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,19 @@ -# Go Config Parser +# Sharkk Config File -A lightweight, intuitive configuration parser for Go applications with a clean, readable syntax.` +SCF, pronounced scuff! -## Features +A very light, very intuitive config file format! Few symbols, few rules, quite flexible. Has support for comments, arrays, maps, and type guarantees. -- Simple, human-readable configuration format -- Strong typing with automatic type conversion -- Nested structures with dot notation access -- Support for arrays and maps -- Inline and block comments -- Fast parsing with no dependencies -- Full test coverage +Mercilessly benchmarked, fully tested. SCF competes toe-to-toe with Go's native JSON library, and handily outperforms the reference TOML and YAML implementations. -## Configuration Format +## Format -This parser uses a clean, minimal syntax that's easy to read and write: +SCF has very very simple syntax. ``` +-- Lua style comments! host "localhost" -port 8080 +port 8080 -- no = for assignment! debug true allowed_ips { @@ -27,6 +22,11 @@ allowed_ips { "10.0.0.1" } +--[[ + Feel free to space out your explanations! + + All is well and good. +]] database { host "db.example.com" port 5432 @@ -35,11 +35,6 @@ database { password "secure123" } } - --- This is a line comment ---[[ This is a - block comment spanning - multiple lines ]] ``` ## Installation @@ -63,25 +58,29 @@ import ( ) func main() { - // Load configuration from file file, err := os.Open("config.conf") if err != nil { panic(err) } defer file.Close() + // Pass a string to be loaded by the parser! cfg, err := config.Load(file) if err != nil { panic(err) } - // Access values with type conversion + // Access values with type conversion. host, err := cfg.GetString("database.host") port, err := cfg.GetInt("database.port") debug, err := cfg.GetBool("debug") // Use default values for missing keys - timeout := cfg.GetOr("timeout", 30).(int64) + timeout := cfg.GetOr("timeout", 30).(int) + + // Get the entire map to traverse directly (cuts traversal time by 50%!) + entireMap := cfg.GetData() + other, err := entireMap["database"]["host"].(string) } ``` @@ -90,17 +89,14 @@ func main() { The parser automatically converts values to appropriate types: ```go -// These will return properly typed values -boolValue, err := cfg.GetBool("feature.enabled") -intValue, err := cfg.GetInt("server.port") -floatValue, err := cfg.GetFloat("threshold") +boolValue, err := cfg.GetBool("feature.enabled") +intValue, err := cfg.GetInt("server.port") +floatValue, err := cfg.GetFloat("threshold") stringValue, err := cfg.GetString("app.name") +arrayValue, err := cfg.GetArray("allowed_ips") +mapValue, err := cfg.GetMap("database") -// For complex types -arrayValue, err := cfg.GetArray("allowed_ips") -mapValue, err := cfg.GetMap("database") - -// Generic getter (returns interface{}) +// Generic getter (returns any) value, err := cfg.Get("some.key") ``` @@ -123,20 +119,20 @@ This parser provides competitive performance compared to popular formats like JS | Benchmark | Operations | Time (ns/op) | Memory (B/op) | Allocations (allocs/op) | |-----------|----------:|-------------:|--------------:|------------------------:| | **Small Config Files** | -| Config | 718,893 | 1,611 | 5,256 | 25 | -| JSON | 1,000,000 | 1,170 | 1,384 | 23 | -| YAML | 213,438 | 5,668 | 8,888 | 82 | -| TOML | 273,586 | 4,505 | 4,520 | 67 | +| Config | 1,000,000 | 1,052 | 1,743 | 15 | +| JSON | 1,000,000 | 1,112 | 1,384 | 23 | +| YAML | 215,121 | 5,600 | 8,888 | 82 | +| TOML | 286,334 | 4,483 | 4,520 | 67 | | **Medium Config Files** | -| Config | 138,517 | 8,777 | 11,247 | 114 | -| JSON | 241,069 | 4,996 | 5,344 | 89 | -| YAML | 47,695 | 24,183 | 21,577 | 347 | -| TOML | 66,411 | 17,709 | 16,349 | 208 | +| Config | 211,863 | 5,696 | 4,056 | 74 | +| JSON | 261,925 | 4,602 | 5,344 | 89 | +| YAML | 50,010 | 23,965 | 21,577 | 347 | +| TOML | 68,420 | 17,639 | 16,348 | 208 | | **Large Config Files** | -| Config | 33,177 | 35,591 | 31,791 | 477 | -| JSON | 66,384 | 18,066 | 18,138 | 297 | -| YAML | 12,482 | 95,248 | 65,574 | 1,208 | -| TOML | 17,594 | 67,928 | 66,038 | 669 | +| Config | 55,338 | 21,556 | 12,208 | 207 | +| JSON | 70,219 | 17,202 | 18,140 | 297 | +| YAML | 12,536 | 95,945 | 65,568 | 1,208 | +| TOML | 14,732 | 74,198 | 66,050 | 669 | *Benchmarked on AMD Ryzen 9 7950X 16-Core Processor* diff --git a/bench/get_test.go b/bench/get_test.go index c16d4be..2c73058 100644 --- a/bench/get_test.go +++ b/bench/get_test.go @@ -47,7 +47,7 @@ func BenchmarkRetrieveSimpleValues(b *testing.B) { // Generic retrieval timeout, err := cfg.Get("timeout") - if err != nil || timeout.(int64) != 30 { + if err != nil || timeout.(int) != 30 { b.Fatalf("Failed to retrieve timeout: %v", err) } } @@ -223,7 +223,7 @@ func BenchmarkRetrieveSimpleValuesJSON(b *testing.B) { }` // Parse once before benchmarking retrieval - var result map[string]interface{} + var result map[string]any err := json.Unmarshal([]byte(configData), &result) if err != nil { b.Fatalf("Failed to parse JSON: %v", err) @@ -278,7 +278,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) { }` // Parse once before benchmarking retrieval - var result map[string]interface{} + var result map[string]any err := json.Unmarshal([]byte(configData), &result) if err != nil { b.Fatalf("Failed to parse JSON: %v", err) @@ -287,7 +287,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { // First level nesting - app, ok := result["app"].(map[string]interface{}) + app, ok := result["app"].(map[string]any) if !ok { b.Fatalf("Failed to retrieve app") } @@ -296,7 +296,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) { b.Fatalf("Failed to retrieve app.name") } - database, ok := result["database"].(map[string]interface{}) + database, ok := result["database"].(map[string]any) if !ok { b.Fatalf("Failed to retrieve database") } @@ -306,7 +306,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) { } // Second level nesting - settings, ok := app["settings"].(map[string]interface{}) + settings, ok := app["settings"].(map[string]any) if !ok { b.Fatalf("Failed to retrieve app.settings") } @@ -315,7 +315,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) { b.Fatalf("Failed to retrieve app.settings.enableLogging") } - credentials, ok := database["credentials"].(map[string]interface{}) + credentials, ok := database["credentials"].(map[string]any) if !ok { b.Fatalf("Failed to retrieve database.credentials") } @@ -338,7 +338,7 @@ func BenchmarkRetrieveArrayValuesJSON(b *testing.B) { }` // Parse once before benchmarking retrieval - var result map[string]interface{} + var result map[string]any err := json.Unmarshal([]byte(configData), &result) if err != nil { b.Fatalf("Failed to parse JSON: %v", err) @@ -347,7 +347,7 @@ func BenchmarkRetrieveArrayValuesJSON(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { // Get entire array - features, ok := result["features"].([]interface{}) + features, ok := result["features"].([]any) if !ok || len(features) != 4 { b.Fatalf("Failed to retrieve features array") } @@ -358,7 +358,7 @@ func BenchmarkRetrieveArrayValuesJSON(b *testing.B) { } // Numeric array - numbers, ok := result["numbers"].([]interface{}) + numbers, ok := result["numbers"].([]any) if !ok || len(numbers) != 5 { b.Fatalf("Failed to retrieve numbers array") } @@ -375,7 +375,7 @@ timeout: 30 ` // Parse once before benchmarking retrieval - var result map[string]interface{} + var result map[string]any err := yaml.Unmarshal([]byte(configData), &result) if err != nil { b.Fatalf("Failed to parse YAML: %v", err) @@ -419,7 +419,7 @@ timeout = 30 ` // Parse once before benchmarking retrieval - var result map[string]interface{} + var result map[string]any err := toml.Unmarshal([]byte(configData), &result) if err != nil { b.Fatalf("Failed to parse TOML: %v", err) @@ -537,26 +537,28 @@ write = 10 } // JSON config - var jsonResult map[string]interface{} + var jsonResult map[string]any err = json.Unmarshal([]byte(jsonConfig), &jsonResult) if err != nil { b.Fatalf("Failed to parse JSON: %v", err) } // YAML config - var yamlResult map[string]interface{} + var yamlResult map[string]any err = yaml.Unmarshal([]byte(yamlConfig), &yamlResult) if err != nil { b.Fatalf("Failed to parse YAML: %v", err) } // TOML config - var tomlResult map[string]interface{} + var tomlResult map[string]any err = toml.Unmarshal([]byte(tomlConfig), &tomlResult) if err != nil { b.Fatalf("Failed to parse TOML: %v", err) } + directData := customCfg.GetData() + b.Run("Custom-SimpleGet", func(b *testing.B) { for i := 0; i < b.N; i++ { _, err := customCfg.GetString("app.name") @@ -584,74 +586,98 @@ write = 10 } }) + b.Run("Direct-SimpleGet", func(b *testing.B) { + for i := 0; i < b.N; i++ { + app := directData["app"].(map[string]any) + _ = app["name"].(string) + } + }) + + b.Run("Direct-DeepGet", func(b *testing.B) { + for i := 0; i < b.N; i++ { + app := directData["app"].(map[string]any) + settings := app["settings"].(map[string]any) + timeouts := settings["timeouts"].(map[string]any) + _ = timeouts["read"].(int) + } + }) + + b.Run("Direct-ArrayGet", func(b *testing.B) { + for i := 0; i < b.N; i++ { + app := directData["app"].(map[string]any) + environments := app["environments"].([]any) + _ = environments[1].(string) + } + }) + b.Run("JSON-SimpleGet", func(b *testing.B) { for i := 0; i < b.N; i++ { - app := jsonResult["app"].(map[string]interface{}) + app := jsonResult["app"].(map[string]any) _ = app["name"].(string) } }) b.Run("JSON-DeepGet", func(b *testing.B) { for i := 0; i < b.N; i++ { - app := jsonResult["app"].(map[string]interface{}) - settings := app["settings"].(map[string]interface{}) - timeouts := settings["timeouts"].(map[string]interface{}) + app := jsonResult["app"].(map[string]any) + settings := app["settings"].(map[string]any) + timeouts := settings["timeouts"].(map[string]any) _ = timeouts["read"].(float64) } }) b.Run("JSON-ArrayGet", func(b *testing.B) { for i := 0; i < b.N; i++ { - app := jsonResult["app"].(map[string]interface{}) - environments := app["environments"].([]interface{}) + app := jsonResult["app"].(map[string]any) + environments := app["environments"].([]any) _ = environments[1].(string) } }) b.Run("YAML-SimpleGet", func(b *testing.B) { for i := 0; i < b.N; i++ { - app := yamlResult["app"].(map[string]interface{}) + app := yamlResult["app"].(map[string]any) _ = app["name"].(string) } }) b.Run("YAML-DeepGet", func(b *testing.B) { for i := 0; i < b.N; i++ { - app := yamlResult["app"].(map[string]interface{}) - settings := app["settings"].(map[string]interface{}) - timeouts := settings["timeouts"].(map[string]interface{}) + app := yamlResult["app"].(map[string]any) + settings := app["settings"].(map[string]any) + timeouts := settings["timeouts"].(map[string]any) _ = timeouts["read"].(int) } }) b.Run("YAML-ArrayGet", func(b *testing.B) { for i := 0; i < b.N; i++ { - app := yamlResult["app"].(map[string]interface{}) - environments := app["environments"].([]interface{}) + app := yamlResult["app"].(map[string]any) + environments := app["environments"].([]any) _ = environments[1].(string) } }) b.Run("TOML-SimpleGet", func(b *testing.B) { for i := 0; i < b.N; i++ { - app := tomlResult["app"].(map[string]interface{}) + app := tomlResult["app"].(map[string]any) _ = app["name"].(string) } }) b.Run("TOML-DeepGet", func(b *testing.B) { for i := 0; i < b.N; i++ { - app := tomlResult["app"].(map[string]interface{}) - settings := app["settings"].(map[string]interface{}) - timeouts := settings["timeouts"].(map[string]interface{}) + app := tomlResult["app"].(map[string]any) + settings := app["settings"].(map[string]any) + timeouts := settings["timeouts"].(map[string]any) _ = timeouts["read"].(int64) } }) b.Run("TOML-ArrayGet", func(b *testing.B) { for i := 0; i < b.N; i++ { - app := tomlResult["app"].(map[string]interface{}) - environments := app["environments"].([]interface{}) + app := tomlResult["app"].(map[string]any) + environments := app["environments"].([]any) _ = environments[1].(string) } }) diff --git a/config.go b/config.go index 725d596..1cc2e5b 100644 --- a/config.go +++ b/config.go @@ -6,7 +6,7 @@ import ( "strconv" ) -// Config holds a single hierarchical structure like JSON and handles parsing +// Config holds a single hierarchical structure and handles parsing type Config struct { data map[string]any dataRef *map[string]any // Reference to pooled map @@ -46,26 +46,30 @@ func (c *Config) Release() { c.stack = nil } +// GetData retrieves the entirety of the internal data map +func (c *Config) GetData() map[string]any { + return c.data +} + // Get retrieves a value from the config using dot notation func (c *Config) Get(key string) (any, error) { if key == "" { return c.data, nil } - // Parse the dot-notation path manually - var start, i int var current any = c.data + start := 0 + keyLen := len(key) - for i = 0; i < len(key); i++ { - if key[i] == '.' || i == len(key)-1 { + for i := 0; i < keyLen; i++ { + if key[i] == '.' || i == keyLen-1 { end := i - if i == len(key)-1 && key[i] != '.' { + if i == keyLen-1 && key[i] != '.' { end = i + 1 } part := key[start:end] - // Handle current node based on its type switch node := current.(type) { case map[string]any: val, ok := node[part] @@ -73,7 +77,6 @@ func (c *Config) Get(key string) (any, error) { return nil, fmt.Errorf("key %s not found", part) } current = val - case []any: index, err := strconv.Atoi(part) if err != nil { @@ -83,17 +86,13 @@ func (c *Config) Get(key string) (any, error) { return nil, fmt.Errorf("array index out of bounds: %d", index) } current = node[index] - default: return nil, fmt.Errorf("cannot access %s in non-container value", part) } - if i == len(key)-1 || (i < len(key)-1 && key[i] == '.' && end == i) { - if i == len(key)-1 { - return current, nil - } + if i == keyLen-1 { + return current, nil } - start = i + 1 } } @@ -122,8 +121,8 @@ func (c *Config) GetString(key string) (string, error) { return v, nil case bool: return strconv.FormatBool(v), nil - case int64: - return strconv.FormatInt(v, 10), nil + case int: + return strconv.Itoa(v), nil case float64: return strconv.FormatFloat(v, 'f', -1, 64), nil default: @@ -149,19 +148,20 @@ func (c *Config) GetBool(key string) (bool, error) { } // GetInt gets a value as int64 -func (c *Config) GetInt(key string) (int64, error) { +func (c *Config) GetInt(key string) (int, error) { val, err := c.Get(key) if err != nil { return 0, err } switch v := val.(type) { - case int64: + case int: return v, nil case float64: - return int64(v), nil + return int(v), nil case string: - return strconv.ParseInt(v, 10, 64) + parsed, err := strconv.Atoi(v) + return parsed, err default: return 0, fmt.Errorf("value for key %s cannot be converted to int", key) } @@ -177,7 +177,7 @@ func (c *Config) GetFloat(key string) (float64, error) { switch v := val.(type) { case float64: return v, nil - case int64: + case int: return float64(v), nil case string: return strconv.ParseFloat(v, 64) @@ -491,7 +491,7 @@ func (c *Config) tokenToValue(token Token) any { return val } // Integer - val, _ := strconv.ParseInt(valueStr, 10, 64) + val, _ := strconv.Atoi(valueStr) return val case TokenBoolean: @@ -519,7 +519,7 @@ func (c *Config) tokenToValue(token Token) any { return val } } else { - val, err := strconv.ParseInt(valueStr, 10, 64) + val, err := strconv.Atoi(valueStr) if err == nil { return val }