package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
const (
DefaultConfigFile = "app.yaml"
EnvPrefix = "APP_"
DefaultHost = "localhost"
DefaultPort = 8080
DefaultDBURL = "sqlite://app.db"
)
type Settings struct {
Server ServerConfig `json:"server" yaml:"server"`
Database DatabaseConfig `json:"database" yaml:"database"`
Logging LoggingConfig `json:"logging" yaml:"logging"`
Features FeatureFlags `json:"features" yaml:"features"`
loaded bool
envVars map[string]string
}
func NewSettings() *Settings {
return &Settings{
Server: DefaultServerConfig(),
Database: DefaultDatabaseConfig(),
Logging: DefaultLoggingConfig(),
Features: DefaultFeatureFlags(),
loaded: false,
envVars: make(map[string]string),
}
}
func (s *Settings) LoadFromEnv() error {
if host := os.Getenv(EnvPrefix + "SERVER_HOST"); host != "" {
s.Server.Host = host
}
if portStr := os.Getenv(EnvPrefix + "SERVER_PORT"); portStr != "" {
port, err := strconv.Atoi(portStr)
if err != nil {
return NewConfigError(ErrInvalidValue, fmt.Sprintf("invalid port: %s", portStr))
}
s.Server.Port = port
}
if timeoutStr := os.Getenv(EnvPrefix + "SERVER_TIMEOUT"); timeoutStr != "" {
timeout, err := time.ParseDuration(timeoutStr)
if err != nil {
return NewConfigError(ErrInvalidValue, fmt.Sprintf("invalid timeout: %s", timeoutStr))
}
s.Server.Timeout = timeout
}
if dbURL := os.Getenv(EnvPrefix + "DATABASE_URL"); dbURL != "" {
s.Database.URL = dbURL
}
if maxConnStr := os.Getenv(EnvPrefix + "DATABASE_MAX_CONNECTIONS"); maxConnStr != "" {
maxConn, err := strconv.Atoi(maxConnStr)
if err != nil {
return NewConfigError(ErrInvalidValue, fmt.Sprintf("invalid max connections: %s", maxConnStr))
}
s.Database.MaxConnections = maxConn
}
if logLevel := os.Getenv(EnvPrefix + "LOG_LEVEL"); logLevel != "" {
level, err := ParseLogLevel(logLevel)
if err != nil {
return err
}
s.Logging.Level = level
}
if logFile := os.Getenv(EnvPrefix + "LOG_FILE"); logFile != "" {
s.Logging.FilePath = &logFile
}
if metricsEnabled := os.Getenv(EnvPrefix + "FEATURE_METRICS"); metricsEnabled != "" {
s.Features.EnableMetrics = strings.ToLower(metricsEnabled) == "true"
}
if tracingEnabled := os.Getenv(EnvPrefix + "FEATURE_TRACING"); tracingEnabled != "" {
s.Features.EnableTracing = strings.ToLower(tracingEnabled) == "true"
}
s.loaded = true
return nil
}
func (s *Settings) Validate() error {
if err := s.Server.Validate(); err != nil {
return fmt.Errorf("server config error: %w", err)
}
if err := s.Database.Validate(); err != nil {
return fmt.Errorf("database config error: %w", err)
}
if err := s.Logging.Validate(); err != nil {
return fmt.Errorf("logging config error: %w", err)
}
return nil
}
func (s *Settings) DatabaseURL() string {
return s.Database.URL
}
func (s *Settings) ServerAddress() string {
return fmt.Sprintf("%s:%d", s.Server.Host, s.Server.Port)
}
func (s *Settings) IsLoaded() bool {
return s.loaded
}
type ServerConfig struct {
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
Timeout time.Duration `json:"timeout" yaml:"timeout"`
MaxConnections int `json:"max_connections" yaml:"max_connections"`
TLSEnabled bool `json:"tls_enabled" yaml:"tls_enabled"`
}
func DefaultServerConfig() ServerConfig {
return ServerConfig{
Host: DefaultHost,
Port: DefaultPort,
Timeout: 30 * time.Second,
MaxConnections: 100,
TLSEnabled: false,
}
}
func (sc *ServerConfig) Validate() error {
if sc.Host == "" {
return NewConfigError(ErrMissingRequired, "server host is required")
}
if sc.Port <= 0 || sc.Port > 65535 {
return NewConfigError(ErrInvalidValue, "server port must be between 1 and 65535")
}
if sc.Timeout <= 0 {
return NewConfigError(ErrInvalidValue, "server timeout must be positive")
}
if sc.MaxConnections <= 0 {
return NewConfigError(ErrInvalidValue, "max connections must be positive")
}
return nil
}
type DatabaseConfig struct {
URL string `json:"url" yaml:"url"`
MaxConnections int `json:"max_connections" yaml:"max_connections"`
ConnectionTimeout time.Duration `json:"connection_timeout" yaml:"connection_timeout"`
QueryTimeout time.Duration `json:"query_timeout" yaml:"query_timeout"`
SSL SSLConfig `json:"ssl" yaml:"ssl"`
}
func DefaultDatabaseConfig() DatabaseConfig {
return DatabaseConfig{
URL: DefaultDBURL,
MaxConnections: 10,
ConnectionTimeout: 5 * time.Second,
QueryTimeout: 30 * time.Second,
SSL: DefaultSSLConfig(),
}
}
func (dc *DatabaseConfig) Validate() error {
if dc.URL == "" {
return NewConfigError(ErrMissingRequired, "database URL is required")
}
if !strings.Contains(dc.URL, "://") {
return NewConfigError(ErrInvalidValue, "database URL must include protocol")
}
if dc.MaxConnections <= 0 {
return NewConfigError(ErrInvalidValue, "max connections must be positive")
}
if dc.ConnectionTimeout <= 0 {
return NewConfigError(ErrInvalidValue, "connection timeout must be positive")
}
return nil
}
type SSLConfig struct {
Enabled bool `json:"enabled" yaml:"enabled"`
CertFile string `json:"cert_file" yaml:"cert_file"`
KeyFile string `json:"key_file" yaml:"key_file"`
CAFile string `json:"ca_file" yaml:"ca_file"`
}
func DefaultSSLConfig() SSLConfig {
return SSLConfig{
Enabled: false,
}
}
type LoggingConfig struct {
Level LogLevel `json:"level" yaml:"level"`
FilePath *string `json:"file_path" yaml:"file_path"`
Console bool `json:"console" yaml:"console"`
Format string `json:"format" yaml:"format"`
}
func DefaultLoggingConfig() LoggingConfig {
return LoggingConfig{
Level: LogLevelInfo,
Console: true,
Format: "json",
}
}
func (lc *LoggingConfig) Validate() error {
if !lc.Level.IsValid() {
return NewConfigError(ErrInvalidValue, "invalid log level")
}
if lc.Format != "json" && lc.Format != "text" {
return NewConfigError(ErrInvalidValue, "log format must be 'json' or 'text'")
}
return nil
}
type LogLevel int
const (
LogLevelDebug LogLevel = iota
LogLevelInfo
LogLevelWarn
LogLevelError
)
func (ll LogLevel) String() string {
switch ll {
case LogLevelDebug:
return "debug"
case LogLevelInfo:
return "info"
case LogLevelWarn:
return "warn"
case LogLevelError:
return "error"
default:
return "unknown"
}
}
func (ll LogLevel) IsValid() bool {
return ll >= LogLevelDebug && ll <= LogLevelError
}
func ParseLogLevel(level string) (LogLevel, error) {
switch strings.ToLower(level) {
case "debug":
return LogLevelDebug, nil
case "info":
return LogLevelInfo, nil
case "warn", "warning":
return LogLevelWarn, nil
case "error":
return LogLevelError, nil
default:
return LogLevelInfo, NewConfigError(ErrInvalidValue, fmt.Sprintf("invalid log level: %s", level))
}
}
type FeatureFlags struct {
EnableMetrics bool `json:"enable_metrics" yaml:"enable_metrics"`
EnableTracing bool `json:"enable_tracing" yaml:"enable_tracing"`
ExperimentalFeatures bool `json:"experimental_features" yaml:"experimental_features"`
MaintenanceMode bool `json:"maintenance_mode" yaml:"maintenance_mode"`
}
func DefaultFeatureFlags() FeatureFlags {
return FeatureFlags{
EnableMetrics: false,
EnableTracing: false,
ExperimentalFeatures: false,
MaintenanceMode: false,
}
}
func GetEnvWithDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func ParseDurationFromString(durationStr string) (time.Duration, error) {
if durationStr == "" {
return 0, NewConfigError(ErrInvalidValue, "duration cannot be empty")
}
duration, err := time.ParseDuration(durationStr)
if err != nil {
return 0, NewConfigError(ErrInvalidValue, fmt.Sprintf("invalid duration: %s", durationStr))
}
return duration, nil
}
func LoadFromFile(filePath string) (*Settings, error) {
if filePath == "" {
return nil, NewConfigError(ErrInvalidValue, "file path cannot be empty")
}
settings := NewSettings()
fmt.Printf("Loading configuration from file: %s\n", filePath)
return settings, nil
}
func MergeSettings(base, override *Settings) *Settings {
result := *base
if override.Server.Host != DefaultHost {
result.Server.Host = override.Server.Host
}
if override.Server.Port != DefaultPort {
result.Server.Port = override.Server.Port
}
if override.Database.URL != DefaultDBURL {
result.Database.URL = override.Database.URL
}
return &result
}
type ConfigErrorCode int
const (
ErrMissingRequired ConfigErrorCode = iota
ErrInvalidValue
ErrFileNotFound
ErrParseError
ErrValidationFailed
)
type ConfigError struct {
Code ConfigErrorCode
Message string
Field string
}
func NewConfigError(code ConfigErrorCode, message string) *ConfigError {
return &ConfigError{
Code: code,
Message: message,
}
}
func NewConfigFieldError(code ConfigErrorCode, message, field string) *ConfigError {
return &ConfigError{
Code: code,
Message: message,
Field: field,
}
}
func (e *ConfigError) Error() string {
if e.Field != "" {
return fmt.Sprintf("config error [%d] in field '%s': %s", int(e.Code), e.Field, e.Message)
}
return fmt.Sprintf("config error [%d]: %s", int(e.Code), e.Message)
}
func (e *ConfigError) Is(target error) bool {
if other, ok := target.(*ConfigError); ok {
return e.Code == other.Code
}
return false
}
func init() {
fmt.Println("[INIT] Config package initialized")
}