Files
database_render/internal/config/config.go
2025-08-05 17:26:59 +08:00

237 lines
7.0 KiB
Go

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"`
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
}
}