Compare commits

..

No commits in common. "80500963b438624286b4a991338b11d00fbcff77" and "44827e16d41a392ffbf528b755648ebda3d09a83" have entirely different histories.

8 changed files with 1027 additions and 1024 deletions

View File

@ -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"
} }
} }

View File

@ -2,6 +2,7 @@ package config
import ( import (
"encoding/json" "encoding/json"
"strconv"
"strings" "strings"
"testing" "testing"
@ -10,12 +11,618 @@ 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
@ -56,19 +663,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"
} }
} }
` `
@ -108,13 +715,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
@ -160,19 +767,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
} }
} }
} }
@ -458,17 +1065,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"

View File

@ -1,617 +0,0 @@
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)
}
}
}

519
config.go
View File

@ -4,13 +4,20 @@ 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
@ -19,58 +26,45 @@ 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: data, data: make(map[string]any, 16), // Pre-allocate with expected capacity
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
} }
if !strings.Contains(key, ".") { // Parse the dot-notation path manually
if val, ok := c.data[key]; ok { var start, i int
return val, nil var current any = c.data
}
return nil, fmt.Errorf("key %s not found", key) 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
} }
parts := strings.Split(key, ".") part := key[start:end]
current := any(c.data)
for _, part := range parts { // Handle current node based on its type
switch node := current.(type) { switch node := current.(type) {
case map[string]any: case map[string]any:
var exists bool // Simple map lookup
current, exists = node[part] val, ok := node[part]
if !exists { if !ok {
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)
@ -79,10 +73,22 @@ 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
} }
@ -197,6 +203,8 @@ 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",
@ -249,198 +257,244 @@ 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(fmt.Sprintf("expected name at top level, got token type %v", token.Type)) return c.Error("expected name at top level")
} }
// Get the property name - copy to create a stable key // Get the property name - copy to create a stable key
nameBytes := GetByteSlice() nameBytes := token.Value
*nameBytes = append((*nameBytes)[:0], token.Value...) name := string(nameBytes)
name := string(*nameBytes)
PutByteSlice(nameBytes)
// Get the next token // Get the next token (should be = or {)
nextToken, err := c.nextToken() token, 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 nextToken.Type == TokenOpenBrace { if token.Type == TokenEquals {
// It's a nested object/array // It's a standard key=value assignment
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 {
// It's a simple value return c.Error("expected '=' or '{' after name")
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
} }
// parseObject parses a map or array // parseValue parses a value after an equals sign
func (c *Config) parseObject() (any, error) { func (c *Config) parseValue() (any, error) {
// Default to treating as an array until we see a name
isArray := true
arrayRef := GetArray()
arrayElements := *arrayRef
mapRef := GetMap()
objectElements := *mapRef
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, err
return nil, fmt.Errorf("unexpected EOF in object/array")
} }
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
func (c *Config) parseObject() (any, error) {
// Initialize stack with first state
stack := []*ParseState{{
object: make(map[string]any, 8),
arrayElements: make([]any, 0, 8),
isArray: true,
}}
for len(stack) > 0 {
// Get current state from top of stack
current := stack[len(stack)-1]
token, err := c.nextToken()
if err != nil {
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 {
if isArray && len(arrayElements) > 0 { // Determine result based on what we've collected
result := arrayElements var result any
// Don't release the array, transfer ownership if current.isArray && len(current.object) == 0 {
*arrayRef = nil // Detach from pool reference result = current.arrayElements
} 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
// Don't release the map, transfer ownership // Otherwise, add result to parent
*mapRef = nil // Detach from pool reference parent := stack[len(stack)-1]
return result, nil if parent.expectValue {
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:
// Copy token value to create a stable key name := string(token.Value)
keyBytes := GetByteSlice()
*keyBytes = append((*keyBytes)[:0], token.Value...)
key := string(*keyBytes)
PutByteSlice(keyBytes)
// Look ahead to see what follows // Look ahead to determine context
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 == TokenOpenBrace { if nextToken.Type == TokenEquals {
// 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
isArray = false // If we see a key, it's a map current.isArray = false
value := c.tokenToValue(nextToken) current.currentKey = name
objectElements[key] = value current.expectValue = true
// Check if there's an object following // Parse the value
lookAhead, nextErr := c.nextToken() valueToken, err := 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
} }
// Check if we need to convert the value to a map if valueToken.Type == TokenOpenBrace {
if mapValue, ok := nestedValue.(map[string]any); ok { // Push new state for nested object/array
// Create a combined map newState := &ParseState{
combinedMapRef := GetMap() object: make(map[string]any, 8),
combinedMap := *combinedMapRef arrayElements: make([]any, 0, 8),
for k, v := range mapValue { isArray: true,
combinedMap[k] = v
} }
combinedMap["value"] = value stack = append(stack, newState)
objectElements[key] = combinedMap } else {
// Handle primitive value
value := c.tokenToValue(valueToken)
current.object[name] = value
} }
} else if nextErr == nil && lookAhead.Type != TokenEOF && lookAhead.Type != TokenCloseBrace { } else if nextToken.Type == TokenOpenBrace {
c.scanner.UnreadToken(lookAhead) // Nested object with name
} else if nextErr == nil && lookAhead.Type == TokenCloseBrace { current.isArray = false
// We found the closing brace - unread it so it's handled by the main loop current.currentKey = name
c.scanner.UnreadToken(lookAhead) current.expectValue = true
// 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:
// Nested object/array // New nested object/array
nestedValue, err := c.parseObject() newState := &ParseState{
if err != nil { object: make(map[string]any, 8),
return nil, err arrayElements: make([]any, 0, 8),
} 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 type: %v", token.Type)) return nil, c.Error(fmt.Sprintf("unexpected token: %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
@ -449,80 +503,13 @@ 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
} }
// tokenToValue converts a token to a Go value, preserving byte slices until final conversion // Helpers
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')
@ -532,27 +519,77 @@ func isDigit(b byte) bool {
return b >= '0' && b <= '9' return b >= '0' && b <= '9'
} }
func isDigitOrMinus(b byte) bool { // ParseNumber converts a string to a number (int64 or float64)
return isDigit(b) || b == '-' func ParseNumber(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 bytesEqual(b1, b2 []byte) bool { // bytesEqual compares a byte slice with either a string or byte slice
if len(b1) != len(b2) { func bytesEqual(b []byte, s []byte) bool {
if len(b) != len(s) {
return false return false
} }
for i := 0; i < len(b1); i++ { for i := 0; i < len(b); i++ {
if b1[i] != b2[i] { if b[i] != s[i] {
return false return false
} }
} }
return true return true
} }
func containsChar(b []byte, c byte) bool { // isDigitOrMinus checks if a string starts with a digit or minus sign
for _, v := range b { func isDigitOrMinus(s string) bool {
if v == c { if len(s) == 0 {
return true
}
}
return false 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
} else if name == "false" {
return false
} else if isDigitOrMinus(name) {
val, err := parseStringAsNumber(name)
if err == nil {
return val
}
}
return name
} }

71
pool.go
View File

@ -1,71 +0,0 @@
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)
}
}

View File

@ -22,20 +22,17 @@ type Scanner struct {
reader *bufio.Reader reader *bufio.Reader
line int line int
col int col int
buffer []byte // Slice to the pooled buffer buffer []byte
bufferRef *[]byte // Reference to the pooled buffer
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() any { New: func() interface{} {
bufferRef := GetByteSlice()
return &Scanner{ return &Scanner{
line: 1, line: 1,
col: 0, col: 0,
bufferRef: bufferRef, buffer: make([]byte, 0, 128),
buffer: (*bufferRef)[:0],
} }
}, },
} }
@ -43,10 +40,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.NewReaderSize(r, 1024) s.reader = bufio.NewReader(r)
s.line = 1 s.line = 1
s.col = 0 s.col = 0
s.buffer = (*s.bufferRef)[:0] s.buffer = s.buffer[:0]
s.token = Token{Type: TokenError} s.token = Token{Type: TokenError}
return s return s
} }
@ -56,7 +53,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.bufferRef)[:0] s.buffer = s.buffer[:0]
scannerPool.Put(s) scannerPool.Put(s)
} }
} }
@ -142,19 +139,19 @@ func (s *Scanner) NextToken() (Token, error) {
// Skip whitespace // Skip whitespace
err := s.SkipWhitespace() err := s.SkipWhitespace()
if err != nil {
if err == io.EOF { if err == io.EOF {
return Token{Type: TokenEOF, Line: s.line, Column: s.col}, nil return Token{Type: TokenEOF}, nil
} }
return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err if err != nil {
return Token{Type: TokenError, Value: []byte(err.Error())}, 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, Line: s.line, Column: s.col}, nil return Token{Type: TokenEOF}, nil
} }
return Token{Type: TokenError, Value: []byte(err.Error()), Line: s.line, Column: s.col}, err return Token{Type: TokenError, Value: []byte(err.Error())}, err
} }
// Record start position for error reporting // Record start position for error reporting
@ -162,6 +159,10 @@ 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
@ -263,7 +264,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.bufferRef)[:0] s.buffer = s.buffer[:0]
// Consume opening quote // Consume opening quote
_, err := s.ReadByte() _, err := s.ReadByte()
@ -316,7 +317,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.bufferRef)[:0] s.buffer = s.buffer[:0]
// Read first character // Read first character
b, err := s.ReadByte() b, err := s.ReadByte()
@ -362,7 +363,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.bufferRef)[:0] s.buffer = s.buffer[: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()

View File

@ -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,6 +135,15 @@ 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"
@ -166,6 +175,23 @@ 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 {
@ -202,20 +228,28 @@ 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"
} }
} }
} }
@ -247,6 +281,17 @@ 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" {

View File

@ -10,6 +10,7 @@ const (
TokenString TokenString
TokenNumber TokenNumber
TokenBoolean TokenBoolean
TokenEquals
TokenOpenBrace TokenOpenBrace
TokenCloseBrace TokenCloseBrace
TokenComment TokenComment