Compare commits
4 Commits
44827e16d4
...
80500963b4
Author | SHA1 | Date | |
---|---|---|---|
80500963b4 | |||
41b82c71cd | |||
b334b09efa | |||
456854246b |
14
README.md
14
README.md
|
@ -17,9 +17,9 @@ A lightweight, intuitive configuration parser for Go applications with a clean,
|
||||||
This parser uses a clean, minimal syntax that's easy to read and write:
|
This parser uses a clean, minimal syntax that's easy to read and write:
|
||||||
|
|
||||||
```
|
```
|
||||||
host = "localhost"
|
host "localhost"
|
||||||
port = 8080
|
port 8080
|
||||||
debug = true
|
debug true
|
||||||
|
|
||||||
allowed_ips {
|
allowed_ips {
|
||||||
"192.168.1.1"
|
"192.168.1.1"
|
||||||
|
@ -28,11 +28,11 @@ allowed_ips {
|
||||||
}
|
}
|
||||||
|
|
||||||
database {
|
database {
|
||||||
host = "db.example.com"
|
host "db.example.com"
|
||||||
port = 5432
|
port 5432
|
||||||
credentials {
|
credentials {
|
||||||
username = "admin"
|
username "admin"
|
||||||
password = "secure123"
|
password "secure123"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -11,618 +10,12 @@ import (
|
||||||
"gopkg.in/yaml.v3"
|
"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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value Retrieval Benchmarks for Custom Config Format
|
|
||||||
func BenchmarkRetrieveSimpleValues(b *testing.B) {
|
func BenchmarkRetrieveSimpleValues(b *testing.B) {
|
||||||
configData := `
|
configData := `
|
||||||
host = "localhost"
|
host "localhost"
|
||||||
port = 8080
|
port 8080
|
||||||
debug = true
|
debug true
|
||||||
timeout = 30
|
timeout 30
|
||||||
`
|
`
|
||||||
|
|
||||||
// Parse once before benchmarking retrieval
|
// Parse once before benchmarking retrieval
|
||||||
|
@ -663,19 +56,19 @@ func BenchmarkRetrieveSimpleValues(b *testing.B) {
|
||||||
func BenchmarkRetrieveNestedValues(b *testing.B) {
|
func BenchmarkRetrieveNestedValues(b *testing.B) {
|
||||||
configData := `
|
configData := `
|
||||||
app {
|
app {
|
||||||
name = "TestApp"
|
name "TestApp"
|
||||||
version = "1.0.0"
|
version "1.0.0"
|
||||||
settings {
|
settings {
|
||||||
enableLogging = true
|
enableLogging true
|
||||||
maxConnections = 100
|
maxConnections 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
database {
|
database {
|
||||||
host = "db.example.com"
|
host "db.example.com"
|
||||||
port = 5432
|
port 5432
|
||||||
credentials {
|
credentials {
|
||||||
username = "admin"
|
username "admin"
|
||||||
password = "secure123"
|
password "secure123"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -715,13 +108,13 @@ func BenchmarkRetrieveNestedValues(b *testing.B) {
|
||||||
|
|
||||||
func BenchmarkRetrieveArrayValues(b *testing.B) {
|
func BenchmarkRetrieveArrayValues(b *testing.B) {
|
||||||
configData := `
|
configData := `
|
||||||
features = {
|
features {
|
||||||
"authentication"
|
"authentication"
|
||||||
"authorization"
|
"authorization"
|
||||||
"reporting"
|
"reporting"
|
||||||
"analytics"
|
"analytics"
|
||||||
}
|
}
|
||||||
numbers = {
|
numbers {
|
||||||
1
|
1
|
||||||
2
|
2
|
||||||
3
|
3
|
||||||
|
@ -767,19 +160,19 @@ func BenchmarkRetrieveArrayValues(b *testing.B) {
|
||||||
func BenchmarkRetrieveMixedValues(b *testing.B) {
|
func BenchmarkRetrieveMixedValues(b *testing.B) {
|
||||||
configData := `
|
configData := `
|
||||||
app {
|
app {
|
||||||
name = "TestApp"
|
name "TestApp"
|
||||||
version = "1.0.0"
|
version "1.0.0"
|
||||||
environments = {
|
environments {
|
||||||
"development"
|
"development"
|
||||||
"testing"
|
"testing"
|
||||||
"production"
|
"production"
|
||||||
}
|
}
|
||||||
limits {
|
limits {
|
||||||
requests = 1000
|
requests 1000
|
||||||
connections = 100
|
connections 100
|
||||||
timeouts {
|
timeouts {
|
||||||
read = 5
|
read 5
|
||||||
write = 10
|
write 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1065,17 +458,17 @@ func BenchmarkComplexValueRetrieval(b *testing.B) {
|
||||||
// Setup complex config with deep nesting and arrays for all formats
|
// Setup complex config with deep nesting and arrays for all formats
|
||||||
customConfig := `
|
customConfig := `
|
||||||
app {
|
app {
|
||||||
name = "TestApp"
|
name "TestApp"
|
||||||
version = "1.0.0"
|
version "1.0.0"
|
||||||
settings {
|
settings {
|
||||||
enableLogging = true
|
enableLogging true
|
||||||
maxConnections = 100
|
maxConnections 100
|
||||||
timeouts {
|
timeouts {
|
||||||
read = 5
|
read 5
|
||||||
write = 10
|
write 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
environments = {
|
environments {
|
||||||
"development"
|
"development"
|
||||||
"testing"
|
"testing"
|
||||||
"production"
|
"production"
|
617
bench/parse_test.go
Normal file
617
bench/parse_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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
511
config.go
511
config.go
|
@ -4,20 +4,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseState represents a parsing level state
|
|
||||||
type ParseState struct {
|
|
||||||
object map[string]any
|
|
||||||
arrayElements []any
|
|
||||||
isArray bool
|
|
||||||
currentKey string
|
|
||||||
expectValue bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config holds a single hierarchical structure like JSON 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
|
||||||
scanner *Scanner
|
scanner *Scanner
|
||||||
currentObject map[string]any
|
currentObject map[string]any
|
||||||
stack []map[string]any
|
stack []map[string]any
|
||||||
|
@ -26,45 +19,58 @@ type Config struct {
|
||||||
|
|
||||||
// NewConfig creates a new empty config
|
// NewConfig creates a new empty config
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
|
dataRef := GetMap()
|
||||||
|
data := *dataRef
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
data: make(map[string]any, 16), // Pre-allocate with expected capacity
|
data: data,
|
||||||
|
dataRef: dataRef,
|
||||||
stack: make([]map[string]any, 0, 8),
|
stack: make([]map[string]any, 0, 8),
|
||||||
}
|
}
|
||||||
cfg.currentObject = cfg.data
|
cfg.currentObject = cfg.data
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release frees any resources and returns them to pools
|
||||||
|
func (c *Config) Release() {
|
||||||
|
if c.scanner != nil {
|
||||||
|
ReleaseScanner(c.scanner)
|
||||||
|
c.scanner = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.dataRef != nil {
|
||||||
|
PutMap(c.dataRef)
|
||||||
|
c.data = nil
|
||||||
|
c.dataRef = nil
|
||||||
|
}
|
||||||
|
c.currentObject = nil
|
||||||
|
c.stack = nil
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the dot-notation path manually
|
if !strings.Contains(key, ".") {
|
||||||
var start, i int
|
if val, ok := c.data[key]; ok {
|
||||||
var current any = c.data
|
return val, nil
|
||||||
|
}
|
||||||
for i = 0; i < len(key); i++ {
|
return nil, fmt.Errorf("key %s not found", key)
|
||||||
if key[i] == '.' || i == len(key)-1 {
|
|
||||||
end := i
|
|
||||||
if i == len(key)-1 && key[i] != '.' {
|
|
||||||
end = i + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
part := key[start:end]
|
parts := strings.Split(key, ".")
|
||||||
|
current := any(c.data)
|
||||||
// Handle current node based on its type
|
for _, part := range parts {
|
||||||
switch node := current.(type) {
|
switch node := current.(type) {
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
// Simple map lookup
|
var exists bool
|
||||||
val, ok := node[part]
|
current, exists = node[part]
|
||||||
if !ok {
|
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:
|
||||||
// Must be numeric index
|
|
||||||
index, err := strconv.Atoi(part)
|
index, err := strconv.Atoi(part)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid array index: %s", part)
|
return nil, fmt.Errorf("invalid array index: %s", part)
|
||||||
|
@ -73,22 +79,10 @@ func (c *Config) Get(key string) (any, error) {
|
||||||
return nil, fmt.Errorf("array index out of bounds: %d", index)
|
return nil, fmt.Errorf("array index out of bounds: %d", index)
|
||||||
}
|
}
|
||||||
current = node[index]
|
current = node[index]
|
||||||
|
|
||||||
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 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
|
return current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,8 +197,6 @@ func (c *Config) GetMap(key string) (map[string]any, error) {
|
||||||
return nil, fmt.Errorf("value for key %s is not a map", key)
|
return nil, fmt.Errorf("value for key %s is not a map", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Parser Methods (integrated into Config) ---
|
|
||||||
|
|
||||||
// Error creates an error with line information from the current token
|
// Error creates an error with line information from the current token
|
||||||
func (c *Config) Error(msg string) error {
|
func (c *Config) Error(msg string) error {
|
||||||
return fmt.Errorf("line %d, column %d: %s",
|
return fmt.Errorf("line %d, column %d: %s",
|
||||||
|
@ -257,244 +249,198 @@ func (c *Config) parseContent() error {
|
||||||
|
|
||||||
// We expect top level entries to be names
|
// We expect top level entries to be names
|
||||||
if token.Type != TokenName {
|
if token.Type != TokenName {
|
||||||
return c.Error("expected name at top level")
|
return c.Error(fmt.Sprintf("expected name at top level, got token type %v", token.Type))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the property name - copy to create a stable key
|
// Get the property name - copy to create a stable key
|
||||||
nameBytes := token.Value
|
nameBytes := GetByteSlice()
|
||||||
name := string(nameBytes)
|
*nameBytes = append((*nameBytes)[:0], token.Value...)
|
||||||
|
name := string(*nameBytes)
|
||||||
|
PutByteSlice(nameBytes)
|
||||||
|
|
||||||
// Get the next token (should be = or {)
|
// Get the next token
|
||||||
token, err = c.nextToken()
|
nextToken, err := c.nextToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
// EOF after name - store as empty string
|
||||||
|
c.currentObject[name] = ""
|
||||||
|
break
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var value any
|
var value any
|
||||||
|
|
||||||
if token.Type == TokenEquals {
|
if nextToken.Type == TokenOpenBrace {
|
||||||
// It's a standard key=value assignment
|
// It's a nested object/array
|
||||||
value, err = c.parseValue()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if token.Type == TokenOpenBrace {
|
|
||||||
// It's a map/array without '='
|
|
||||||
value, err = c.parseObject()
|
value, err = c.parseObject()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return c.Error("expected '=' or '{' after name")
|
// It's a simple value
|
||||||
|
value = c.tokenToValue(nextToken)
|
||||||
|
|
||||||
|
// Check for potential nested object - look ahead
|
||||||
|
lookAhead, nextErr := c.nextToken()
|
||||||
|
if nextErr == nil && lookAhead.Type == TokenOpenBrace {
|
||||||
|
// It's a complex object that follows a value
|
||||||
|
nestedValue, err := c.parseObject()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the previous simple value in a map to add to the object
|
||||||
|
if mapValue, ok := nestedValue.(map[string]any); ok {
|
||||||
|
// Create a new map value with both the simple value and the map
|
||||||
|
mapRef := GetMap()
|
||||||
|
newMap := *mapRef
|
||||||
|
for k, v := range mapValue {
|
||||||
|
newMap[k] = v
|
||||||
|
}
|
||||||
|
newMap["value"] = value // Store simple value under "value" key
|
||||||
|
value = newMap
|
||||||
|
}
|
||||||
|
} else if nextErr == nil && lookAhead.Type != TokenEOF {
|
||||||
|
// Put the token back if it's not EOF
|
||||||
|
c.scanner.UnreadToken(lookAhead)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the value in the config
|
// Store the value in the config
|
||||||
if mapValue, ok := value.(map[string]any); ok {
|
|
||||||
// Add an entry in current object
|
|
||||||
newMap := make(map[string]any, 8) // Pre-allocate with capacity
|
|
||||||
c.currentObject[name] = newMap
|
|
||||||
|
|
||||||
// Process the map contents
|
|
||||||
c.stack = append(c.stack, c.currentObject)
|
|
||||||
c.currentObject = newMap
|
|
||||||
|
|
||||||
// Copy values from scanned map to our object
|
|
||||||
for k, v := range mapValue {
|
|
||||||
c.currentObject[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore parent object
|
|
||||||
n := len(c.stack)
|
|
||||||
if n > 0 {
|
|
||||||
c.currentObject = c.stack[n-1]
|
|
||||||
c.stack = c.stack[:n-1]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Direct storage for primitives and arrays
|
|
||||||
c.currentObject[name] = value
|
c.currentObject[name] = value
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseValue parses a value after an equals sign
|
|
||||||
func (c *Config) parseValue() (any, error) {
|
|
||||||
token, err := c.nextToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch token.Type {
|
|
||||||
case TokenString:
|
|
||||||
// Copy the value for string stability
|
|
||||||
return string(token.Value), nil
|
|
||||||
|
|
||||||
case TokenNumber:
|
|
||||||
strValue := string(token.Value)
|
|
||||||
for i := 0; i < len(strValue); i++ {
|
|
||||||
if strValue[i] == '.' {
|
|
||||||
// It's a float
|
|
||||||
val, err := strconv.ParseFloat(strValue, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.Error(fmt.Sprintf("invalid float: %s", strValue))
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// It's an integer
|
|
||||||
val, err := strconv.ParseInt(strValue, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.Error(fmt.Sprintf("invalid integer: %s", strValue))
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
|
|
||||||
case TokenBoolean:
|
|
||||||
return bytesEqual(token.Value, []byte("true")), nil
|
|
||||||
|
|
||||||
case TokenOpenBrace:
|
|
||||||
// It's a map or array
|
|
||||||
return c.parseObject()
|
|
||||||
|
|
||||||
case TokenName:
|
|
||||||
// Treat as a string value - copy to create a stable string
|
|
||||||
return string(token.Value), nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, c.Error(fmt.Sprintf("unexpected token: %v", token.Type))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseObject parses a map or array
|
// parseObject parses a map or array
|
||||||
func (c *Config) parseObject() (any, error) {
|
func (c *Config) parseObject() (any, error) {
|
||||||
// Initialize stack with first state
|
// Default to treating as an array until we see a name
|
||||||
stack := []*ParseState{{
|
isArray := true
|
||||||
object: make(map[string]any, 8),
|
arrayRef := GetArray()
|
||||||
arrayElements: make([]any, 0, 8),
|
arrayElements := *arrayRef
|
||||||
isArray: true,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for len(stack) > 0 {
|
mapRef := GetMap()
|
||||||
// Get current state from top of stack
|
objectElements := *mapRef
|
||||||
current := stack[len(stack)-1]
|
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if !isArray {
|
||||||
|
PutArray(arrayRef) // We didn't use the array
|
||||||
|
} else {
|
||||||
|
PutMap(mapRef) // We didn't use the map
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
token, err := c.nextToken()
|
token, err := c.nextToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, fmt.Errorf("unexpected EOF in object/array")
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle closing brace - finish current object/array
|
// Handle closing brace - finish current object/array
|
||||||
if token.Type == TokenCloseBrace {
|
if token.Type == TokenCloseBrace {
|
||||||
// Determine result based on what we've collected
|
if isArray && len(arrayElements) > 0 {
|
||||||
var result any
|
result := arrayElements
|
||||||
if current.isArray && len(current.object) == 0 {
|
// Don't release the array, transfer ownership
|
||||||
result = current.arrayElements
|
*arrayRef = nil // Detach from pool reference
|
||||||
} else {
|
|
||||||
result = current.object
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop the stack
|
|
||||||
stack = stack[:len(stack)-1]
|
|
||||||
|
|
||||||
// If stack is empty, we're done with the root object
|
|
||||||
if len(stack) == 0 {
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
result := objectElements
|
||||||
// Otherwise, add result to parent
|
// Don't release the map, transfer ownership
|
||||||
parent := stack[len(stack)-1]
|
*mapRef = nil // Detach from pool reference
|
||||||
if parent.expectValue {
|
return result, nil
|
||||||
parent.object[parent.currentKey] = result
|
|
||||||
parent.expectValue = false
|
|
||||||
} else {
|
|
||||||
parent.arrayElements = append(parent.arrayElements, result)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tokens based on type
|
// Handle tokens based on type
|
||||||
switch token.Type {
|
switch token.Type {
|
||||||
case TokenName:
|
case TokenName:
|
||||||
name := string(token.Value)
|
// Copy token value to create a stable key
|
||||||
|
keyBytes := GetByteSlice()
|
||||||
|
*keyBytes = append((*keyBytes)[:0], token.Value...)
|
||||||
|
key := string(*keyBytes)
|
||||||
|
PutByteSlice(keyBytes)
|
||||||
|
|
||||||
// Look ahead to determine context
|
// Look ahead to see what follows
|
||||||
nextToken, err := c.nextToken()
|
nextToken, err := c.nextToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
// EOF after key - store as empty value
|
||||||
|
objectElements[key] = ""
|
||||||
|
isArray = false
|
||||||
|
return objectElements, nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if nextToken.Type == TokenEquals {
|
if nextToken.Type == TokenOpenBrace {
|
||||||
|
// Nested object
|
||||||
|
isArray = false // If we see a key, it's a map
|
||||||
|
nestedValue, err := c.parseObject()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
objectElements[key] = nestedValue
|
||||||
|
} else {
|
||||||
// Key-value pair
|
// Key-value pair
|
||||||
current.isArray = false
|
isArray = false // If we see a key, it's a map
|
||||||
current.currentKey = name
|
value := c.tokenToValue(nextToken)
|
||||||
current.expectValue = true
|
objectElements[key] = value
|
||||||
|
|
||||||
// Parse the value
|
// Check if there's an object following
|
||||||
valueToken, err := c.nextToken()
|
lookAhead, nextErr := c.nextToken()
|
||||||
|
if nextErr == nil && lookAhead.Type == TokenOpenBrace {
|
||||||
|
// Nested object after value
|
||||||
|
nestedValue, err := c.parseObject()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if valueToken.Type == TokenOpenBrace {
|
// Check if we need to convert the value to a map
|
||||||
// Push new state for nested object/array
|
if mapValue, ok := nestedValue.(map[string]any); ok {
|
||||||
newState := &ParseState{
|
// Create a combined map
|
||||||
object: make(map[string]any, 8),
|
combinedMapRef := GetMap()
|
||||||
arrayElements: make([]any, 0, 8),
|
combinedMap := *combinedMapRef
|
||||||
isArray: true,
|
for k, v := range mapValue {
|
||||||
|
combinedMap[k] = v
|
||||||
}
|
}
|
||||||
stack = append(stack, newState)
|
combinedMap["value"] = value
|
||||||
} else {
|
objectElements[key] = combinedMap
|
||||||
// Handle primitive value
|
|
||||||
value := c.tokenToValue(valueToken)
|
|
||||||
current.object[name] = value
|
|
||||||
}
|
}
|
||||||
} else if nextToken.Type == TokenOpenBrace {
|
} else if nextErr == nil && lookAhead.Type != TokenEOF && lookAhead.Type != TokenCloseBrace {
|
||||||
// Nested object with name
|
c.scanner.UnreadToken(lookAhead)
|
||||||
current.isArray = false
|
} else if nextErr == nil && lookAhead.Type == TokenCloseBrace {
|
||||||
current.currentKey = name
|
// We found the closing brace - unread it so it's handled by the main loop
|
||||||
current.expectValue = true
|
c.scanner.UnreadToken(lookAhead)
|
||||||
|
|
||||||
// Push new state for nested object
|
|
||||||
newState := &ParseState{
|
|
||||||
object: make(map[string]any, 8),
|
|
||||||
arrayElements: make([]any, 0, 8),
|
|
||||||
isArray: true,
|
|
||||||
}
|
}
|
||||||
stack = append(stack, newState)
|
|
||||||
} else {
|
|
||||||
// Array element
|
|
||||||
c.scanner.UnreadToken(nextToken)
|
|
||||||
|
|
||||||
// Convert name to appropriate type
|
|
||||||
value := c.convertNameValue(name)
|
|
||||||
current.arrayElements = append(current.arrayElements, value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case TokenString, TokenNumber, TokenBoolean:
|
case TokenString, TokenNumber, TokenBoolean:
|
||||||
|
// Array element
|
||||||
value := c.tokenToValue(token)
|
value := c.tokenToValue(token)
|
||||||
|
arrayElements = append(arrayElements, value)
|
||||||
if current.expectValue {
|
|
||||||
current.object[current.currentKey] = value
|
|
||||||
current.expectValue = false
|
|
||||||
} else {
|
|
||||||
current.arrayElements = append(current.arrayElements, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
case TokenOpenBrace:
|
case TokenOpenBrace:
|
||||||
// New nested object/array
|
// Nested object/array
|
||||||
newState := &ParseState{
|
nestedValue, err := c.parseObject()
|
||||||
object: make(map[string]any, 8),
|
if err != nil {
|
||||||
arrayElements: make([]any, 0, 8),
|
return nil, err
|
||||||
isArray: true,
|
}
|
||||||
|
|
||||||
|
if isArray {
|
||||||
|
arrayElements = append(arrayElements, nestedValue)
|
||||||
|
} else {
|
||||||
|
// If we're in an object context, this is an error
|
||||||
|
return nil, c.Error("unexpected nested object without a key")
|
||||||
}
|
}
|
||||||
stack = append(stack, newState)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, c.Error(fmt.Sprintf("unexpected token: %v", token.Type))
|
return nil, c.Error(fmt.Sprintf("unexpected token type: %v", token.Type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("unexpected end of parsing")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load parses a config from a reader
|
// Load parses a config from a reader
|
||||||
|
@ -503,13 +449,80 @@ func Load(r io.Reader) (*Config, error) {
|
||||||
err := config.Parse(r)
|
err := config.Parse(r)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
config.Release()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// tokenToValue converts a token to a Go value, preserving byte slices until final conversion
|
||||||
|
func (c *Config) tokenToValue(token Token) any {
|
||||||
|
switch token.Type {
|
||||||
|
case TokenString:
|
||||||
|
// Convert to string using pooled buffer
|
||||||
|
valueBytes := GetByteSlice()
|
||||||
|
*valueBytes = append((*valueBytes)[:0], token.Value...)
|
||||||
|
result := string(*valueBytes)
|
||||||
|
PutByteSlice(valueBytes)
|
||||||
|
return result
|
||||||
|
|
||||||
|
case TokenNumber:
|
||||||
|
// Parse number
|
||||||
|
valueStr := string(token.Value)
|
||||||
|
if containsChar(token.Value, '.') {
|
||||||
|
// Float
|
||||||
|
val, _ := strconv.ParseFloat(valueStr, 64)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
// Integer
|
||||||
|
val, _ := strconv.ParseInt(valueStr, 10, 64)
|
||||||
|
return val
|
||||||
|
|
||||||
|
case TokenBoolean:
|
||||||
|
return bytesEqual(token.Value, []byte("true"))
|
||||||
|
|
||||||
|
case TokenName:
|
||||||
|
// Check if name is a special value
|
||||||
|
valueBytes := GetByteSlice()
|
||||||
|
*valueBytes = append((*valueBytes)[:0], token.Value...)
|
||||||
|
|
||||||
|
if bytesEqual(*valueBytes, []byte("true")) {
|
||||||
|
PutByteSlice(valueBytes)
|
||||||
|
return true
|
||||||
|
} else if bytesEqual(*valueBytes, []byte("false")) {
|
||||||
|
PutByteSlice(valueBytes)
|
||||||
|
return false
|
||||||
|
} else if isDigitOrMinus((*valueBytes)[0]) {
|
||||||
|
// Try to convert to number
|
||||||
|
valueStr := string(*valueBytes)
|
||||||
|
PutByteSlice(valueBytes)
|
||||||
|
|
||||||
|
if containsChar(token.Value, '.') {
|
||||||
|
val, err := strconv.ParseFloat(valueStr, 64)
|
||||||
|
if err == nil {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val, err := strconv.ParseInt(valueStr, 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return valueStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to string
|
||||||
|
result := string(*valueBytes)
|
||||||
|
PutByteSlice(valueBytes)
|
||||||
|
return result
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
func isLetter(b byte) bool {
|
func isLetter(b byte) bool {
|
||||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
|
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
|
||||||
|
@ -519,77 +532,27 @@ func isDigit(b byte) bool {
|
||||||
return b >= '0' && b <= '9'
|
return b >= '0' && b <= '9'
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseNumber converts a string to a number (int64 or float64)
|
func isDigitOrMinus(b byte) bool {
|
||||||
func ParseNumber(s string) (any, error) {
|
return isDigit(b) || b == '-'
|
||||||
// Check if it has a decimal point
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
if s[i] == '.' {
|
|
||||||
// It's a float
|
|
||||||
return strconv.ParseFloat(s, 64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// It's an integer
|
|
||||||
return strconv.ParseInt(s, 10, 64)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// bytesEqual compares a byte slice with either a string or byte slice
|
func bytesEqual(b1, b2 []byte) bool {
|
||||||
func bytesEqual(b []byte, s []byte) bool {
|
if len(b1) != len(b2) {
|
||||||
if len(b) != len(s) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for i := 0; i < len(b); i++ {
|
for i := 0; i < len(b1); i++ {
|
||||||
if b[i] != s[i] {
|
if b1[i] != b2[i] {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// isDigitOrMinus checks if a string starts with a digit or minus sign
|
func containsChar(b []byte, c byte) bool {
|
||||||
func isDigitOrMinus(s string) bool {
|
for _, v := range b {
|
||||||
if len(s) == 0 {
|
if v == c {
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isDigit(s[0]) || (s[0] == '-' && len(s) > 1 && isDigit(s[1]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseStringAsNumber tries to parse a string as a number (float or int)
|
|
||||||
func parseStringAsNumber(s string) (any, error) {
|
|
||||||
// Check if it has a decimal point
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
if s[i] == '.' {
|
|
||||||
// It's a float
|
|
||||||
return strconv.ParseFloat(s, 64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// It's an integer
|
|
||||||
return strconv.ParseInt(s, 10, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) tokenToValue(token Token) any {
|
|
||||||
switch token.Type {
|
|
||||||
case TokenString:
|
|
||||||
return string(token.Value)
|
|
||||||
case TokenNumber:
|
|
||||||
val, _ := parseStringAsNumber(string(token.Value))
|
|
||||||
return val
|
|
||||||
case TokenBoolean:
|
|
||||||
return bytesEqual(token.Value, []byte("true"))
|
|
||||||
default:
|
|
||||||
return string(token.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) convertNameValue(name string) any {
|
|
||||||
if name == "true" {
|
|
||||||
return true
|
return true
|
||||||
} else if name == "false" {
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
} else if isDigitOrMinus(name) {
|
|
||||||
val, err := parseStringAsNumber(name)
|
|
||||||
if err == nil {
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
}
|
||||||
|
|
71
pool.go
Normal file
71
pool.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// byteSlicePool helps reuse byte slices
|
||||||
|
var byteSlicePool = sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
b := make([]byte, 0, 64)
|
||||||
|
return &b
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByteSlice gets a byte slice from the pool
|
||||||
|
func GetByteSlice() *[]byte {
|
||||||
|
return byteSlicePool.Get().(*[]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutByteSlice returns a byte slice to the pool
|
||||||
|
func PutByteSlice(b *[]byte) {
|
||||||
|
if b != nil {
|
||||||
|
*b = (*b)[:0] // Clear but keep capacity
|
||||||
|
byteSlicePool.Put(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapPool helps reuse maps for config objects
|
||||||
|
var mapPool = sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
m := make(map[string]any, 8)
|
||||||
|
return &m
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMap gets a map from the pool
|
||||||
|
func GetMap() *map[string]any {
|
||||||
|
return mapPool.Get().(*map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutMap returns a map to the pool after clearing it
|
||||||
|
func PutMap(m *map[string]any) {
|
||||||
|
if m != nil {
|
||||||
|
// Clear the map
|
||||||
|
for k := range *m {
|
||||||
|
delete(*m, k)
|
||||||
|
}
|
||||||
|
mapPool.Put(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// arrayPool helps reuse slices
|
||||||
|
var arrayPool = sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
a := make([]any, 0, 4)
|
||||||
|
return &a
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetArray gets a slice from the pool
|
||||||
|
func GetArray() *[]any {
|
||||||
|
return arrayPool.Get().(*[]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutArray returns a slice to the pool
|
||||||
|
func PutArray(a *[]any) {
|
||||||
|
if a != nil {
|
||||||
|
*a = (*a)[:0] // Clear but keep capacity
|
||||||
|
arrayPool.Put(a)
|
||||||
|
}
|
||||||
|
}
|
37
scanner.go
37
scanner.go
|
@ -22,17 +22,20 @@ type Scanner struct {
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
line int
|
line int
|
||||||
col int
|
col int
|
||||||
buffer []byte
|
buffer []byte // Slice to the pooled buffer
|
||||||
|
bufferRef *[]byte // Reference to the pooled buffer
|
||||||
token Token // Current token for unread
|
token Token // Current token for unread
|
||||||
}
|
}
|
||||||
|
|
||||||
// scannerPool helps reuse scanner objects
|
// scannerPool helps reuse scanner objects
|
||||||
var scannerPool = sync.Pool{
|
var scannerPool = sync.Pool{
|
||||||
New: func() interface{} {
|
New: func() any {
|
||||||
|
bufferRef := GetByteSlice()
|
||||||
return &Scanner{
|
return &Scanner{
|
||||||
line: 1,
|
line: 1,
|
||||||
col: 0,
|
col: 0,
|
||||||
buffer: make([]byte, 0, 128),
|
bufferRef: bufferRef,
|
||||||
|
buffer: (*bufferRef)[:0],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -40,10 +43,10 @@ var scannerPool = sync.Pool{
|
||||||
// NewScanner creates a new scanner from a pool
|
// NewScanner creates a new scanner from a pool
|
||||||
func NewScanner(r io.Reader) *Scanner {
|
func NewScanner(r io.Reader) *Scanner {
|
||||||
s := scannerPool.Get().(*Scanner)
|
s := scannerPool.Get().(*Scanner)
|
||||||
s.reader = bufio.NewReader(r)
|
s.reader = bufio.NewReaderSize(r, 1024)
|
||||||
s.line = 1
|
s.line = 1
|
||||||
s.col = 0
|
s.col = 0
|
||||||
s.buffer = s.buffer[:0]
|
s.buffer = (*s.bufferRef)[:0]
|
||||||
s.token = Token{Type: TokenError}
|
s.token = Token{Type: TokenError}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
@ -53,7 +56,7 @@ func ReleaseScanner(s *Scanner) {
|
||||||
if s != nil {
|
if s != nil {
|
||||||
// Clear references but keep allocated memory
|
// Clear references but keep allocated memory
|
||||||
s.reader = nil
|
s.reader = nil
|
||||||
s.buffer = s.buffer[:0]
|
s.buffer = (*s.bufferRef)[:0]
|
||||||
scannerPool.Put(s)
|
scannerPool.Put(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,19 +142,19 @@ func (s *Scanner) NextToken() (Token, error) {
|
||||||
|
|
||||||
// Skip whitespace
|
// Skip whitespace
|
||||||
err := s.SkipWhitespace()
|
err := s.SkipWhitespace()
|
||||||
if err == io.EOF {
|
|
||||||
return Token{Type: TokenEOF}, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error())}, err
|
if err == io.EOF {
|
||||||
|
return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil
|
||||||
|
}
|
||||||
|
return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := s.PeekByte()
|
b, err := s.PeekByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return Token{Type: TokenEOF}, nil
|
return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil
|
||||||
}
|
}
|
||||||
return Token{Type: TokenError, Value: []byte(err.Error())}, err
|
return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record start position for error reporting
|
// Record start position for error reporting
|
||||||
|
@ -159,10 +162,6 @@ func (s *Scanner) NextToken() (Token, error) {
|
||||||
|
|
||||||
// Process based on first character
|
// Process based on first character
|
||||||
switch {
|
switch {
|
||||||
case b == '=':
|
|
||||||
_, _ = s.ReadByte() // consume equals
|
|
||||||
return Token{Type: TokenEquals, Line: startLine, Column: startColumn}, nil
|
|
||||||
|
|
||||||
case b == '{':
|
case b == '{':
|
||||||
_, _ = s.ReadByte() // consume open brace
|
_, _ = s.ReadByte() // consume open brace
|
||||||
return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil
|
return Token{Type: TokenOpenBrace, Line: startLine, Column: startColumn}, nil
|
||||||
|
@ -264,7 +263,7 @@ func (s *Scanner) scanComment() error {
|
||||||
// scanString scans a quoted string
|
// scanString scans a quoted string
|
||||||
func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
|
func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
|
||||||
// Reset buffer
|
// Reset buffer
|
||||||
s.buffer = s.buffer[:0]
|
s.buffer = (*s.bufferRef)[:0]
|
||||||
|
|
||||||
// Consume opening quote
|
// Consume opening quote
|
||||||
_, err := s.ReadByte()
|
_, err := s.ReadByte()
|
||||||
|
@ -317,7 +316,7 @@ func (s *Scanner) scanString(startLine, startColumn int) (Token, error) {
|
||||||
// scanName scans an identifier
|
// scanName scans an identifier
|
||||||
func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
|
func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
|
||||||
// Reset buffer
|
// Reset buffer
|
||||||
s.buffer = s.buffer[:0]
|
s.buffer = (*s.bufferRef)[:0]
|
||||||
|
|
||||||
// Read first character
|
// Read first character
|
||||||
b, err := s.ReadByte()
|
b, err := s.ReadByte()
|
||||||
|
@ -363,7 +362,7 @@ func (s *Scanner) scanName(startLine, startColumn int) (Token, error) {
|
||||||
// scanNumber scans a numeric value
|
// scanNumber scans a numeric value
|
||||||
func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
|
func (s *Scanner) scanNumber(startLine, startColumn int) (Token, error) {
|
||||||
// Reset buffer
|
// Reset buffer
|
||||||
s.buffer = s.buffer[:0]
|
s.buffer = (*s.bufferRef)[:0]
|
||||||
|
|
||||||
// Read first character (might be a minus sign or digit)
|
// Read first character (might be a minus sign or digit)
|
||||||
b, err := s.ReadByte()
|
b, err := s.ReadByte()
|
||||||
|
|
|
@ -10,13 +10,13 @@ import (
|
||||||
|
|
||||||
func TestBasicKeyValuePairs(t *testing.T) {
|
func TestBasicKeyValuePairs(t *testing.T) {
|
||||||
input := `
|
input := `
|
||||||
boolTrue = true
|
boolTrue true
|
||||||
boolFalse = false
|
boolFalse false
|
||||||
integer = 42
|
integer 42
|
||||||
negativeInt = -10
|
negativeInt -10
|
||||||
floatValue = 3.14
|
floatValue 3.14
|
||||||
negativeFloat = -2.5
|
negativeFloat -2.5
|
||||||
stringValue = "hello world"
|
stringValue "hello world"
|
||||||
`
|
`
|
||||||
config, err := config.Load(strings.NewReader(input))
|
config, err := config.Load(strings.NewReader(input))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -83,18 +83,18 @@ func TestBasicKeyValuePairs(t *testing.T) {
|
||||||
func TestComments(t *testing.T) {
|
func TestComments(t *testing.T) {
|
||||||
input := `
|
input := `
|
||||||
-- This is a line comment
|
-- This is a line comment
|
||||||
key1 = "value1"
|
key1 "value1"
|
||||||
|
|
||||||
--[[ This is a
|
--[[ This is a
|
||||||
block comment spanning
|
block comment spanning
|
||||||
multiple lines ]]
|
multiple lines ]]
|
||||||
key2 = "value2"
|
key2 "value2"
|
||||||
|
|
||||||
settings {
|
settings {
|
||||||
-- Comment inside a map
|
-- Comment inside a map
|
||||||
timeout = 30
|
timeout 30
|
||||||
--[[ Another block comment ]]
|
--[[ Another block comment ]]
|
||||||
retries = 3
|
retries 3
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -135,15 +135,6 @@ func TestArrays(t *testing.T) {
|
||||||
"cherry"
|
"cherry"
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Array with equals sign
|
|
||||||
numbers = {
|
|
||||||
1
|
|
||||||
2
|
|
||||||
3
|
|
||||||
4
|
|
||||||
5
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Mixed types array
|
-- Mixed types array
|
||||||
mixed {
|
mixed {
|
||||||
"string"
|
"string"
|
||||||
|
@ -175,23 +166,6 @@ func TestArrays(t *testing.T) {
|
||||||
t.Errorf("expected fruits.0=\"apple\", got %v, err: %v", apple, err)
|
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
|
// Verify mixed types array
|
||||||
mixed, err := config.GetArray("mixed")
|
mixed, err := config.GetArray("mixed")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -228,28 +202,20 @@ func TestMaps(t *testing.T) {
|
||||||
input := `
|
input := `
|
||||||
-- Simple map
|
-- Simple map
|
||||||
server {
|
server {
|
||||||
host = "localhost"
|
host "localhost"
|
||||||
port = 8080
|
port 8080
|
||||||
}
|
|
||||||
|
|
||||||
-- Map with equals sign
|
|
||||||
database = {
|
|
||||||
username = "admin"
|
|
||||||
password = "secret"
|
|
||||||
enabled = true
|
|
||||||
maxConnections = 100
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Nested maps
|
-- Nested maps
|
||||||
application {
|
application {
|
||||||
name = "MyApp"
|
name "MyApp"
|
||||||
version = "1.0.0"
|
version "1.0.0"
|
||||||
settings {
|
settings {
|
||||||
theme = "dark"
|
theme "dark"
|
||||||
notifications = true
|
notifications true
|
||||||
logging {
|
logging {
|
||||||
level = "info"
|
level "info"
|
||||||
file = "app.log"
|
file "app.log"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,17 +247,6 @@ func TestMaps(t *testing.T) {
|
||||||
t.Errorf("expected server.port=8080, got %v, err: %v", port, err)
|
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
|
// Verify deeply nested maps
|
||||||
appName, err := config.GetString("application.name")
|
appName, err := config.GetString("application.name")
|
||||||
if err != nil || appName != "MyApp" {
|
if err != nil || appName != "MyApp" {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user