Files
2025-07-07 01:44:12 +02:00

385 lines
13 KiB
Go

package config
import (
"fmt"
"os"
"strings"
"time"
"github.com/spf13/viper"
)
// Config holds all application configuration
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
App AppConfig `mapstructure:"app"`
Security SecurityConfig `mapstructure:"security"`
Logging LoggingConfig `mapstructure:"logging"`
External ExternalConfig `mapstructure:"external_services"`
Features FeatureConfig `mapstructure:"features"`
Defaults DefaultConfig `mapstructure:"defaults"`
}
// ServerConfig holds server-related configuration
type ServerConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
}
// DatabaseConfig holds database-related configuration
type DatabaseConfig struct {
Path string `mapstructure:"path"`
ConnectionPool DatabaseConnectionConfig `mapstructure:"connection_pool"`
Logging DatabaseLoggingConfig `mapstructure:"logging"`
Migration DatabaseMigrationConfig `mapstructure:"migration"`
Performance DatabasePerformanceConfig `mapstructure:"performance"`
}
// DatabaseConnectionConfig holds database connection pool settings
type DatabaseConnectionConfig struct {
MaxIdleConnections int `mapstructure:"max_idle_connections"`
MaxOpenConnections int `mapstructure:"max_open_connections"`
ConnectionMaxLifetime time.Duration `mapstructure:"connection_max_lifetime"`
ConnectionMaxIdleTime time.Duration `mapstructure:"connection_max_idle_time"`
}
// DatabaseLoggingConfig holds database logging settings
type DatabaseLoggingConfig struct {
Level string `mapstructure:"level"`
SlowQueryThreshold time.Duration `mapstructure:"slow_query_threshold"`
Debug bool `mapstructure:"debug"`
}
// DatabaseMigrationConfig holds database migration settings
type DatabaseMigrationConfig struct {
AutoMigrate bool `mapstructure:"auto_migrate"`
DropTablesFirst bool `mapstructure:"drop_tables_first"`
CreateBatchSize int `mapstructure:"create_batch_size"`
}
// DatabasePerformanceConfig holds database performance settings
type DatabasePerformanceConfig struct {
PrepareStatements bool `mapstructure:"prepare_statements"`
DisableForeignKeyCheck bool `mapstructure:"disable_foreign_key_check"`
IgnoreRelationshipsWhenMigrating bool `mapstructure:"ignore_relationships_when_migrating"`
QueryFields bool `mapstructure:"query_fields"`
DryRun bool `mapstructure:"dry_run"`
CreateInBatches int `mapstructure:"create_in_batches"`
}
// AppConfig holds general application settings
type AppConfig struct {
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
Environment string `mapstructure:"environment"`
Debug bool `mapstructure:"debug"`
}
// SecurityConfig holds security-related settings
type SecurityConfig struct {
Session SessionConfig `mapstructure:"session"`
Password PasswordConfig `mapstructure:"password"`
}
// SessionConfig holds session management settings
type SessionConfig struct {
Timeout time.Duration `mapstructure:"timeout"`
CookieName string `mapstructure:"cookie_name"`
SecureCookies bool `mapstructure:"secure_cookies"`
HttpOnly bool `mapstructure:"http_only"`
}
// PasswordConfig holds password requirements
type PasswordConfig struct {
MinLength int `mapstructure:"min_length"`
RequireUppercase bool `mapstructure:"require_uppercase"`
RequireLowercase bool `mapstructure:"require_lowercase"`
RequireNumbers bool `mapstructure:"require_numbers"`
RequireSpecialChars bool `mapstructure:"require_special_chars"`
}
// LoggingConfig holds logging settings
type LoggingConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
Output string `mapstructure:"output"`
FilePath string `mapstructure:"file_path"`
Rotation LogRotationConfig `mapstructure:"rotation"`
}
// LogRotationConfig holds log rotation settings
type LogRotationConfig struct {
Enabled bool `mapstructure:"enabled"`
MaxSize string `mapstructure:"max_size"`
MaxAge string `mapstructure:"max_age"`
MaxBackups int `mapstructure:"max_backups"`
}
// ExternalConfig holds external service configurations
type ExternalConfig struct {
OverpassAPI OverpassAPIConfig `mapstructure:"overpass_api"`
}
// OverpassAPIConfig holds OpenStreetMap Overpass API settings
type OverpassAPIConfig struct {
URL string `mapstructure:"url"`
Timeout time.Duration `mapstructure:"timeout"`
MaxRetries int `mapstructure:"max_retries"`
SearchRadius int `mapstructure:"search_radius"`
}
// FeatureConfig holds feature flag settings
type FeatureConfig struct {
FuelStationSearch bool `mapstructure:"fuel_station_search"`
VehicleManagement bool `mapstructure:"vehicle_management"`
StatisticsDashboard bool `mapstructure:"statistics_dashboard"`
DataExport bool `mapstructure:"data_export"`
APIEndpoints bool `mapstructure:"api_endpoints"`
}
// DefaultConfig holds default values for new entities
type DefaultConfig struct {
Currency string `mapstructure:"currency"`
FuelType string `mapstructure:"fuel_type"`
DistanceUnit string `mapstructure:"distance_unit"`
VolumeUnit string `mapstructure:"volume_unit"`
}
// Load loads configuration from file, environment variables, and defaults
func Load(configPath string) (*Config, error) {
v := viper.New()
// Set defaults
setDefaults(v)
// Configure Viper
if configPath != "" {
v.SetConfigFile(configPath)
} else {
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath(".")
v.AddConfigPath("./config")
v.AddConfigPath("$HOME/.tankstopp")
v.AddConfigPath("/etc/tankstopp")
}
// Enable environment variable binding
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.SetEnvPrefix("TANKSTOPP")
// Read config file
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("error reading config file: %w", err)
}
// Config file not found, continue with defaults and env vars
}
// Unmarshal into struct
var config Config
if err := v.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("error unmarshaling config: %w", err)
}
// Validate configuration
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid configuration: %w", err)
}
return &config, nil
}
// setDefaults sets default values for configuration
func setDefaults(v *viper.Viper) {
// Server defaults
v.SetDefault("server.host", "localhost")
v.SetDefault("server.port", 8081)
v.SetDefault("server.read_timeout", "30s")
v.SetDefault("server.write_timeout", "30s")
v.SetDefault("server.idle_timeout", "120s")
v.SetDefault("server.shutdown_timeout", "10s")
// Database defaults
v.SetDefault("database.path", "fuel_stops.db")
v.SetDefault("database.connection_pool.max_idle_connections", 10)
v.SetDefault("database.connection_pool.max_open_connections", 100)
v.SetDefault("database.connection_pool.connection_max_lifetime", "1h")
v.SetDefault("database.connection_pool.connection_max_idle_time", "30m")
v.SetDefault("database.logging.level", "warn")
v.SetDefault("database.logging.slow_query_threshold", "200ms")
v.SetDefault("database.logging.debug", false)
v.SetDefault("database.migration.auto_migrate", true)
v.SetDefault("database.migration.drop_tables_first", false)
v.SetDefault("database.migration.create_batch_size", 1000)
v.SetDefault("database.performance.prepare_statements", true)
v.SetDefault("database.performance.disable_foreign_key_check", false)
v.SetDefault("database.performance.ignore_relationships_when_migrating", false)
v.SetDefault("database.performance.query_fields", true)
v.SetDefault("database.performance.dry_run", false)
v.SetDefault("database.performance.create_in_batches", 100)
// App defaults
v.SetDefault("app.name", "TankStopp")
v.SetDefault("app.version", "1.0.0")
v.SetDefault("app.environment", "development")
v.SetDefault("app.debug", true)
// Security defaults
v.SetDefault("security.session.timeout", "24h")
v.SetDefault("security.session.cookie_name", "tankstopp_session")
v.SetDefault("security.session.secure_cookies", false)
v.SetDefault("security.session.http_only", true)
v.SetDefault("security.password.min_length", 8)
v.SetDefault("security.password.require_uppercase", true)
v.SetDefault("security.password.require_lowercase", true)
v.SetDefault("security.password.require_numbers", true)
v.SetDefault("security.password.require_special_chars", false)
// Logging defaults
v.SetDefault("logging.level", "info")
v.SetDefault("logging.format", "text")
v.SetDefault("logging.output", "stdout")
v.SetDefault("logging.file_path", "logs/tankstopp.log")
v.SetDefault("logging.rotation.enabled", false)
v.SetDefault("logging.rotation.max_size", "100MB")
v.SetDefault("logging.rotation.max_age", "30d")
v.SetDefault("logging.rotation.max_backups", 5)
// External services defaults
v.SetDefault("external_services.overpass_api.url", "https://overpass-api.de/api/interpreter")
v.SetDefault("external_services.overpass_api.timeout", "30s")
v.SetDefault("external_services.overpass_api.max_retries", 3)
v.SetDefault("external_services.overpass_api.search_radius", 5000)
// Feature flags defaults
v.SetDefault("features.fuel_station_search", true)
v.SetDefault("features.vehicle_management", true)
v.SetDefault("features.statistics_dashboard", true)
v.SetDefault("features.data_export", true)
v.SetDefault("features.api_endpoints", true)
// Default values
v.SetDefault("defaults.currency", "EUR")
v.SetDefault("defaults.fuel_type", "Super E5")
v.SetDefault("defaults.distance_unit", "km")
v.SetDefault("defaults.volume_unit", "liters")
}
// Validate validates the configuration
func (c *Config) Validate() error {
// Validate server config
if c.Server.Port < 1 || c.Server.Port > 65535 {
return fmt.Errorf("invalid server port: %d", c.Server.Port)
}
// Validate database config
if c.Database.Path == "" {
return fmt.Errorf("database path cannot be empty")
}
if c.Database.ConnectionPool.MaxIdleConnections < 0 {
return fmt.Errorf("max idle connections cannot be negative")
}
if c.Database.ConnectionPool.MaxOpenConnections < 0 {
return fmt.Errorf("max open connections cannot be negative")
}
// Validate app config
if c.App.Name == "" {
return fmt.Errorf("app name cannot be empty")
}
validEnvs := []string{"development", "production", "test"}
if !contains(validEnvs, c.App.Environment) {
return fmt.Errorf("invalid environment: %s (must be one of: %v)", c.App.Environment, validEnvs)
}
// Validate security config
if c.Security.Password.MinLength < 4 {
return fmt.Errorf("minimum password length cannot be less than 4")
}
// Validate logging config
validLogLevels := []string{"debug", "info", "warn", "error"}
if !contains(validLogLevels, c.Logging.Level) {
return fmt.Errorf("invalid log level: %s (must be one of: %v)", c.Logging.Level, validLogLevels)
}
validLogFormats := []string{"json", "text"}
if !contains(validLogFormats, c.Logging.Format) {
return fmt.Errorf("invalid log format: %s (must be one of: %v)", c.Logging.Format, validLogFormats)
}
return nil
}
// IsProduction returns true if the app is running in production environment
func (c *Config) IsProduction() bool {
return c.App.Environment == "production"
}
// IsDevelopment returns true if the app is running in development environment
func (c *Config) IsDevelopment() bool {
return c.App.Environment == "development"
}
// IsTest returns true if the app is running in test environment
func (c *Config) IsTest() bool {
return c.App.Environment == "test"
}
// GetServerAddress returns the server address in host:port format
func (c *Config) GetServerAddress() string {
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
}
// String returns a string representation of the configuration (without sensitive data)
func (c *Config) String() string {
return fmt.Sprintf(`TankStopp Configuration:
Server: %s
Database: %s
Environment: %s
Debug: %t
Features: %+v`,
c.GetServerAddress(),
c.Database.Path,
c.App.Environment,
c.App.Debug,
c.Features)
}
// contains checks if a slice contains a string
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// GetConfigFromEnv returns environment-specific configuration
func GetConfigFromEnv() string {
if configPath := os.Getenv("TANKSTOPP_CONFIG_PATH"); configPath != "" {
return configPath
}
env := os.Getenv("TANKSTOPP_ENV")
if env == "" {
env = os.Getenv("ENV")
}
if env == "" {
env = "development"
}
return fmt.Sprintf("config.%s.yaml", env)
}