package config import ( "fmt" "log/slog" "strings" "github.com/spf13/viper" ) // Config represents the application configuration type Config struct { App AppConfig `mapstructure:"app"` Database DatabaseConfig `mapstructure:"database"` Tables map[string]TableConfig `mapstructure:"tables"` } // AppConfig holds application-level configuration type AppConfig struct { Name string `mapstructure:"name"` Theme string `mapstructure:"theme"` Language string `mapstructure:"language"` Port int `mapstructure:"port"` } // DatabaseConfig holds database connection settings type DatabaseConfig struct { Type string `mapstructure:"type"` Host string `mapstructure:"host"` Port int `mapstructure:"port"` User string `mapstructure:"user"` Password string `mapstructure:"password"` DBName string `mapstructure:"dbname"` Path string `mapstructure:"path"` DSN string `mapstructure:"dsn"` } // TableConfig holds table-specific configuration type TableConfig struct { Alias string `mapstructure:"alias"` Layout string `mapstructure:"layout"` PageSize int `mapstructure:"page_size"` Fields map[string]FieldConfig `mapstructure:"fields"` Features map[string]interface{} `mapstructure:"features"` Options map[string]interface{} `mapstructure:"options"` } // FieldConfig holds field-specific configuration type FieldConfig struct { Type string `mapstructure:"type"` Alias string `mapstructure:"alias"` Hidden bool `mapstructure:"hidden"` Searchable bool `mapstructure:"searchable"` MaxLength int `mapstructure:"max_length"` Length int `mapstructure:"length"` Markdown bool `mapstructure:"markdown"` Excerpt int `mapstructure:"excerpt"` Size string `mapstructure:"size"` Fit string `mapstructure:"fit"` Format string `mapstructure:"format"` Relative bool `mapstructure:"relative"` Colors map[string]string `mapstructure:"colors"` Separator string `mapstructure:"separator"` Primary bool `mapstructure:"primary"` AvatarField string `mapstructure:"avatar_field"` Suffix string `mapstructure:"suffix"` Prefix string `mapstructure:"prefix"` Options map[string]interface{} `mapstructure:"options"` } // LoadConfig loads configuration from file and environment variables func LoadConfig(configPath string) (*Config, error) { logger := slog.With("component", "config") // Set default values viper.SetDefault("app.name", "Database Render") viper.SetDefault("app.theme", "modern") viper.SetDefault("app.language", "zh-CN") viper.SetDefault("app.port", 8080) // Database defaults viper.SetDefault("database.type", "sqlite") viper.SetDefault("database.host", "localhost") viper.SetDefault("database.port", 3306) viper.SetDefault("database.dbname", "testdb") // Set config file if configPath != "" { viper.SetConfigFile(configPath) } else { viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") viper.AddConfigPath("./config") viper.AddConfigPath("/etc/database-render") } // Environment variables viper.SetEnvPrefix("DR") viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // Read config if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { logger.Error("failed to read config file", "error", err) return nil, fmt.Errorf("failed to read config: %w", err) } logger.Warn("config file not found, using defaults") } // Parse database DSN if provided if dsn := viper.GetString("database"); dsn != "" { if err := parseDSN(dsn); err != nil { return nil, err } } // Unmarshal config var config Config if err := viper.Unmarshal(&config); err != nil { logger.Error("failed to unmarshal config", "error", err) return nil, fmt.Errorf("failed to unmarshal config: %w", err) } // Validate config if err := validateConfig(&config); err != nil { logger.Error("config validation failed", "error", err) return nil, fmt.Errorf("config validation failed: %w", err) } logger.Info("configuration loaded successfully", "config_file", viper.ConfigFileUsed(), "tables_count", len(config.Tables)) return &config, nil } // parseDSN parses database connection string func parseDSN(dsn string) error { if strings.HasPrefix(dsn, "sqlite://") { path := strings.TrimPrefix(dsn, "sqlite://") viper.Set("database.type", "sqlite") viper.Set("database.path", path) return nil } if strings.HasPrefix(dsn, "mysql://") { dsn = strings.TrimPrefix(dsn, "mysql://") parts := strings.Split(dsn, "@") if len(parts) != 2 { return fmt.Errorf("invalid mysql dsn format") } userPass := strings.Split(parts[0], ":") if len(userPass) != 2 { return fmt.Errorf("invalid mysql user:pass format") } hostPort := strings.Split(parts[1], "/") if len(hostPort) != 2 { return fmt.Errorf("invalid mysql host/db format") } host := strings.Split(hostPort[0], ":") if len(host) == 2 { viper.Set("database.port", host[1]) } viper.Set("database.type", "mysql") viper.Set("database.user", userPass[0]) viper.Set("database.password", userPass[1]) viper.Set("database.host", host[0]) viper.Set("database.dbname", hostPort[1]) return nil } if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { viper.Set("database.type", "postgres") viper.Set("database.dsn", dsn) return nil } return fmt.Errorf("unsupported database dsn format") } // validateConfig validates the loaded configuration func validateConfig(config *Config) error { if len(config.Tables) == 0 { return fmt.Errorf("no tables configured") } for name, table := range config.Tables { if name == "" { return fmt.Errorf("table name cannot be empty") } if table.Alias == "" { table.Alias = name } if table.Layout == "" { table.Layout = "card" } if table.PageSize == 0 { table.PageSize = 12 } } return nil } // GetTableConfig returns configuration for a specific table func (c *Config) GetTableConfig(tableName string) (*TableConfig, bool) { config, exists := c.Tables[tableName] if !exists { return nil, false } return &config, true } // GetDatabaseDSN returns the database connection string func (c *Config) GetDatabaseDSN() string { switch c.Database.Type { case "sqlite": return c.Database.Path case "mysql": return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", c.Database.User, c.Database.Password, c.Database.Host, c.Database.Port, c.Database.DBName) case "postgres": if c.Database.DSN != "" { return c.Database.DSN } return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", c.Database.Host, c.Database.Port, c.Database.User, c.Database.Password, c.Database.DBName) default: return c.Database.DSN } }