Files
config/doc/file.md
2025-11-08 07:16:48 -05:00

6.0 KiB

Configuration Files

The config package supports TOML configuration files with automatic loading, discovery, and atomic saving.

TOML Format

TOML (Tom's Obvious, Minimal Language) is the supported configuration format:

# Basic values
host = "localhost"
port = 8080
debug = false

# Nested sections
[server]
host = "0.0.0.0"
port = 9090
timeout = "30s"

[database]
url = "postgres://localhost/mydb"
max_conns = 25
timeout = "5s"

# Arrays
[features]
enabled = ["auth", "api", "metrics"]

# Inline tables
tls = { enabled = true, cert = "/path/to/cert", key = "/path/to/key" }

Loading Configuration Files

Basic Loading

cfg := config.New()
cfg.RegisterStruct("", &Config{})

if err := cfg.LoadFile("config.toml"); err != nil {
    if errors.Is(err, config.ErrConfigNotFound) {
        log.Println("Config file not found, using defaults")
    } else {
        log.Fatal("Failed to load config:", err)
    }
}

With Builder

cfg, err := config.NewBuilder().
    WithDefaults(&Config{}).
    WithFile("/etc/myapp/config.toml").
    Build()

Multiple File Attempts

// Try multiple locations
locations := []string{
    "./config.toml",
    "~/.config/myapp/config.toml",
    "/etc/myapp/config.toml",
}

var cfg *config.Config
var err error

for _, path := range locations {
    cfg, err = config.NewBuilder().
        WithDefaults(&Config{}).
        WithFile(path).
        Build()
    
    if err == nil || !errors.Is(err, config.ErrConfigNotFound) {
        break
    }
}

Automatic File Discovery

Use file discovery to find configuration automatically:

cfg, _ := config.NewBuilder().
    WithDefaults(&Config{}).
    WithFileDiscovery(config.FileDiscoveryOptions{
        Name:          "myapp",
        Extensions:    []string{".toml", ".conf"},
        EnvVar:        "MYAPP_CONFIG",
        CLIFlag:       "--config",
        UseXDG:        true,
        UseCurrentDir: true,
        Paths:         []string{"/opt/myapp"},
    }).
    Build()

Search order:

  1. CLI flag: --config=/path/to/config.toml
  2. Environment variable: $MYAPP_CONFIG
  3. Current directory: ./myapp.toml, ./myapp.conf
  4. XDG config: ~/.config/myapp/myapp.toml
  5. System paths: /etc/myapp/myapp.toml
  6. Custom paths: /opt/myapp/myapp.toml

Saving Configuration

Save Current State

// Save all current values atomically
if err := cfg.Save("config.toml"); err != nil {
    log.Fatal("Failed to save config:", err)
}

The save operation is atomic - it writes to a temporary file then renames it.

Save Specific Source

// Save only values from environment variables
if err := cfg.SaveSource("env-config.toml", config.SourceEnv); err != nil {
    log.Fatal(err)
}

// Save only file-loaded values
if err := cfg.SaveSource("file-only.toml", config.SourceFile); err != nil {
    log.Fatal(err)
}

Generate Default Configuration

// Create a default config file
defaults := &Config{}
// ... set default values ...

cfg, _ := config.NewBuilder().
    WithDefaults(defaults).
    Build()

// Save defaults as config template
if err := cfg.SaveSource("config.toml.example", config.SourceDefault); err != nil {
    log.Fatal(err)
}

File Structure Mapping

TOML structure maps directly to dot-notation paths:

# Maps to "debug"
debug = true

[server]
# Maps to "server.host"
host = "localhost"
# Maps to "server.port"  
port = 8080

[server.tls]
# Maps to "server.tls.enabled"
enabled = true
# Maps to "server.tls.cert"
cert = "/path/to/cert"

[[users]]
# Array elements: "users.0.name", "users.0.role"
name = "admin"
role = "administrator"

[[users]]
# Array elements: "users.1.name", "users.1.role"
name = "user"
role = "standard"

Type Handling

TOML types map to Go types:

# Strings
name = "myapp"
multiline = """
Line one
Line two
"""

# Numbers
port = 8080          # int64
timeout = 30         # int64
ratio = 0.95         # float64
max_size = 1_000_000 # int64 (underscores allowed)

# Booleans
enabled = true
debug = false

# Dates/Times (RFC 3339)
created_at = 2024-01-15T09:30:00Z
expires = 2024-12-31

# Arrays
ports = [8080, 8081, 8082]
tags = ["production", "stable"]

# Tables (objects)
[database]
host = "localhost"
port = 5432

# Array of tables
[[servers]]
name = "web1"
host = "10.0.0.1"

[[servers]]
name = "web2"  
host = "10.0.0.2"

Error Handling

File loading can produce several error types:

err := cfg.LoadFile("config.toml")
if err != nil {
    switch {
    case errors.Is(err, config.ErrConfigNotFound):
        // File doesn't exist - often not fatal
        log.Println("No config file, using defaults")
        
    case strings.Contains(err.Error(), "failed to parse TOML"):
        // TOML syntax error
        log.Fatal("Invalid TOML syntax:", err)
        
    case strings.Contains(err.Error(), "failed to read"):
        // Permission or I/O error
        log.Fatal("Cannot read config file:", err)
        
    default:
        log.Fatal("Config error:", err)
    }
}

Security Considerations

File Permissions

// After saving, verify permissions
info, err := os.Stat("config.toml")
if err == nil {
    mode := info.Mode()
    if mode&0077 != 0 {
        log.Warn("Config file is world/group readable")
        // Fix permissions
        os.Chmod("config.toml", 0600)
    }
}

Size Limits

Files and values have size limits:

  • Maximum file size: ~10MB (10 * MaxValueSize)
  • Maximum value size: 1MB

Partial Loading

Load only specific sections:

var serverCfg ServerConfig
if err := cfg.Scan("server", &serverCfg); err != nil {
    log.Fatal(err)
}

var dbCfg DatabaseConfig  
if err := cfg.Scan("database", &dbCfg); err != nil {
    log.Fatal(err)
}

Best Practices

  1. Use Example Files: Generate .example files with defaults
  2. Check Permissions: Ensure config files aren't world-readable
  3. Validate After Load: Add validators to check loaded values
  4. Handle Missing Files: Missing config files often aren't fatal
  5. Use Atomic Saves: The built-in Save method is atomic
  6. Document Structure: Comment your TOML files thoroughly