e4.0.0 Refactored, file watcher and improved builder, doc update
This commit is contained in:
168
cmd/main.go
Normal file
168
cmd/main.go
Normal file
@ -0,0 +1,168 @@
|
||||
// FILE: example/watch_demo.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/lixenwraith/config"
|
||||
)
|
||||
|
||||
// AppConfig represents our application configuration
|
||||
type AppConfig struct {
|
||||
Server struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
} `toml:"server"`
|
||||
|
||||
Database struct {
|
||||
URL string `toml:"url"`
|
||||
MaxConns int `toml:"max_conns"`
|
||||
IdleTimeout time.Duration `toml:"idle_timeout"`
|
||||
} `toml:"database"`
|
||||
|
||||
Features struct {
|
||||
RateLimit bool `toml:"rate_limit"`
|
||||
Caching bool `toml:"caching"`
|
||||
} `toml:"features"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Create configuration with defaults
|
||||
defaults := &AppConfig{}
|
||||
defaults.Server.Host = "localhost"
|
||||
defaults.Server.Port = 8080
|
||||
defaults.Database.MaxConns = 10
|
||||
defaults.Database.IdleTimeout = 30 * time.Second
|
||||
|
||||
// Build configuration
|
||||
cfg, err := config.NewBuilder().
|
||||
WithDefaults(defaults).
|
||||
WithEnvPrefix("MYAPP_").
|
||||
WithFile("config.toml").
|
||||
Build()
|
||||
if err != nil && !errors.Is(err, config.ErrConfigNotFound) {
|
||||
log.Fatal("Failed to load config:", err)
|
||||
}
|
||||
|
||||
// Enable auto-update with custom options
|
||||
watchOpts := config.WatchOptions{
|
||||
PollInterval: 500 * time.Millisecond, // Check twice per second
|
||||
Debounce: 200 * time.Millisecond, // Quick response
|
||||
MaxWatchers: 10,
|
||||
ReloadTimeout: 2 * time.Second,
|
||||
VerifyPermissions: true, // SECURITY: Detect permission changes
|
||||
}
|
||||
cfg.AutoUpdateWithOptions(watchOpts)
|
||||
defer cfg.StopAutoUpdate()
|
||||
|
||||
// Start watching for changes
|
||||
changes := cfg.Watch()
|
||||
|
||||
// Context for graceful shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Handle signals
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Log initial configuration
|
||||
logConfig(cfg)
|
||||
|
||||
// Watch for changes
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case path := <-changes:
|
||||
handleConfigChange(cfg, path)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Main loop
|
||||
log.Println("Watching for configuration changes. Edit config.toml to see updates.")
|
||||
log.Println("Press Ctrl+C to exit.")
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigCh:
|
||||
log.Println("Shutting down...")
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
// Periodic health check
|
||||
var port int64
|
||||
port, _ = cfg.Get("server.port")
|
||||
log.Printf("Server still running on port %d", port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleConfigChange(cfg *config.Config, path string) {
|
||||
switch path {
|
||||
case "__file_deleted__":
|
||||
log.Println("⚠️ Config file was deleted!")
|
||||
case "__permissions_changed__":
|
||||
log.Println("⚠️ SECURITY: Config file permissions changed!")
|
||||
case "__reload_error__":
|
||||
log.Printf("❌ Failed to reload config: %s", path)
|
||||
case "__reload_timeout__":
|
||||
log.Println("⚠️ Config reload timed out")
|
||||
default:
|
||||
// Normal configuration change
|
||||
value, _ := cfg.Get(path)
|
||||
log.Printf("📝 Config changed: %s = %v", path, value)
|
||||
|
||||
// Handle specific changes
|
||||
switch path {
|
||||
case "server.port":
|
||||
log.Println("Port changed - server restart required")
|
||||
case "database.url":
|
||||
log.Println("Database URL changed - reconnection required")
|
||||
case "features.rate_limit":
|
||||
if cfg.Bool("features.rate_limit") {
|
||||
log.Println("Rate limiting enabled")
|
||||
} else {
|
||||
log.Println("Rate limiting disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logConfig(cfg *config.Config) {
|
||||
log.Println("Current configuration:")
|
||||
log.Printf(" Server: %s:%d", cfg.String("server.host"), cfg.Int("server.port"))
|
||||
log.Printf(" Database: %s (max_conns=%d)",
|
||||
cfg.String("database.url"),
|
||||
cfg.Int("database.max_conns"))
|
||||
log.Printf(" Features: rate_limit=%v, caching=%v",
|
||||
cfg.Bool("features.rate_limit"),
|
||||
cfg.Bool("features.caching"))
|
||||
}
|
||||
|
||||
// Example config.toml file:
|
||||
/*
|
||||
[server]
|
||||
host = "localhost"
|
||||
port = 8080
|
||||
|
||||
[database]
|
||||
url = "postgres://localhost/myapp"
|
||||
max_conns = 25
|
||||
idle_timeout = "30s"
|
||||
|
||||
[features]
|
||||
rate_limit = true
|
||||
caching = false
|
||||
*/
|
||||
301
cmd/test/main.go
301
cmd/test/main.go
@ -1,301 +0,0 @@
|
||||
// File: lixenwraith/cmd/test/main.go
|
||||
// Test program for the config package
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/lixenwraith/config"
|
||||
)
|
||||
|
||||
// AppConfig represents a simple application configuration
|
||||
type AppConfig struct {
|
||||
Server struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
} `toml:"server"`
|
||||
Database struct {
|
||||
URL string `toml:"url"`
|
||||
MaxConns int `toml:"max_conns"`
|
||||
} `toml:"database"`
|
||||
API struct {
|
||||
Key string `toml:"key" env:"CUSTOM_API_KEY"` // Custom env mapping
|
||||
Timeout int `toml:"timeout"`
|
||||
} `toml:"api"`
|
||||
Debug bool `toml:"debug"`
|
||||
LogFile string `toml:"log_file"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== Config Package Feature Test ===\n")
|
||||
|
||||
// Test directories
|
||||
tempDir := os.TempDir()
|
||||
configPath := filepath.Join(tempDir, "test_config.toml")
|
||||
defer os.Remove(configPath)
|
||||
|
||||
// Set up test environment variables
|
||||
setupEnvironment()
|
||||
defer cleanupEnvironment()
|
||||
|
||||
// Run feature tests
|
||||
testQuickStart()
|
||||
testBuilder()
|
||||
testSourceTracking()
|
||||
testEnvironmentFeatures()
|
||||
testValidation()
|
||||
testUtilities()
|
||||
|
||||
fmt.Println("\n=== All Tests Complete ===")
|
||||
}
|
||||
|
||||
func testQuickStart() {
|
||||
fmt.Println("=== Test 1: Quick Start ===")
|
||||
|
||||
// Define defaults
|
||||
defaults := AppConfig{}
|
||||
defaults.Server.Host = "localhost"
|
||||
defaults.Server.Port = 8080
|
||||
defaults.Database.URL = "postgres://localhost/testdb"
|
||||
defaults.Database.MaxConns = 10
|
||||
defaults.Debug = false
|
||||
|
||||
// Quick initialization
|
||||
cfg, err := config.Quick(defaults, "TEST_", "")
|
||||
if err != nil {
|
||||
log.Fatalf("Quick init failed: %v", err)
|
||||
}
|
||||
|
||||
// Access values
|
||||
host, _ := cfg.String("server.host")
|
||||
port, _ := cfg.Int64("server.port")
|
||||
fmt.Printf("Quick config - Host: %s, Port: %d\n", host, port)
|
||||
|
||||
// Verify env override (TEST_DEBUG=true was set)
|
||||
debug, _ := cfg.Bool("debug")
|
||||
fmt.Printf("Debug from env: %v (should be true)\n", debug)
|
||||
}
|
||||
|
||||
func testBuilder() {
|
||||
fmt.Println("\n=== Test 2: Builder Pattern ===")
|
||||
|
||||
defaults := AppConfig{}
|
||||
defaults.Server.Port = 8080
|
||||
defaults.API.Timeout = 30
|
||||
|
||||
// Custom precedence: Env > File > CLI > Default
|
||||
cfg, err := config.NewBuilder().
|
||||
WithDefaults(defaults).
|
||||
WithEnvPrefix("APP_").
|
||||
WithSources(
|
||||
config.SourceEnv,
|
||||
config.SourceFile,
|
||||
config.SourceCLI,
|
||||
config.SourceDefault,
|
||||
).
|
||||
WithArgs([]string{"--server.port=9999"}).
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Builder failed: %v", err)
|
||||
}
|
||||
|
||||
// ENV should win over CLI due to custom precedence
|
||||
port, _ := cfg.Int64("server.port")
|
||||
fmt.Printf("Port with Env > CLI precedence: %d (should be 7070 from env)\n", port)
|
||||
}
|
||||
|
||||
func testSourceTracking() {
|
||||
fmt.Println("\n=== Test 3: Source Tracking ===")
|
||||
|
||||
cfg := config.New()
|
||||
cfg.Register("test.value", "default")
|
||||
|
||||
// Set from multiple sources
|
||||
cfg.SetSource("test.value", config.SourceFile, "from-file")
|
||||
cfg.SetSource("test.value", config.SourceEnv, "from-env")
|
||||
cfg.SetSource("test.value", config.SourceCLI, "from-cli")
|
||||
|
||||
// Show all sources
|
||||
sources := cfg.GetSources("test.value")
|
||||
fmt.Println("All sources for test.value:")
|
||||
for source, value := range sources {
|
||||
fmt.Printf(" %s: %v\n", source, value)
|
||||
}
|
||||
|
||||
// Get from specific source
|
||||
envVal, exists := cfg.GetSource("test.value", config.SourceEnv)
|
||||
fmt.Printf("Value from env source: %v (exists: %v)\n", envVal, exists)
|
||||
|
||||
// Current value (default precedence)
|
||||
current, _ := cfg.String("test.value")
|
||||
fmt.Printf("Current value: %s (should be from-cli)\n", current)
|
||||
}
|
||||
|
||||
func testEnvironmentFeatures() {
|
||||
fmt.Println("\n=== Test 4: Environment Features ===")
|
||||
|
||||
cfg := config.New()
|
||||
cfg.Register("api.key", "")
|
||||
cfg.Register("api.secret", "")
|
||||
cfg.Register("database.host", "localhost")
|
||||
|
||||
// Test 4a: Custom env transform
|
||||
fmt.Println("\n4a. Custom Environment Transform:")
|
||||
opts := config.LoadOptions{
|
||||
Sources: []config.Source{config.SourceEnv, config.SourceDefault},
|
||||
EnvTransform: func(path string) string {
|
||||
switch path {
|
||||
case "api.key":
|
||||
return "CUSTOM_API_KEY"
|
||||
case "database.host":
|
||||
return "DB_HOST"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
},
|
||||
}
|
||||
cfg.LoadWithOptions("", nil, opts)
|
||||
|
||||
apiKey, _ := cfg.String("api.key")
|
||||
fmt.Printf("API Key from CUSTOM_API_KEY: %s\n", apiKey)
|
||||
|
||||
// Test 4b: Discover environment variables
|
||||
fmt.Println("\n4b. Environment Discovery:")
|
||||
cfg2 := config.New()
|
||||
cfg2.Register("server.port", 8080)
|
||||
cfg2.Register("debug", false)
|
||||
cfg2.Register("api.timeout", 30)
|
||||
|
||||
discovered := cfg2.DiscoverEnv("TEST_")
|
||||
fmt.Println("Discovered env vars with TEST_ prefix:")
|
||||
for path, envVar := range discovered {
|
||||
fmt.Printf(" %s -> %s\n", path, envVar)
|
||||
}
|
||||
|
||||
// Test 4c: Export configuration as env vars
|
||||
fmt.Println("\n4c. Export as Environment:")
|
||||
cfg2.Set("server.port", 3000)
|
||||
cfg2.Set("debug", true)
|
||||
|
||||
exports := cfg2.ExportEnv("EXPORT_")
|
||||
fmt.Println("Non-default values exported:")
|
||||
for env, value := range exports {
|
||||
fmt.Printf(" export %s=%s\n", env, value)
|
||||
}
|
||||
|
||||
// Test 4d: RegisterWithEnv
|
||||
fmt.Println("\n4d. RegisterWithEnv:")
|
||||
cfg3 := config.New()
|
||||
err := cfg3.RegisterWithEnv("special.value", "default", "SPECIAL_ENV_VAR")
|
||||
if err != nil {
|
||||
fmt.Printf("RegisterWithEnv error: %v\n", err)
|
||||
}
|
||||
special, _ := cfg3.String("special.value")
|
||||
fmt.Printf("Value from SPECIAL_ENV_VAR: %s\n", special)
|
||||
}
|
||||
|
||||
func testValidation() {
|
||||
fmt.Println("\n=== Test 5: Validation ===")
|
||||
|
||||
cfg := config.New()
|
||||
cfg.RegisterRequired("api.key", "")
|
||||
cfg.RegisterRequired("database.url", "")
|
||||
cfg.Register("optional.setting", "default")
|
||||
|
||||
// Should fail validation
|
||||
err := cfg.Validate("api.key", "database.url")
|
||||
if err != nil {
|
||||
fmt.Printf("Validation failed as expected: %v\n", err)
|
||||
}
|
||||
|
||||
// Set required values
|
||||
cfg.Set("api.key", "secret-key")
|
||||
cfg.Set("database.url", "postgres://localhost/db")
|
||||
|
||||
// Should pass validation
|
||||
err = cfg.Validate("api.key", "database.url")
|
||||
if err == nil {
|
||||
fmt.Println("Validation passed after setting required values")
|
||||
}
|
||||
}
|
||||
|
||||
func testUtilities() {
|
||||
fmt.Println("\n=== Test 6: Utility Features ===")
|
||||
|
||||
// Create config with some data
|
||||
cfg := config.New()
|
||||
cfg.Register("app.name", "testapp")
|
||||
cfg.Register("app.version", "1.0.0")
|
||||
cfg.Register("server.port", 8080)
|
||||
|
||||
cfg.SetSource("app.version", config.SourceFile, "1.1.0")
|
||||
cfg.SetSource("server.port", config.SourceEnv, 9090)
|
||||
|
||||
// Test 6a: Debug output
|
||||
fmt.Println("\n6a. Debug Output:")
|
||||
debug := cfg.Debug()
|
||||
fmt.Printf("Debug info (first 200 chars): %.200s...\n", debug)
|
||||
|
||||
// Test 6b: Clone
|
||||
fmt.Println("\n6b. Clone Configuration:")
|
||||
clone := cfg.Clone()
|
||||
clone.Set("app.name", "cloned-app")
|
||||
|
||||
original, _ := cfg.String("app.name")
|
||||
cloned, _ := clone.String("app.name")
|
||||
fmt.Printf("Original app.name: %s, Cloned: %s\n", original, cloned)
|
||||
|
||||
// Test 6c: Reset source
|
||||
fmt.Println("\n6c. Reset Sources:")
|
||||
sources := cfg.GetSources("server.port")
|
||||
fmt.Printf("Sources before reset: %v\n", sources)
|
||||
|
||||
cfg.ResetSource(config.SourceEnv)
|
||||
sources = cfg.GetSources("server.port")
|
||||
fmt.Printf("Sources after env reset: %v\n", sources)
|
||||
|
||||
// Test 6d: Save and load specific source
|
||||
fmt.Println("\n6d. Save/Load Specific Source:")
|
||||
tempFile := filepath.Join(os.TempDir(), "source_test.toml")
|
||||
defer os.Remove(tempFile)
|
||||
|
||||
err := cfg.SaveSource(tempFile, config.SourceFile)
|
||||
if err != nil {
|
||||
fmt.Printf("SaveSource error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Saved SourceFile values to temp file")
|
||||
}
|
||||
|
||||
// Test 6e: GetRegisteredPaths
|
||||
fmt.Println("\n6e. Registered Paths:")
|
||||
paths := cfg.GetRegisteredPaths("app.")
|
||||
fmt.Printf("Paths with 'app.' prefix: %v\n", paths)
|
||||
|
||||
pathsWithDefaults := cfg.GetRegisteredPathsWithDefaults("app.")
|
||||
for path, def := range pathsWithDefaults {
|
||||
fmt.Printf(" %s: %v\n", path, def)
|
||||
}
|
||||
}
|
||||
|
||||
func setupEnvironment() {
|
||||
// Set test environment variables
|
||||
os.Setenv("TEST_DEBUG", "true")
|
||||
os.Setenv("TEST_SERVER_PORT", "6666")
|
||||
os.Setenv("APP_SERVER_PORT", "7070")
|
||||
os.Setenv("CUSTOM_API_KEY", "env-api-key")
|
||||
os.Setenv("DB_HOST", "env-db-host")
|
||||
os.Setenv("SPECIAL_ENV_VAR", "special-value")
|
||||
}
|
||||
|
||||
func cleanupEnvironment() {
|
||||
os.Unsetenv("TEST_DEBUG")
|
||||
os.Unsetenv("TEST_SERVER_PORT")
|
||||
os.Unsetenv("APP_SERVER_PORT")
|
||||
os.Unsetenv("CUSTOM_API_KEY")
|
||||
os.Unsetenv("DB_HOST")
|
||||
os.Unsetenv("SPECIAL_ENV_VAR")
|
||||
}
|
||||
Reference in New Issue
Block a user