Compare commits

..

No commits in common. "57d38fd82e3b5e448621d96781cb694098d57173" and "80500963b438624286b4a991338b11d00fbcff77" have entirely different histories.

3 changed files with 113 additions and 150 deletions

View File

@ -1,19 +1,24 @@
# Sharkk Config File # Go Config Parser
SCF, pronounced scuff! A lightweight, intuitive configuration parser for Go applications with a clean, readable syntax.`
A very light, very intuitive config file format! Few symbols, few rules, quite flexible. Has support for comments, arrays, maps, and type guarantees. ## Features
Mercilessly benchmarked, fully tested. SCF competes toe-to-toe with Go's native JSON library, and handily outperforms the reference TOML and YAML implementations. - 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
## Format ## Configuration Format
SCF has very very simple syntax. This parser uses a clean, minimal syntax that's easy to read and write:
``` ```
-- Lua style comments!
host "localhost" host "localhost"
port 8080 -- no = for assignment! port 8080
debug true debug true
allowed_ips { allowed_ips {
@ -22,11 +27,6 @@ allowed_ips {
"10.0.0.1" "10.0.0.1"
} }
--[[
Feel free to space out your explanations!
All is well and good.
]]
database { database {
host "db.example.com" host "db.example.com"
port 5432 port 5432
@ -35,6 +35,11 @@ database {
password "secure123" password "secure123"
} }
} }
-- This is a line comment
--[[ This is a
block comment spanning
multiple lines ]]
``` ```
## Installation ## Installation
@ -58,29 +63,25 @@ import (
) )
func main() { func main() {
// Load configuration from file
file, err := os.Open("config.conf") file, err := os.Open("config.conf")
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer file.Close() defer file.Close()
// Pass a string to be loaded by the parser!
cfg, err := config.Load(file) cfg, err := config.Load(file)
if err != nil { if err != nil {
panic(err) panic(err)
} }
// Access values with type conversion. // Access values with type conversion
host, err := cfg.GetString("database.host") host, err := cfg.GetString("database.host")
port, err := cfg.GetInt("database.port") port, err := cfg.GetInt("database.port")
debug, err := cfg.GetBool("debug") debug, err := cfg.GetBool("debug")
// Use default values for missing keys // Use default values for missing keys
timeout := cfg.GetOr("timeout", 30).(int) timeout := cfg.GetOr("timeout", 30).(int64)
// Get the entire map to traverse directly (cuts traversal time by 50%!)
entireMap := cfg.GetData()
other, err := entireMap["database"]["host"].(string)
} }
``` ```
@ -89,14 +90,17 @@ func main() {
The parser automatically converts values to appropriate types: The parser automatically converts values to appropriate types:
```go ```go
// These will return properly typed values
boolValue, err := cfg.GetBool("feature.enabled") boolValue, err := cfg.GetBool("feature.enabled")
intValue, err := cfg.GetInt("server.port") intValue, err := cfg.GetInt("server.port")
floatValue, err := cfg.GetFloat("threshold") floatValue, err := cfg.GetFloat("threshold")
stringValue, err := cfg.GetString("app.name") stringValue, err := cfg.GetString("app.name")
// For complex types
arrayValue, err := cfg.GetArray("allowed_ips") arrayValue, err := cfg.GetArray("allowed_ips")
mapValue, err := cfg.GetMap("database") mapValue, err := cfg.GetMap("database")
// Generic getter (returns any) // Generic getter (returns interface{})
value, err := cfg.Get("some.key") value, err := cfg.Get("some.key")
``` ```
@ -119,20 +123,20 @@ This parser provides competitive performance compared to popular formats like JS
| Benchmark | Operations | Time (ns/op) | Memory (B/op) | Allocations (allocs/op) | | Benchmark | Operations | Time (ns/op) | Memory (B/op) | Allocations (allocs/op) |
|-----------|----------:|-------------:|--------------:|------------------------:| |-----------|----------:|-------------:|--------------:|------------------------:|
| **Small Config Files** | | **Small Config Files** |
| Config | 1,000,000 | 1,052 | 1,743 | 15 | | Config | 718,893 | 1,611 | 5,256 | 25 |
| JSON | 1,000,000 | 1,112 | 1,384 | 23 | | JSON | 1,000,000 | 1,170 | 1,384 | 23 |
| YAML | 215,121 | 5,600 | 8,888 | 82 | | YAML | 213,438 | 5,668 | 8,888 | 82 |
| TOML | 286,334 | 4,483 | 4,520 | 67 | | TOML | 273,586 | 4,505 | 4,520 | 67 |
| **Medium Config Files** | | **Medium Config Files** |
| Config | 211,863 | 5,696 | 4,056 | 74 | | Config | 138,517 | 8,777 | 11,247 | 114 |
| JSON | 261,925 | 4,602 | 5,344 | 89 | | JSON | 241,069 | 4,996 | 5,344 | 89 |
| YAML | 50,010 | 23,965 | 21,577 | 347 | | YAML | 47,695 | 24,183 | 21,577 | 347 |
| TOML | 68,420 | 17,639 | 16,348 | 208 | | TOML | 66,411 | 17,709 | 16,349 | 208 |
| **Large Config Files** | | **Large Config Files** |
| Config | 55,338 | 21,556 | 12,208 | 207 | | Config | 33,177 | 35,591 | 31,791 | 477 |
| JSON | 70,219 | 17,202 | 18,140 | 297 | | JSON | 66,384 | 18,066 | 18,138 | 297 |
| YAML | 12,536 | 95,945 | 65,568 | 1,208 | | YAML | 12,482 | 95,248 | 65,574 | 1,208 |
| TOML | 14,732 | 74,198 | 66,050 | 669 | | TOML | 17,594 | 67,928 | 66,038 | 669 |
*Benchmarked on AMD Ryzen 9 7950X 16-Core Processor* *Benchmarked on AMD Ryzen 9 7950X 16-Core Processor*

View File

@ -47,7 +47,7 @@ func BenchmarkRetrieveSimpleValues(b *testing.B) {
// Generic retrieval // Generic retrieval
timeout, err := cfg.Get("timeout") timeout, err := cfg.Get("timeout")
if err != nil || timeout.(int) != 30 { if err != nil || timeout.(int64) != 30 {
b.Fatalf("Failed to retrieve timeout: %v", err) b.Fatalf("Failed to retrieve timeout: %v", err)
} }
} }
@ -223,7 +223,7 @@ func BenchmarkRetrieveSimpleValuesJSON(b *testing.B) {
}` }`
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
var result map[string]any var result map[string]interface{}
err := json.Unmarshal([]byte(configData), &result) err := json.Unmarshal([]byte(configData), &result)
if err != nil { if err != nil {
b.Fatalf("Failed to parse JSON: %v", err) b.Fatalf("Failed to parse JSON: %v", err)
@ -278,7 +278,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) {
}` }`
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
var result map[string]any var result map[string]interface{}
err := json.Unmarshal([]byte(configData), &result) err := json.Unmarshal([]byte(configData), &result)
if err != nil { if err != nil {
b.Fatalf("Failed to parse JSON: %v", err) b.Fatalf("Failed to parse JSON: %v", err)
@ -287,7 +287,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
// First level nesting // First level nesting
app, ok := result["app"].(map[string]any) app, ok := result["app"].(map[string]interface{})
if !ok { if !ok {
b.Fatalf("Failed to retrieve app") b.Fatalf("Failed to retrieve app")
} }
@ -296,7 +296,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) {
b.Fatalf("Failed to retrieve app.name") b.Fatalf("Failed to retrieve app.name")
} }
database, ok := result["database"].(map[string]any) database, ok := result["database"].(map[string]interface{})
if !ok { if !ok {
b.Fatalf("Failed to retrieve database") b.Fatalf("Failed to retrieve database")
} }
@ -306,7 +306,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) {
} }
// Second level nesting // Second level nesting
settings, ok := app["settings"].(map[string]any) settings, ok := app["settings"].(map[string]interface{})
if !ok { if !ok {
b.Fatalf("Failed to retrieve app.settings") b.Fatalf("Failed to retrieve app.settings")
} }
@ -315,7 +315,7 @@ func BenchmarkRetrieveNestedValuesJSON(b *testing.B) {
b.Fatalf("Failed to retrieve app.settings.enableLogging") b.Fatalf("Failed to retrieve app.settings.enableLogging")
} }
credentials, ok := database["credentials"].(map[string]any) credentials, ok := database["credentials"].(map[string]interface{})
if !ok { if !ok {
b.Fatalf("Failed to retrieve database.credentials") b.Fatalf("Failed to retrieve database.credentials")
} }
@ -338,7 +338,7 @@ func BenchmarkRetrieveArrayValuesJSON(b *testing.B) {
}` }`
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
var result map[string]any var result map[string]interface{}
err := json.Unmarshal([]byte(configData), &result) err := json.Unmarshal([]byte(configData), &result)
if err != nil { if err != nil {
b.Fatalf("Failed to parse JSON: %v", err) b.Fatalf("Failed to parse JSON: %v", err)
@ -347,7 +347,7 @@ func BenchmarkRetrieveArrayValuesJSON(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
// Get entire array // Get entire array
features, ok := result["features"].([]any) features, ok := result["features"].([]interface{})
if !ok || len(features) != 4 { if !ok || len(features) != 4 {
b.Fatalf("Failed to retrieve features array") b.Fatalf("Failed to retrieve features array")
} }
@ -358,7 +358,7 @@ func BenchmarkRetrieveArrayValuesJSON(b *testing.B) {
} }
// Numeric array // Numeric array
numbers, ok := result["numbers"].([]any) numbers, ok := result["numbers"].([]interface{})
if !ok || len(numbers) != 5 { if !ok || len(numbers) != 5 {
b.Fatalf("Failed to retrieve numbers array") b.Fatalf("Failed to retrieve numbers array")
} }
@ -375,7 +375,7 @@ timeout: 30
` `
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
var result map[string]any var result map[string]interface{}
err := yaml.Unmarshal([]byte(configData), &result) err := yaml.Unmarshal([]byte(configData), &result)
if err != nil { if err != nil {
b.Fatalf("Failed to parse YAML: %v", err) b.Fatalf("Failed to parse YAML: %v", err)
@ -419,7 +419,7 @@ timeout = 30
` `
// Parse once before benchmarking retrieval // Parse once before benchmarking retrieval
var result map[string]any var result map[string]interface{}
err := toml.Unmarshal([]byte(configData), &result) err := toml.Unmarshal([]byte(configData), &result)
if err != nil { if err != nil {
b.Fatalf("Failed to parse TOML: %v", err) b.Fatalf("Failed to parse TOML: %v", err)
@ -537,28 +537,26 @@ write = 10
} }
// JSON config // JSON config
var jsonResult map[string]any var jsonResult map[string]interface{}
err = json.Unmarshal([]byte(jsonConfig), &jsonResult) err = json.Unmarshal([]byte(jsonConfig), &jsonResult)
if err != nil { if err != nil {
b.Fatalf("Failed to parse JSON: %v", err) b.Fatalf("Failed to parse JSON: %v", err)
} }
// YAML config // YAML config
var yamlResult map[string]any var yamlResult map[string]interface{}
err = yaml.Unmarshal([]byte(yamlConfig), &yamlResult) err = yaml.Unmarshal([]byte(yamlConfig), &yamlResult)
if err != nil { if err != nil {
b.Fatalf("Failed to parse YAML: %v", err) b.Fatalf("Failed to parse YAML: %v", err)
} }
// TOML config // TOML config
var tomlResult map[string]any var tomlResult map[string]interface{}
err = toml.Unmarshal([]byte(tomlConfig), &tomlResult) err = toml.Unmarshal([]byte(tomlConfig), &tomlResult)
if err != nil { if err != nil {
b.Fatalf("Failed to parse TOML: %v", err) b.Fatalf("Failed to parse TOML: %v", err)
} }
directData := customCfg.GetData()
b.Run("Custom-SimpleGet", func(b *testing.B) { b.Run("Custom-SimpleGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := customCfg.GetString("app.name") _, err := customCfg.GetString("app.name")
@ -586,98 +584,74 @@ 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) { b.Run("JSON-SimpleGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := jsonResult["app"].(map[string]any) app := jsonResult["app"].(map[string]interface{})
_ = app["name"].(string) _ = app["name"].(string)
} }
}) })
b.Run("JSON-DeepGet", func(b *testing.B) { b.Run("JSON-DeepGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := jsonResult["app"].(map[string]any) app := jsonResult["app"].(map[string]interface{})
settings := app["settings"].(map[string]any) settings := app["settings"].(map[string]interface{})
timeouts := settings["timeouts"].(map[string]any) timeouts := settings["timeouts"].(map[string]interface{})
_ = timeouts["read"].(float64) _ = timeouts["read"].(float64)
} }
}) })
b.Run("JSON-ArrayGet", func(b *testing.B) { b.Run("JSON-ArrayGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := jsonResult["app"].(map[string]any) app := jsonResult["app"].(map[string]interface{})
environments := app["environments"].([]any) environments := app["environments"].([]interface{})
_ = environments[1].(string) _ = environments[1].(string)
} }
}) })
b.Run("YAML-SimpleGet", func(b *testing.B) { b.Run("YAML-SimpleGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := yamlResult["app"].(map[string]any) app := yamlResult["app"].(map[string]interface{})
_ = app["name"].(string) _ = app["name"].(string)
} }
}) })
b.Run("YAML-DeepGet", func(b *testing.B) { b.Run("YAML-DeepGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := yamlResult["app"].(map[string]any) app := yamlResult["app"].(map[string]interface{})
settings := app["settings"].(map[string]any) settings := app["settings"].(map[string]interface{})
timeouts := settings["timeouts"].(map[string]any) timeouts := settings["timeouts"].(map[string]interface{})
_ = timeouts["read"].(int) _ = timeouts["read"].(int)
} }
}) })
b.Run("YAML-ArrayGet", func(b *testing.B) { b.Run("YAML-ArrayGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := yamlResult["app"].(map[string]any) app := yamlResult["app"].(map[string]interface{})
environments := app["environments"].([]any) environments := app["environments"].([]interface{})
_ = environments[1].(string) _ = environments[1].(string)
} }
}) })
b.Run("TOML-SimpleGet", func(b *testing.B) { b.Run("TOML-SimpleGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := tomlResult["app"].(map[string]any) app := tomlResult["app"].(map[string]interface{})
_ = app["name"].(string) _ = app["name"].(string)
} }
}) })
b.Run("TOML-DeepGet", func(b *testing.B) { b.Run("TOML-DeepGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := tomlResult["app"].(map[string]any) app := tomlResult["app"].(map[string]interface{})
settings := app["settings"].(map[string]any) settings := app["settings"].(map[string]interface{})
timeouts := settings["timeouts"].(map[string]any) timeouts := settings["timeouts"].(map[string]interface{})
_ = timeouts["read"].(int64) _ = timeouts["read"].(int64)
} }
}) })
b.Run("TOML-ArrayGet", func(b *testing.B) { b.Run("TOML-ArrayGet", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
app := tomlResult["app"].(map[string]any) app := tomlResult["app"].(map[string]interface{})
environments := app["environments"].([]any) environments := app["environments"].([]interface{})
_ = environments[1].(string) _ = environments[1].(string)
} }
}) })

View File

@ -4,9 +4,10 @@ import (
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"strings"
) )
// Config holds a single hierarchical structure and handles parsing // Config holds a single hierarchical structure like JSON and handles parsing
type Config struct { type Config struct {
data map[string]any data map[string]any
dataRef *map[string]any // Reference to pooled map dataRef *map[string]any // Reference to pooled map
@ -46,37 +47,29 @@ func (c *Config) Release() {
c.stack = nil 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 // Get retrieves a value from the config using dot notation
func (c *Config) Get(key string) (any, error) { func (c *Config) Get(key string) (any, error) {
if key == "" { if key == "" {
return c.data, nil return c.data, nil
} }
var current any = c.data if !strings.Contains(key, ".") {
start := 0 if val, ok := c.data[key]; ok {
keyLen := len(key) return val, nil
}
for i := 0; i < keyLen; i++ { return nil, fmt.Errorf("key %s not found", key)
if key[i] == '.' || i == keyLen-1 {
end := i
if i == keyLen-1 && key[i] != '.' {
end = i + 1
} }
part := key[start:end] parts := strings.Split(key, ".")
current := any(c.data)
for _, part := range parts {
switch node := current.(type) { switch node := current.(type) {
case map[string]any: case map[string]any:
val, ok := node[part] var exists bool
if !ok { current, exists = node[part]
if !exists {
return nil, fmt.Errorf("key %s not found", part) return nil, fmt.Errorf("key %s not found", part)
} }
current = val
case []any: case []any:
index, err := strconv.Atoi(part) index, err := strconv.Atoi(part)
if err != nil { if err != nil {
@ -89,14 +82,7 @@ func (c *Config) Get(key string) (any, error) {
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 {
return current, nil
} }
start = i + 1
}
}
return current, nil return current, nil
} }
@ -121,8 +107,8 @@ func (c *Config) GetString(key string) (string, error) {
return v, nil return v, nil
case bool: case bool:
return strconv.FormatBool(v), nil return strconv.FormatBool(v), nil
case int: case int64:
return strconv.Itoa(v), nil return strconv.FormatInt(v, 10), nil
case float64: case float64:
return strconv.FormatFloat(v, 'f', -1, 64), nil return strconv.FormatFloat(v, 'f', -1, 64), nil
default: default:
@ -148,20 +134,19 @@ func (c *Config) GetBool(key string) (bool, error) {
} }
// GetInt gets a value as int64 // GetInt gets a value as int64
func (c *Config) GetInt(key string) (int, error) { func (c *Config) GetInt(key string) (int64, error) {
val, err := c.Get(key) val, err := c.Get(key)
if err != nil { if err != nil {
return 0, err return 0, err
} }
switch v := val.(type) { switch v := val.(type) {
case int: case int64:
return v, nil return v, nil
case float64: case float64:
return int(v), nil return int64(v), nil
case string: case string:
parsed, err := strconv.Atoi(v) return strconv.ParseInt(v, 10, 64)
return parsed, err
default: default:
return 0, fmt.Errorf("value for key %s cannot be converted to int", key) return 0, fmt.Errorf("value for key %s cannot be converted to int", key)
} }
@ -177,7 +162,7 @@ func (c *Config) GetFloat(key string) (float64, error) {
switch v := val.(type) { switch v := val.(type) {
case float64: case float64:
return v, nil return v, nil
case int: case int64:
return float64(v), nil return float64(v), nil
case string: case string:
return strconv.ParseFloat(v, 64) return strconv.ParseFloat(v, 64)
@ -491,7 +476,7 @@ func (c *Config) tokenToValue(token Token) any {
return val return val
} }
// Integer // Integer
val, _ := strconv.Atoi(valueStr) val, _ := strconv.ParseInt(valueStr, 10, 64)
return val return val
case TokenBoolean: case TokenBoolean:
@ -519,7 +504,7 @@ func (c *Config) tokenToValue(token Token) any {
return val return val
} }
} else { } else {
val, err := strconv.Atoi(valueStr) val, err := strconv.ParseInt(valueStr, 10, 64)
if err == nil { if err == nil {
return val return val
} }