package config import ( "errors" "fmt" "os" "path/filepath" "strings" "time" "github.com/ilyakaznacheev/cleanenv" ) type Config struct { App AppConfig `yaml:"app"` Server ServerConfig `yaml:"server"` Database DatabaseConfig `yaml:"database"` Auth AuthConfig `yaml:"auth"` Seed SeedConfig `yaml:"seed"` MultiTable MultiTableConfig `yaml:"multitable"` } type AppConfig struct { Name string `yaml:"name" env:"APP_NAME" env-default:"iqudo-top1"` Env string `yaml:"env" env:"APP_ENV" env-default:"development"` } type ServerConfig struct { Host string `yaml:"host" env:"APP_SERVER_HOST" env-default:"127.0.0.1"` Port int `yaml:"port" env:"APP_SERVER_PORT" env-default:"8080"` ReadTimeout time.Duration `yaml:"read_timeout" env:"APP_SERVER_READ_TIMEOUT" env-default:"10s"` WriteTimeout time.Duration `yaml:"write_timeout" env:"APP_SERVER_WRITE_TIMEOUT" env-default:"20s"` ShutdownTimeout time.Duration `yaml:"shutdown_timeout" env:"APP_SERVER_SHUTDOWN_TIMEOUT" env-default:"10s"` } func (c ServerConfig) Address() string { return fmt.Sprintf("%s:%d", c.Host, c.Port) } type DatabaseConfig struct { Host string `yaml:"host" env:"APP_DATABASE_HOST" env-default:"127.0.0.1"` Port int `yaml:"port" env:"APP_DATABASE_PORT" env-default:"3306"` Name string `yaml:"name" env:"APP_DATABASE_NAME"` User string `yaml:"user" env:"APP_DATABASE_USER"` Password string `yaml:"password" env:"APP_DATABASE_PASSWORD"` MaxIdleConnections int `yaml:"max_idle_connections" env:"APP_DATABASE_MAX_IDLE_CONNECTIONS" env-default:"10"` MaxOpenConnections int `yaml:"max_open_connections" env:"APP_DATABASE_MAX_OPEN_CONNECTIONS" env-default:"40"` ConnectionMaxLifetime time.Duration `yaml:"connection_max_lifetime" env:"APP_DATABASE_CONNECTION_MAX_LIFETIME" env-default:"30m"` } func (c DatabaseConfig) DSN() string { return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true", c.User, c.Password, c.Host, c.Port, c.Name) } type AuthConfig struct { JWTSecret string `yaml:"jwt_secret" env:"APP_AUTH_JWT_SECRET"` AccessTokenTTL time.Duration `yaml:"access_token_ttl" env:"APP_AUTH_ACCESS_TOKEN_TTL" env-default:"2h"` RefreshTokenTTL time.Duration `yaml:"refresh_token_ttl" env:"APP_AUTH_REFRESH_TOKEN_TTL" env-default:"168h"` } type SeedConfig struct { AdminUsername string `yaml:"admin_username" env:"APP_SEED_ADMIN_USERNAME"` AdminPassword string `yaml:"admin_password" env:"APP_SEED_ADMIN_PASSWORD"` AdminDisplayName string `yaml:"admin_display_name" env:"APP_SEED_ADMIN_DISPLAY_NAME"` } type MultiTableConfig struct { Enabled bool `yaml:"enabled" env:"APP_MULTITABLE_ENABLED" env-default:"false"` BaseURL string `yaml:"base_url" env:"APP_MULTITABLE_BASE_URL" env-default:"https://table.iwork-ai.com/open/v1"` APIKey string `yaml:"api_key" env:"APP_MULTITABLE_API_KEY"` SyncInterval time.Duration `yaml:"sync_interval" env:"APP_MULTITABLE_SYNC_INTERVAL" env-default:"5s"` RequestTimeout time.Duration `yaml:"request_timeout" env:"APP_MULTITABLE_REQUEST_TIMEOUT" env-default:"10s"` BatchSize int `yaml:"batch_size" env:"APP_MULTITABLE_BATCH_SIZE" env-default:"20"` MaxAttempts int `yaml:"max_attempts" env:"APP_MULTITABLE_MAX_ATTEMPTS" env-default:"12"` Tables MultiTableTables `yaml:"tables"` } type MultiTableTables struct { Scenarios uint64 `yaml:"scenarios" env:"APP_MULTITABLE_TABLES_SCENARIOS"` ScenarioFields uint64 `yaml:"scenario_fields" env:"APP_MULTITABLE_TABLES_SCENARIO_FIELDS"` ScenarioRules uint64 `yaml:"scenario_rules" env:"APP_MULTITABLE_TABLES_SCENARIO_RULES"` SOPs uint64 `yaml:"sops" env:"APP_MULTITABLE_TABLES_SOPS"` SOPNodes uint64 `yaml:"sop_nodes" env:"APP_MULTITABLE_TABLES_SOP_NODES"` SOPEdges uint64 `yaml:"sop_edges" env:"APP_MULTITABLE_TABLES_SOP_EDGES"` KnowledgeItems uint64 `yaml:"knowledge_items" env:"APP_MULTITABLE_TABLES_KNOWLEDGE_ITEMS"` KnowledgeRelations uint64 `yaml:"knowledge_relations" env:"APP_MULTITABLE_TABLES_KNOWLEDGE_RELATIONS"` Runs uint64 `yaml:"runs" env:"APP_MULTITABLE_TABLES_RUNS"` Feedback uint64 `yaml:"feedback" env:"APP_MULTITABLE_TABLES_FEEDBACK"` } type LoadOptions struct { Environment string ConfigDir string ConfigFile string } func Load(options LoadOptions) (Config, error) { configDir := options.ConfigDir if configDir == "" { configDir = "configs" } environment := normalizeEnvironment(options.Environment) if environment == "" { environment = normalizeEnvironment(os.Getenv("APP_ENV")) } if options.ConfigFile != "" && environment == "" { environment = normalizeEnvironment(environmentFromConfigFile(options.ConfigFile)) } var cfg Config basePath := filepath.Join(configDir, "config.yml") if err := cleanenv.ReadConfig(basePath, &cfg); err != nil { return Config{}, fmt.Errorf("load base config %s: %w", basePath, err) } if options.ConfigFile != "" { profilePath := options.ConfigFile if !filepath.IsAbs(profilePath) { profilePath = filepath.Join(configDir, profilePath) } if err := cleanenv.ReadConfig(profilePath, &cfg); err != nil { return Config{}, fmt.Errorf("load config file %s: %w", profilePath, err) } } else if environment == "test" || environment == "prod" { profilePath := filepath.Join(configDir, "config."+environment+".yml") if err := cleanenv.ReadConfig(profilePath, &cfg); err != nil { return Config{}, fmt.Errorf("load profile config %s: %w", profilePath, err) } } if err := cleanenv.ReadEnv(&cfg); err != nil { return Config{}, fmt.Errorf("load environment variables: %w", err) } if environment != "" { cfg.App.Env = environment } if err := cfg.Validate(); err != nil { return Config{}, err } return cfg, nil } func environmentFromConfigFile(path string) string { name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) return strings.TrimPrefix(name, "config.") } func normalizeEnvironment(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "production", "prod": return "prod" case "testing", "test": return "test" case "development", "dev", "local": return "development" default: return strings.ToLower(strings.TrimSpace(value)) } } func (c Config) Validate() error { if c.Database.Name == "" || c.Database.User == "" { return errors.New("database name and user are required") } if len(c.Auth.JWTSecret) < 16 { return errors.New("auth.jwt_secret must contain at least 16 characters") } if c.Server.Port < 1 || c.Server.Port > 65535 { return errors.New("server.port must be between 1 and 65535") } if c.App.Env == "prod" && c.Database.Password == "" { return errors.New("database password is required in production") } if c.MultiTable.Enabled { if !strings.HasPrefix(c.MultiTable.BaseURL, "https://") { return errors.New("multitable.base_url must use https when multitable is enabled") } if strings.TrimSpace(c.MultiTable.APIKey) == "" { return errors.New("multitable.api_key is required when multitable is enabled") } if c.MultiTable.SyncInterval <= 0 || c.MultiTable.RequestTimeout <= 0 || c.MultiTable.BatchSize < 1 || c.MultiTable.MaxAttempts < 1 { return errors.New("multitable retry and timeout settings must be positive") } ids := []uint64{ c.MultiTable.Tables.Scenarios, c.MultiTable.Tables.ScenarioFields, c.MultiTable.Tables.ScenarioRules, c.MultiTable.Tables.SOPs, c.MultiTable.Tables.SOPNodes, c.MultiTable.Tables.SOPEdges, c.MultiTable.Tables.KnowledgeItems, c.MultiTable.Tables.KnowledgeRelations, c.MultiTable.Tables.Runs, c.MultiTable.Tables.Feedback, } for _, id := range ids { if id == 0 { return errors.New("all multitable table IDs are required when multitable is enabled") } } } return nil }