ref 1
This commit is contained in:
parent
4690d93d1f
commit
3563c0f8ee
154
README.md
154
README.md
|
@ -1,3 +1,153 @@
|
||||||
# Config
|
# Go Config Parser
|
||||||
|
|
||||||
A super-simple config file parser that has a Lua-like format.
|
A lightweight, intuitive configuration parser for Go applications with a clean, readable syntax.`
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## Configuration Format
|
||||||
|
|
||||||
|
This parser uses a clean, minimal syntax that's easy to read and write:
|
||||||
|
|
||||||
|
```
|
||||||
|
host = "localhost"
|
||||||
|
port = 8080
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
allowed_ips {
|
||||||
|
"192.168.1.1"
|
||||||
|
"192.168.1.2"
|
||||||
|
"10.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
host = "db.example.com"
|
||||||
|
port = 5432
|
||||||
|
credentials {
|
||||||
|
username = "admin"
|
||||||
|
password = "secure123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- This is a line comment
|
||||||
|
--[[ This is a
|
||||||
|
block comment spanning
|
||||||
|
multiple lines ]]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.sharkk.net/Go/Config
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
config "git.sharkk.net/Go/Config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Load configuration from file
|
||||||
|
file, err := os.Open("config.conf")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
cfg, err := config.Load(file)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Conversion
|
||||||
|
|
||||||
|
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")
|
||||||
|
stringValue, err := cfg.GetString("app.name")
|
||||||
|
|
||||||
|
// For complex types
|
||||||
|
arrayValue, err := cfg.GetArray("allowed_ips")
|
||||||
|
mapValue, err := cfg.GetMap("database")
|
||||||
|
|
||||||
|
// Generic getter (returns interface{})
|
||||||
|
value, err := cfg.Get("some.key")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing Arrays and Maps
|
||||||
|
|
||||||
|
Access array elements and nested map values using dot notation:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Access the first element in the array
|
||||||
|
firstIP, err := cfg.GetString("allowed_ips.0")
|
||||||
|
|
||||||
|
// Access deeply nested values
|
||||||
|
username, err := cfg.GetString("database.credentials.username")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
This parser provides competitive performance compared to popular formats like JSON, YAML, and TOML:
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
| **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 |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
*Benchmarked on AMD Ryzen 9 7950X 16-Core Processor*
|
||||||
|
|
||||||
|
## Why Choose This Parser?
|
||||||
|
|
||||||
|
- **Readability**: Simple syntax that's easy for humans to read and write
|
||||||
|
- **Flexibility**: Supports various data types and nested structures
|
||||||
|
- **Performance**: Fast parsing with minimal overhead
|
||||||
|
- **Type Safety**: Strong typing with automatic conversion
|
||||||
|
- **Simplicity**: No external dependencies required
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
617
bench/bench_test.go
Normal file
617
bench/bench_test.go
Normal file
|
@ -0,0 +1,617 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
config "git.sharkk.net/Go/Config"
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Original benchmarks
|
||||||
|
func BenchmarkSmallConfig(b *testing.B) {
|
||||||
|
// Small config with just a few key-value pairs
|
||||||
|
smallConfig := `
|
||||||
|
host = "localhost"
|
||||||
|
port = 8080
|
||||||
|
debug = true
|
||||||
|
timeout = 30
|
||||||
|
`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
reader := strings.NewReader(smallConfig)
|
||||||
|
_, err := config.Load(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse small config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMediumConfig(b *testing.B) {
|
||||||
|
// Medium config with nested structures and arrays
|
||||||
|
mediumConfig := `
|
||||||
|
app {
|
||||||
|
name = "TestApp"
|
||||||
|
version = "1.0.0"
|
||||||
|
enableLogging = true
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
host = "db.example.com"
|
||||||
|
port = 5432
|
||||||
|
credentials {
|
||||||
|
username = "admin"
|
||||||
|
password = "secure123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
features = {
|
||||||
|
"authentication"
|
||||||
|
"authorization"
|
||||||
|
"reporting"
|
||||||
|
"analytics"
|
||||||
|
}
|
||||||
|
|
||||||
|
timeouts {
|
||||||
|
connect = 5
|
||||||
|
read = 10
|
||||||
|
write = 10
|
||||||
|
idle = 60
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Comments to add some parsing overhead
|
||||||
|
endpoints {
|
||||||
|
api = "/api/v1"
|
||||||
|
web = "/web"
|
||||||
|
admin = "/admin"
|
||||||
|
health = "/health"
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
reader := strings.NewReader(mediumConfig)
|
||||||
|
_, err := config.Load(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse medium config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLargeConfig(b *testing.B) {
|
||||||
|
// Simpler large config with careful bracket matching
|
||||||
|
largeConfig := `
|
||||||
|
application {
|
||||||
|
name = "EnterpriseApp"
|
||||||
|
version = "2.5.1"
|
||||||
|
environment = "production"
|
||||||
|
debug = false
|
||||||
|
maxConnections = 1000
|
||||||
|
timeout = 30
|
||||||
|
retryCount = 3
|
||||||
|
logLevel = "info"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Database cluster configuration
|
||||||
|
databases {
|
||||||
|
primary {
|
||||||
|
host = "primary-db.example.com"
|
||||||
|
port = 5432
|
||||||
|
maxConnections = 100
|
||||||
|
credentials {
|
||||||
|
username = "app_user"
|
||||||
|
password = "super_secret"
|
||||||
|
ssl = true
|
||||||
|
timeout = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replica {
|
||||||
|
host = "replica-db.example.com"
|
||||||
|
port = 5432
|
||||||
|
maxConnections = 200
|
||||||
|
credentials {
|
||||||
|
username = "read_user"
|
||||||
|
password = "read_only_pw"
|
||||||
|
ssl = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedIPs {
|
||||||
|
"192.168.1.1"
|
||||||
|
"192.168.1.2"
|
||||||
|
"192.168.1.3"
|
||||||
|
"192.168.1.4"
|
||||||
|
"192.168.1.5"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Add 50 numbered settings to make the config large
|
||||||
|
settings {`
|
||||||
|
|
||||||
|
// Add many numbered settings
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(largeConfig)
|
||||||
|
|
||||||
|
for i := 1; i <= 50; i++ {
|
||||||
|
builder.WriteString("\n\t\tsetting")
|
||||||
|
builder.WriteString(strconv.Itoa(i))
|
||||||
|
builder.WriteString(" = ")
|
||||||
|
builder.WriteString(strconv.Itoa(i * 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the settings block and add one more block
|
||||||
|
builder.WriteString(`
|
||||||
|
}
|
||||||
|
|
||||||
|
roles {
|
||||||
|
admin {
|
||||||
|
permissions = {
|
||||||
|
"read"
|
||||||
|
"write"
|
||||||
|
"delete"
|
||||||
|
"admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user {
|
||||||
|
permissions = {
|
||||||
|
"read"
|
||||||
|
"write"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
largeConfig = builder.String()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
reader := strings.NewReader(largeConfig)
|
||||||
|
_, err := config.Load(reader)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse large config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON Benchmarks
|
||||||
|
func BenchmarkSmallConfigJSON(b *testing.B) {
|
||||||
|
smallConfigJSON := `{
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 8080,
|
||||||
|
"debug": true,
|
||||||
|
"timeout": 30
|
||||||
|
}`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
reader := strings.NewReader(smallConfigJSON)
|
||||||
|
var result map[string]interface{}
|
||||||
|
decoder := json.NewDecoder(reader)
|
||||||
|
err := decoder.Decode(&result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse small JSON config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMediumConfigJSON(b *testing.B) {
|
||||||
|
mediumConfigJSON := `{
|
||||||
|
"app": {
|
||||||
|
"name": "TestApp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"enableLogging": true
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"host": "db.example.com",
|
||||||
|
"port": 5432,
|
||||||
|
"credentials": {
|
||||||
|
"username": "admin",
|
||||||
|
"password": "secure123"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"features": [
|
||||||
|
"authentication",
|
||||||
|
"authorization",
|
||||||
|
"reporting",
|
||||||
|
"analytics"
|
||||||
|
],
|
||||||
|
"timeouts": {
|
||||||
|
"connect": 5,
|
||||||
|
"read": 10,
|
||||||
|
"write": 10,
|
||||||
|
"idle": 60
|
||||||
|
},
|
||||||
|
"endpoints": {
|
||||||
|
"api": "/api/v1",
|
||||||
|
"web": "/web",
|
||||||
|
"admin": "/admin",
|
||||||
|
"health": "/health"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
reader := strings.NewReader(mediumConfigJSON)
|
||||||
|
var result map[string]interface{}
|
||||||
|
decoder := json.NewDecoder(reader)
|
||||||
|
err := decoder.Decode(&result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse medium JSON config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLargeConfigJSON(b *testing.B) {
|
||||||
|
// Start building the large JSON config
|
||||||
|
largeConfigJSON := `{
|
||||||
|
"application": {
|
||||||
|
"name": "EnterpriseApp",
|
||||||
|
"version": "2.5.1",
|
||||||
|
"environment": "production",
|
||||||
|
"debug": false,
|
||||||
|
"maxConnections": 1000,
|
||||||
|
"timeout": 30,
|
||||||
|
"retryCount": 3,
|
||||||
|
"logLevel": "info"
|
||||||
|
},
|
||||||
|
"databases": {
|
||||||
|
"primary": {
|
||||||
|
"host": "primary-db.example.com",
|
||||||
|
"port": 5432,
|
||||||
|
"maxConnections": 100,
|
||||||
|
"credentials": {
|
||||||
|
"username": "app_user",
|
||||||
|
"password": "super_secret",
|
||||||
|
"ssl": true,
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replica": {
|
||||||
|
"host": "replica-db.example.com",
|
||||||
|
"port": 5432,
|
||||||
|
"maxConnections": 200,
|
||||||
|
"credentials": {
|
||||||
|
"username": "read_user",
|
||||||
|
"password": "read_only_pw",
|
||||||
|
"ssl": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allowedIPs": [
|
||||||
|
"192.168.1.1",
|
||||||
|
"192.168.1.2",
|
||||||
|
"192.168.1.3",
|
||||||
|
"192.168.1.4",
|
||||||
|
"192.168.1.5"
|
||||||
|
],
|
||||||
|
"settings": {`
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(largeConfigJSON)
|
||||||
|
|
||||||
|
// Add many numbered settings
|
||||||
|
for i := 1; i <= 50; i++ {
|
||||||
|
if i > 1 {
|
||||||
|
builder.WriteString(",")
|
||||||
|
}
|
||||||
|
builder.WriteString("\n\t\t\t\"setting")
|
||||||
|
builder.WriteString(strconv.Itoa(i))
|
||||||
|
builder.WriteString("\": ")
|
||||||
|
builder.WriteString(strconv.Itoa(i * 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the settings block and add roles
|
||||||
|
builder.WriteString(`
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"admin": {
|
||||||
|
"permissions": [
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"delete",
|
||||||
|
"admin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"permissions": [
|
||||||
|
"read",
|
||||||
|
"write"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
largeConfigJSONString := builder.String()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
reader := strings.NewReader(largeConfigJSONString)
|
||||||
|
var result map[string]interface{}
|
||||||
|
decoder := json.NewDecoder(reader)
|
||||||
|
err := decoder.Decode(&result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse large JSON config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// YAML Benchmarks
|
||||||
|
func BenchmarkSmallConfigYAML(b *testing.B) {
|
||||||
|
smallConfigYAML := `
|
||||||
|
host: localhost
|
||||||
|
port: 8080
|
||||||
|
debug: true
|
||||||
|
timeout: 30
|
||||||
|
`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := yaml.Unmarshal([]byte(smallConfigYAML), &result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse small YAML config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMediumConfigYAML(b *testing.B) {
|
||||||
|
mediumConfigYAML := `
|
||||||
|
app:
|
||||||
|
name: TestApp
|
||||||
|
version: 1.0.0
|
||||||
|
enableLogging: true
|
||||||
|
database:
|
||||||
|
host: db.example.com
|
||||||
|
port: 5432
|
||||||
|
credentials:
|
||||||
|
username: admin
|
||||||
|
password: secure123
|
||||||
|
features:
|
||||||
|
- authentication
|
||||||
|
- authorization
|
||||||
|
- reporting
|
||||||
|
- analytics
|
||||||
|
timeouts:
|
||||||
|
connect: 5
|
||||||
|
read: 10
|
||||||
|
write: 10
|
||||||
|
idle: 60
|
||||||
|
# Comments to add some parsing overhead
|
||||||
|
endpoints:
|
||||||
|
api: /api/v1
|
||||||
|
web: /web
|
||||||
|
admin: /admin
|
||||||
|
health: /health
|
||||||
|
`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := yaml.Unmarshal([]byte(mediumConfigYAML), &result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse medium YAML config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLargeConfigYAML(b *testing.B) {
|
||||||
|
// Start building the large YAML config
|
||||||
|
largeConfigYAML := `
|
||||||
|
application:
|
||||||
|
name: EnterpriseApp
|
||||||
|
version: 2.5.1
|
||||||
|
environment: production
|
||||||
|
debug: false
|
||||||
|
maxConnections: 1000
|
||||||
|
timeout: 30
|
||||||
|
retryCount: 3
|
||||||
|
logLevel: info
|
||||||
|
|
||||||
|
# Database cluster configuration
|
||||||
|
databases:
|
||||||
|
primary:
|
||||||
|
host: primary-db.example.com
|
||||||
|
port: 5432
|
||||||
|
maxConnections: 100
|
||||||
|
credentials:
|
||||||
|
username: app_user
|
||||||
|
password: super_secret
|
||||||
|
ssl: true
|
||||||
|
timeout: 5
|
||||||
|
replica:
|
||||||
|
host: replica-db.example.com
|
||||||
|
port: 5432
|
||||||
|
maxConnections: 200
|
||||||
|
credentials:
|
||||||
|
username: read_user
|
||||||
|
password: read_only_pw
|
||||||
|
ssl: true
|
||||||
|
|
||||||
|
allowedIPs:
|
||||||
|
- 192.168.1.1
|
||||||
|
- 192.168.1.2
|
||||||
|
- 192.168.1.3
|
||||||
|
- 192.168.1.4
|
||||||
|
- 192.168.1.5
|
||||||
|
|
||||||
|
# Add 50 numbered settings to make the config large
|
||||||
|
settings:
|
||||||
|
`
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(largeConfigYAML)
|
||||||
|
|
||||||
|
// Add many numbered settings
|
||||||
|
for i := 1; i <= 50; i++ {
|
||||||
|
builder.WriteString(" setting")
|
||||||
|
builder.WriteString(strconv.Itoa(i))
|
||||||
|
builder.WriteString(": ")
|
||||||
|
builder.WriteString(strconv.Itoa(i * 10))
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add roles
|
||||||
|
builder.WriteString(`
|
||||||
|
roles:
|
||||||
|
admin:
|
||||||
|
permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- delete
|
||||||
|
- admin
|
||||||
|
user:
|
||||||
|
permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
`)
|
||||||
|
|
||||||
|
largeConfigYAMLString := builder.String()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := yaml.Unmarshal([]byte(largeConfigYAMLString), &result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse large YAML config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOML Benchmarks
|
||||||
|
func BenchmarkSmallConfigTOML(b *testing.B) {
|
||||||
|
smallConfigTOML := `
|
||||||
|
host = "localhost"
|
||||||
|
port = 8080
|
||||||
|
debug = true
|
||||||
|
timeout = 30
|
||||||
|
`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := toml.Unmarshal([]byte(smallConfigTOML), &result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse small TOML config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMediumConfigTOML(b *testing.B) {
|
||||||
|
mediumConfigTOML := `
|
||||||
|
[app]
|
||||||
|
name = "TestApp"
|
||||||
|
version = "1.0.0"
|
||||||
|
enableLogging = true
|
||||||
|
|
||||||
|
[database]
|
||||||
|
host = "db.example.com"
|
||||||
|
port = 5432
|
||||||
|
|
||||||
|
[database.credentials]
|
||||||
|
username = "admin"
|
||||||
|
password = "secure123"
|
||||||
|
|
||||||
|
features = ["authentication", "authorization", "reporting", "analytics"]
|
||||||
|
|
||||||
|
[timeouts]
|
||||||
|
connect = 5
|
||||||
|
read = 10
|
||||||
|
write = 10
|
||||||
|
idle = 60
|
||||||
|
|
||||||
|
# Comments to add some parsing overhead
|
||||||
|
[endpoints]
|
||||||
|
api = "/api/v1"
|
||||||
|
web = "/web"
|
||||||
|
admin = "/admin"
|
||||||
|
health = "/health"
|
||||||
|
`
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := toml.Unmarshal([]byte(mediumConfigTOML), &result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse medium TOML config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLargeConfigTOML(b *testing.B) {
|
||||||
|
// Start building the large TOML config
|
||||||
|
largeConfigTOML := `
|
||||||
|
[application]
|
||||||
|
name = "EnterpriseApp"
|
||||||
|
version = "2.5.1"
|
||||||
|
environment = "production"
|
||||||
|
debug = false
|
||||||
|
maxConnections = 1000
|
||||||
|
timeout = 30
|
||||||
|
retryCount = 3
|
||||||
|
logLevel = "info"
|
||||||
|
|
||||||
|
# Database cluster configuration
|
||||||
|
[databases.primary]
|
||||||
|
host = "primary-db.example.com"
|
||||||
|
port = 5432
|
||||||
|
maxConnections = 100
|
||||||
|
|
||||||
|
[databases.primary.credentials]
|
||||||
|
username = "app_user"
|
||||||
|
password = "super_secret"
|
||||||
|
ssl = true
|
||||||
|
timeout = 5
|
||||||
|
|
||||||
|
[databases.replica]
|
||||||
|
host = "replica-db.example.com"
|
||||||
|
port = 5432
|
||||||
|
maxConnections = 200
|
||||||
|
|
||||||
|
[databases.replica.credentials]
|
||||||
|
username = "read_user"
|
||||||
|
password = "read_only_pw"
|
||||||
|
ssl = true
|
||||||
|
|
||||||
|
allowedIPs = ["192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4", "192.168.1.5"]
|
||||||
|
|
||||||
|
# Add 50 numbered settings to make the config large
|
||||||
|
[settings]
|
||||||
|
`
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(largeConfigTOML)
|
||||||
|
|
||||||
|
// Add many numbered settings
|
||||||
|
for i := 1; i <= 50; i++ {
|
||||||
|
builder.WriteString("setting")
|
||||||
|
builder.WriteString(strconv.Itoa(i))
|
||||||
|
builder.WriteString(" = ")
|
||||||
|
builder.WriteString(strconv.Itoa(i * 10))
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add roles
|
||||||
|
builder.WriteString(`
|
||||||
|
[roles.admin]
|
||||||
|
permissions = ["read", "write", "delete", "admin"]
|
||||||
|
|
||||||
|
[roles.user]
|
||||||
|
permissions = ["read", "write"]
|
||||||
|
`)
|
||||||
|
|
||||||
|
largeConfigTOMLString := builder.String()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := toml.Unmarshal([]byte(largeConfigTOMLString), &result)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("Failed to parse large TOML config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
194
config.go
Normal file
194
config.go
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds a single hierarchical structure like JSON
|
||||||
|
type Config struct {
|
||||||
|
data map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig creates a new empty config
|
||||||
|
func NewConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
data: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
for i = 0; i < len(key); i++ {
|
||||||
|
if key[i] == '.' || i == len(key)-1 {
|
||||||
|
end := i
|
||||||
|
if i == len(key)-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:
|
||||||
|
// Simple map lookup
|
||||||
|
val, ok := node[part]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("key %s not found", part)
|
||||||
|
}
|
||||||
|
current = val
|
||||||
|
|
||||||
|
case []any:
|
||||||
|
// Must be numeric index
|
||||||
|
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:
|
||||||
|
return nil, fmt.Errorf("cannot access %s in non-container value", part)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've processed the entire key, return the current value
|
||||||
|
if i == len(key)-1 || (i < len(key)-1 && key[i] == '.' && end == i) {
|
||||||
|
if i == len(key)-1 {
|
||||||
|
return current, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOr retrieves a value or returns a default if not found
|
||||||
|
func (c *Config) GetOr(key string, defaultValue any) any {
|
||||||
|
val, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString gets a value as string
|
||||||
|
func (c *Config) GetString(key string) (string, error) {
|
||||||
|
val, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
return v, nil
|
||||||
|
case bool:
|
||||||
|
return strconv.FormatBool(v), nil
|
||||||
|
case int64:
|
||||||
|
return strconv.FormatInt(v, 10), 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 (c *Config) GetBool(key string) (bool, error) {
|
||||||
|
val, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 int64
|
||||||
|
func (c *Config) GetInt(key string) (int64, error) {
|
||||||
|
val, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := val.(type) {
|
||||||
|
case int64:
|
||||||
|
return v, nil
|
||||||
|
case float64:
|
||||||
|
return int64(v), nil
|
||||||
|
case string:
|
||||||
|
return strconv.ParseInt(v, 10, 64)
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("value for key %s cannot be converted to int", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloat gets a value as float64
|
||||||
|
func (c *Config) GetFloat(key string) (float64, error) {
|
||||||
|
val, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := val.(type) {
|
||||||
|
case float64:
|
||||||
|
return v, nil
|
||||||
|
case int64:
|
||||||
|
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 (c *Config) GetArray(key string) ([]any, error) {
|
||||||
|
val, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if arr, ok := val.([]any); ok {
|
||||||
|
return arr, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("value for key %s is not an array", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMap gets a value as map[string]any
|
||||||
|
func (c *Config) GetMap(key string) (map[string]any, error) {
|
||||||
|
val, err := c.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m, ok := val.(map[string]any); ok {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("value for key %s is not a map", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load parses a config from a reader
|
||||||
|
func Load(r io.Reader) (*Config, error) {
|
||||||
|
parser := NewParser(r)
|
||||||
|
return parser.Parse()
|
||||||
|
}
|
8
go.mod
Normal file
8
go.mod
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module git.sharkk.net/Go/Config
|
||||||
|
|
||||||
|
go 1.23.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
5
go.sum
Normal file
5
go.sum
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||||
|
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
171
parser.go
Normal file
171
parser.go
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parser parses configuration files
|
||||||
|
type Parser struct {
|
||||||
|
scanner *Scanner
|
||||||
|
config *Config
|
||||||
|
currentObject map[string]any
|
||||||
|
stack []map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParser creates a new parser with a reader and empty config
|
||||||
|
func NewParser(r io.Reader) *Parser {
|
||||||
|
config := NewConfig()
|
||||||
|
return &Parser{
|
||||||
|
scanner: NewScanner(r),
|
||||||
|
config: config,
|
||||||
|
currentObject: config.data,
|
||||||
|
stack: make([]map[string]any, 0, 8), // Pre-allocate stack with reasonable capacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error creates an error with line information from the scanner
|
||||||
|
func (p *Parser) Error(msg string) error {
|
||||||
|
return fmt.Errorf("line %d: %s", p.scanner.line, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses the config file and returns a Config
|
||||||
|
func (p *Parser) Parse() (*Config, error) {
|
||||||
|
err := p.parseContent()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushObject enters a new object scope
|
||||||
|
func (p *Parser) pushObject(obj map[string]any) {
|
||||||
|
p.stack = append(p.stack, p.currentObject)
|
||||||
|
p.currentObject = obj
|
||||||
|
}
|
||||||
|
|
||||||
|
// popObject exits the current object scope
|
||||||
|
func (p *Parser) popObject() {
|
||||||
|
n := len(p.stack)
|
||||||
|
if n > 0 {
|
||||||
|
p.currentObject = p.stack[n-1]
|
||||||
|
p.stack = p.stack[:n-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseContent is the main parsing function
|
||||||
|
func (p *Parser) parseContent() error {
|
||||||
|
skipErr := p.scanner.SkipWhitespace()
|
||||||
|
for ; skipErr == nil; skipErr = p.scanner.SkipWhitespace() {
|
||||||
|
r, peekErr := p.scanner.PeekRune()
|
||||||
|
if peekErr == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if peekErr != nil {
|
||||||
|
return peekErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle comments
|
||||||
|
if r == '-' {
|
||||||
|
if err := p.scanner.ScanComment(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle name=value pairs or named objects
|
||||||
|
if isLetter(r) {
|
||||||
|
name, err := p.scanner.ScanName()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.scanner.SkipWhitespace(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err = p.scanner.PeekRune()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignment or direct map/array
|
||||||
|
if r == '=' {
|
||||||
|
// It's a standard key=value pair
|
||||||
|
p.scanner.ReadRune() // consume '='
|
||||||
|
|
||||||
|
if err = p.scanner.SkipWhitespace(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := p.scanner.ScanValue()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the value directly
|
||||||
|
if mapValue, ok := value.(map[string]any); ok {
|
||||||
|
// Add an entry in current object
|
||||||
|
newMap := make(map[string]any, 8) // Pre-allocate with capacity
|
||||||
|
p.currentObject[name] = newMap
|
||||||
|
|
||||||
|
// Process the map contents
|
||||||
|
p.pushObject(newMap)
|
||||||
|
|
||||||
|
// Copy values from scanned map to our object
|
||||||
|
for k, v := range mapValue {
|
||||||
|
p.currentObject[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
p.popObject()
|
||||||
|
} else {
|
||||||
|
// Direct storage for primitives and arrays
|
||||||
|
p.currentObject[name] = value
|
||||||
|
}
|
||||||
|
} else if r == '{' {
|
||||||
|
// It's a map/array without '='
|
||||||
|
value, err := p.scanner.ScanValue()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the complex value directly
|
||||||
|
if mapValue, ok := value.(map[string]any); ok {
|
||||||
|
// Add an entry in current object
|
||||||
|
newMap := make(map[string]any, 8) // Pre-allocate with capacity
|
||||||
|
p.currentObject[name] = newMap
|
||||||
|
|
||||||
|
// Process the map contents
|
||||||
|
p.pushObject(newMap)
|
||||||
|
|
||||||
|
// Copy values from scanned map to our object
|
||||||
|
for k, v := range mapValue {
|
||||||
|
p.currentObject[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
p.popObject()
|
||||||
|
} else {
|
||||||
|
// Direct storage for arrays
|
||||||
|
p.currentObject[name] = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return p.Error("expected '=' or '{' after name")
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Error("unexpected character")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipErr != nil && skipErr != io.EOF {
|
||||||
|
return skipErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function
|
||||||
|
func isLetter(r rune) bool {
|
||||||
|
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
|
||||||
|
}
|
484
scanner.go
Normal file
484
scanner.go
Normal file
|
@ -0,0 +1,484 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scanner handles the low-level parsing of the configuration format
|
||||||
|
type Scanner struct {
|
||||||
|
reader *bufio.Reader
|
||||||
|
line int // Current line number
|
||||||
|
col int // Current column position
|
||||||
|
buffer []rune
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScanner creates a new scanner with the given reader
|
||||||
|
func NewScanner(r io.Reader) *Scanner {
|
||||||
|
return &Scanner{
|
||||||
|
reader: bufio.NewReader(r),
|
||||||
|
line: 1, // Start at line 1
|
||||||
|
col: 0,
|
||||||
|
buffer: make([]rune, 0, 64),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadRune reads a single rune from the input
|
||||||
|
func (s *Scanner) ReadRune() (rune, int, error) {
|
||||||
|
r, i, err := s.reader.ReadRune()
|
||||||
|
if err == nil {
|
||||||
|
if r == '\n' {
|
||||||
|
s.line++
|
||||||
|
s.col = 0
|
||||||
|
} else {
|
||||||
|
s.col++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeekRune looks at the next rune without consuming it
|
||||||
|
func (s *Scanner) PeekRune() (rune, error) {
|
||||||
|
r, _, err := s.reader.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
s.reader.UnreadRune()
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error creates an error with line and column information
|
||||||
|
func (s *Scanner) Error(msg string) error {
|
||||||
|
return fmt.Errorf("line %d, column %d: %s", s.line, s.col, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipWhitespace skips whitespace characters
|
||||||
|
func (s *Scanner) SkipWhitespace() error {
|
||||||
|
for {
|
||||||
|
r, err := s.PeekRune()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !unicode.IsSpace(r) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, _, err = s.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// peekAndCheckRune checks if the next rune matches expected without consuming it
|
||||||
|
func (s *Scanner) peekAndCheckRune(expected rune) (bool, error) {
|
||||||
|
r, err := s.PeekRune()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return r == expected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeIfMatch consumes the next rune if it matches expected
|
||||||
|
func (s *Scanner) consumeIfMatch(expected rune) (bool, error) {
|
||||||
|
matches, err := s.peekAndCheckRune(expected)
|
||||||
|
if err != nil || !matches {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = s.ReadRune() // consume the rune
|
||||||
|
return err == nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanComment processes a comment
|
||||||
|
func (s *Scanner) ScanComment() error {
|
||||||
|
// Consume the first dash
|
||||||
|
_, _, err := s.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for second dash
|
||||||
|
r, _, err := s.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r != '-' {
|
||||||
|
return s.Error("invalid comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for block comment [[
|
||||||
|
r, err = s.PeekRune()
|
||||||
|
if err == nil && r == '[' {
|
||||||
|
_, _, _ = s.ReadRune() // consume first [
|
||||||
|
r, err = s.PeekRune()
|
||||||
|
if err == nil && r == '[' {
|
||||||
|
_, _, _ = s.ReadRune() // consume second [
|
||||||
|
return s.scanBlockComment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line comment
|
||||||
|
for {
|
||||||
|
r, _, err := s.ReadRune()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r == '\n' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanBlockComment processes a block comment
|
||||||
|
func (s *Scanner) scanBlockComment() error {
|
||||||
|
for {
|
||||||
|
r, _, err := s.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return s.Error("unclosed block comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == ']' {
|
||||||
|
r, err = s.PeekRune()
|
||||||
|
if err == nil && r == ']' {
|
||||||
|
_, _, _ = s.ReadRune() // consume second ]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanName reads a name identifier
|
||||||
|
func (s *Scanner) ScanName() (string, error) {
|
||||||
|
s.buffer = s.buffer[:0] // Reset buffer
|
||||||
|
|
||||||
|
// Read first character
|
||||||
|
r, _, err := s.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !unicode.IsLetter(r) {
|
||||||
|
return "", s.Error("name must start with letter")
|
||||||
|
}
|
||||||
|
s.buffer = append(s.buffer, r)
|
||||||
|
|
||||||
|
// Read rest of name
|
||||||
|
for {
|
||||||
|
r, err := s.PeekRune()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '_' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.buffer = append(s.buffer, r)
|
||||||
|
_, _, _ = s.ReadRune()
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(s.buffer), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processArrayElement processes a single array element
|
||||||
|
func (s *Scanner) processArrayElement() (any, error) {
|
||||||
|
r, err := s.PeekRune()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle identifier-like elements
|
||||||
|
if unicode.IsLetter(r) {
|
||||||
|
name, err := s.ScanName()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert to appropriate type
|
||||||
|
convertedValue, err := s.ConvertValue(name)
|
||||||
|
if err == nil {
|
||||||
|
return convertedValue, nil
|
||||||
|
}
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle other element types (strings, numbers, etc.)
|
||||||
|
return s.ScanValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// processMapEntry processes a key-value pair in a map
|
||||||
|
func (s *Scanner) processMapEntry() (string, any, bool, error) {
|
||||||
|
name, err := s.ScanName()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.SkipWhitespace()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for equals sign
|
||||||
|
isEquals, err := s.consumeIfMatch('=')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return "", nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isEquals {
|
||||||
|
value, err := s.ScanValue()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, false, err
|
||||||
|
}
|
||||||
|
return name, value, true, nil // true indicates this is a map entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for opening brace (nested map/array)
|
||||||
|
isBrace, err := s.peekAndCheckRune('{')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return "", nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isBrace {
|
||||||
|
value, err := s.ScanValue()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, false, err
|
||||||
|
}
|
||||||
|
return name, value, true, nil // true indicates this is a map entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// If neither equals nor brace, it's an array element (name as string)
|
||||||
|
return name, name, false, nil // false indicates this is not a map entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanValue processes and returns a value from the config
|
||||||
|
func (s *Scanner) ScanValue() (any, error) {
|
||||||
|
err := s.SkipWhitespace()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := s.PeekRune()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's an array/map
|
||||||
|
if r == '{' {
|
||||||
|
return s.ScanArrayOrMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a quoted string
|
||||||
|
if r == '"' {
|
||||||
|
return s.ScanString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, treat it as a simple value
|
||||||
|
var value []rune
|
||||||
|
for {
|
||||||
|
r, err := s.PeekRune()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if unicode.IsSpace(r) || r == '}' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
value = append(value, r)
|
||||||
|
_, _, _ = s.ReadRune()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil, s.Error("empty value")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert value to appropriate type
|
||||||
|
return s.ConvertValue(string(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanArrayOrMap processes arrays and maps
|
||||||
|
func (s *Scanner) ScanArrayOrMap() (any, error) {
|
||||||
|
// Consume opening brace
|
||||||
|
_, _, err := s.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a new map to store contents
|
||||||
|
contents := make(map[string]any)
|
||||||
|
// And a slice to track array elements
|
||||||
|
var arrayElements []any
|
||||||
|
isArray := true
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := s.SkipWhitespace()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := s.PeekRune()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, s.Error("unclosed array/map")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for closing brace
|
||||||
|
if r == '}' {
|
||||||
|
_, _, _ = s.ReadRune() // consume the closing brace
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle comments inside arrays/maps
|
||||||
|
if r == '-' {
|
||||||
|
err = s.ScanComment()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we find a named property, it might be a map entry
|
||||||
|
if unicode.IsLetter(r) {
|
||||||
|
name, value, isMapEntry, err := s.processMapEntry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMapEntry {
|
||||||
|
// It's a key-value pair for a map
|
||||||
|
isArray = false
|
||||||
|
contents[name] = value
|
||||||
|
} else {
|
||||||
|
// It's an array element
|
||||||
|
arrayElements = append(arrayElements, value)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle array elements that start with quotes, numbers, etc.
|
||||||
|
value, err := s.processArrayElement()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
arrayElements = append(arrayElements, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for array/map distinction and return appropriate result
|
||||||
|
if isArray && len(contents) == 0 {
|
||||||
|
return arrayElements, nil
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanString reads a quoted string
|
||||||
|
func (s *Scanner) ScanString() (any, error) {
|
||||||
|
// Consume opening quote
|
||||||
|
_, _, err := s.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.Grow(64) // Preallocate with reasonable capacity
|
||||||
|
|
||||||
|
for {
|
||||||
|
r, _, err := s.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return nil, s.Error("unterminated string")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == '"' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle escape sequences
|
||||||
|
if r == '\\' {
|
||||||
|
escaped, _, err := s.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch escaped {
|
||||||
|
case '"':
|
||||||
|
builder.WriteRune('"')
|
||||||
|
case '\\':
|
||||||
|
builder.WriteRune('\\')
|
||||||
|
case 'n':
|
||||||
|
builder.WriteRune('\n')
|
||||||
|
case 't':
|
||||||
|
builder.WriteRune('\t')
|
||||||
|
default:
|
||||||
|
builder.WriteRune('\\')
|
||||||
|
builder.WriteRune(escaped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertValue converts string values to their appropriate types
|
||||||
|
func (s *Scanner) ConvertValue(value string) (any, error) {
|
||||||
|
// Fast path for booleans
|
||||||
|
if value == "true" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if value == "false" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early exit for empty values
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil, errors.New("empty value")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for number type in one pass
|
||||||
|
isNegative := value[0] == '-'
|
||||||
|
startIdx := 0
|
||||||
|
if isNegative {
|
||||||
|
if len(value) == 1 {
|
||||||
|
return nil, errors.New("invalid value: -")
|
||||||
|
}
|
||||||
|
startIdx = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
hasDot := false
|
||||||
|
for i := startIdx; i < len(value); i++ {
|
||||||
|
if value[i] == '.' {
|
||||||
|
if hasDot {
|
||||||
|
return nil, errors.New("invalid number format")
|
||||||
|
}
|
||||||
|
hasDot = true
|
||||||
|
} else if value[i] < '0' || value[i] > '9' {
|
||||||
|
return nil, errors.New("invalid value format: " + value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process as integer or float based on presence of decimal
|
||||||
|
if !hasDot {
|
||||||
|
return strconv.ParseInt(value, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float (ensure not ending with dot)
|
||||||
|
if value[len(value)-1] != '.' {
|
||||||
|
return strconv.ParseFloat(value, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("invalid value format: " + value)
|
||||||
|
}
|
316
tests/config_test.go
Normal file
316
tests/config_test.go
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
config "git.sharkk.net/Go/Config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBasicKeyValuePairs(t *testing.T) {
|
||||||
|
input := `
|
||||||
|
boolTrue = true
|
||||||
|
boolFalse = false
|
||||||
|
integer = 42
|
||||||
|
negativeInt = -10
|
||||||
|
floatValue = 3.14
|
||||||
|
negativeFloat = -2.5
|
||||||
|
stringValue = "hello world"
|
||||||
|
`
|
||||||
|
config, err := config.Load(strings.NewReader(input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify boolean values
|
||||||
|
boolTrue, err := config.GetBool("boolTrue")
|
||||||
|
if err != nil || boolTrue != true {
|
||||||
|
t.Errorf("expected boolTrue=true, got %v, err: %v", boolTrue, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
boolFalse, err := config.GetBool("boolFalse")
|
||||||
|
if err != nil || boolFalse != false {
|
||||||
|
t.Errorf("expected boolFalse=false, got %v, err: %v", boolFalse, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify integer values
|
||||||
|
integer, err := config.GetInt("integer")
|
||||||
|
if err != nil || integer != 42 {
|
||||||
|
t.Errorf("expected integer=42, got %v, err: %v", integer, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
negativeInt, err := config.GetInt("negativeInt")
|
||||||
|
if err != nil || negativeInt != -10 {
|
||||||
|
t.Errorf("expected negativeInt=-10, got %v, err: %v", negativeInt, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify float values
|
||||||
|
floatValue, err := config.GetFloat("floatValue")
|
||||||
|
if err != nil || floatValue != 3.14 {
|
||||||
|
t.Errorf("expected floatValue=3.14, got %v, err: %v", floatValue, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
negativeFloat, err := config.GetFloat("negativeFloat")
|
||||||
|
if err != nil || negativeFloat != -2.5 {
|
||||||
|
t.Errorf("expected negativeFloat=-2.5, got %v, err: %v", negativeFloat, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify string values
|
||||||
|
stringValue, err := config.GetString("stringValue")
|
||||||
|
if err != nil || stringValue != "hello world" {
|
||||||
|
t.Errorf("expected stringValue=\"hello world\", got %v, err: %v", stringValue, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify missing key handling
|
||||||
|
_, err = config.GetString("nonExistentKey")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error for non-existent key, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test GetOr with defaults
|
||||||
|
defaultBool := config.GetOr("nonExistentKey", true).(bool)
|
||||||
|
if defaultBool != true {
|
||||||
|
t.Errorf("expected GetOr to return default true, got %v", defaultBool)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultString := config.GetOr("nonExistentKey", "default").(string)
|
||||||
|
if defaultString != "default" {
|
||||||
|
t.Errorf("expected GetOr to return 'default', got %v", defaultString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComments(t *testing.T) {
|
||||||
|
input := `
|
||||||
|
-- This is a line comment
|
||||||
|
key1 = "value1"
|
||||||
|
|
||||||
|
--[[ This is a
|
||||||
|
block comment spanning
|
||||||
|
multiple lines ]]
|
||||||
|
key2 = "value2"
|
||||||
|
|
||||||
|
settings {
|
||||||
|
-- Comment inside a map
|
||||||
|
timeout = 30
|
||||||
|
--[[ Another block comment ]]
|
||||||
|
retries = 3
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
config, err := config.Load(strings.NewReader(input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that comments are properly ignored and values are parsed
|
||||||
|
val1, err := config.GetString("key1")
|
||||||
|
if err != nil || val1 != "value1" {
|
||||||
|
t.Errorf("expected key1=\"value1\", got %v, err: %v", val1, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val2, err := config.GetString("key2")
|
||||||
|
if err != nil || val2 != "value2" {
|
||||||
|
t.Errorf("expected key2=\"value2\", got %v, err: %v", val2, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify nested values after comments
|
||||||
|
timeout, err := config.GetInt("settings.timeout")
|
||||||
|
if err != nil || timeout != 30 {
|
||||||
|
t.Errorf("expected settings.timeout=30, got %v, err: %v", timeout, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
retries, err := config.GetInt("settings.retries")
|
||||||
|
if err != nil || retries != 3 {
|
||||||
|
t.Errorf("expected settings.retries=3, got %v, err: %v", retries, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArrays(t *testing.T) {
|
||||||
|
input := `
|
||||||
|
-- Simple array
|
||||||
|
fruits {
|
||||||
|
"apple"
|
||||||
|
"banana"
|
||||||
|
"cherry"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Array with equals sign
|
||||||
|
numbers = {
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Mixed types array
|
||||||
|
mixed {
|
||||||
|
"string"
|
||||||
|
42
|
||||||
|
true
|
||||||
|
3.14
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
config, err := config.Load(strings.NewReader(input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify simple array
|
||||||
|
fruits, err := config.GetArray("fruits")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get fruits array: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFruits := []any{"apple", "banana", "cherry"}
|
||||||
|
if !reflect.DeepEqual(fruits, expectedFruits) {
|
||||||
|
t.Errorf("expected fruits=%v, got %v", expectedFruits, fruits)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify array accessed with index notation
|
||||||
|
apple, err := config.GetString("fruits.0")
|
||||||
|
if err != nil || apple != "apple" {
|
||||||
|
t.Errorf("expected fruits.0=\"apple\", got %v, err: %v", apple, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify array with equals sign
|
||||||
|
numbers, err := config.GetArray("numbers")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get numbers array: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check array length
|
||||||
|
if len(numbers) != 5 {
|
||||||
|
t.Errorf("expected 5 numbers, got %d", len(numbers))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify first number
|
||||||
|
firstNumber, err := config.GetInt("numbers.0")
|
||||||
|
if err != nil || firstNumber != 1 {
|
||||||
|
t.Errorf("expected numbers.0=1, got %v, err: %v", firstNumber, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mixed types array
|
||||||
|
mixed, err := config.GetArray("mixed")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get mixed array: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mixed) != 4 {
|
||||||
|
t.Errorf("expected 4 items in mixed array, got %d", len(mixed))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check types in mixed array
|
||||||
|
stringVal, err := config.GetString("mixed.0")
|
||||||
|
if err != nil || stringVal != "string" {
|
||||||
|
t.Errorf("expected mixed.0=\"string\", got %v, err: %v", stringVal, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
intVal, err := config.GetInt("mixed.1")
|
||||||
|
if err != nil || intVal != 42 {
|
||||||
|
t.Errorf("expected mixed.1=42, got %v, err: %v", intVal, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
boolVal, err := config.GetBool("mixed.2")
|
||||||
|
if err != nil || boolVal != true {
|
||||||
|
t.Errorf("expected mixed.2=true, got %v, err: %v", boolVal, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
floatVal, err := config.GetFloat("mixed.3")
|
||||||
|
if err != nil || floatVal != 3.14 {
|
||||||
|
t.Errorf("expected mixed.3=3.14, got %v, err: %v", floatVal, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaps(t *testing.T) {
|
||||||
|
input := `
|
||||||
|
-- Simple map
|
||||||
|
server {
|
||||||
|
host = "localhost"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Map with equals sign
|
||||||
|
database = {
|
||||||
|
username = "admin"
|
||||||
|
password = "secret"
|
||||||
|
enabled = true
|
||||||
|
maxConnections = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Nested maps
|
||||||
|
application {
|
||||||
|
name = "MyApp"
|
||||||
|
version = "1.0.0"
|
||||||
|
settings {
|
||||||
|
theme = "dark"
|
||||||
|
notifications = true
|
||||||
|
logging {
|
||||||
|
level = "info"
|
||||||
|
file = "app.log"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
config, err := config.Load(strings.NewReader(input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify simple map
|
||||||
|
serverMap, err := config.GetMap("server")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get server map: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(serverMap) != 2 {
|
||||||
|
t.Errorf("expected 2 items in server map, got %d", len(serverMap))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify dot notation access
|
||||||
|
host, err := config.GetString("server.host")
|
||||||
|
if err != nil || host != "localhost" {
|
||||||
|
t.Errorf("expected server.host=\"localhost\", got %v, err: %v", host, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := config.GetInt("server.port")
|
||||||
|
if err != nil || port != 8080 {
|
||||||
|
t.Errorf("expected server.port=8080, got %v, err: %v", port, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify map with equals sign
|
||||||
|
dbUser, err := config.GetString("database.username")
|
||||||
|
if err != nil || dbUser != "admin" {
|
||||||
|
t.Errorf("expected database.username=\"admin\", got %v, err: %v", dbUser, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbEnabled, err := config.GetBool("database.enabled")
|
||||||
|
if err != nil || dbEnabled != true {
|
||||||
|
t.Errorf("expected database.enabled=true, got %v, err: %v", dbEnabled, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify deeply nested maps
|
||||||
|
appName, err := config.GetString("application.name")
|
||||||
|
if err != nil || appName != "MyApp" {
|
||||||
|
t.Errorf("expected application.name=\"MyApp\", got %v, err: %v", appName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
theme, err := config.GetString("application.settings.theme")
|
||||||
|
if err != nil || theme != "dark" {
|
||||||
|
t.Errorf("expected application.settings.theme=\"dark\", got %v, err: %v", theme, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logLevel, err := config.GetString("application.settings.logging.level")
|
||||||
|
if err != nil || logLevel != "info" {
|
||||||
|
t.Errorf("expected application.settings.logging.level=\"info\", got %v, err: %v", logLevel, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test non-existent nested paths
|
||||||
|
_, err = config.GetString("application.settings.nonexistent")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error for non-existent nested key, got nil")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user