package main import ( "fmt" "os" "reflect" "strings" logger "git.site.quack-lab.dev/dave/cylogger" "github.com/joho/godotenv" ) var options Options func init() { var err error options, err = LoadOptions() if err != nil { logger.Error("Failed to load options %v", err) return } } type Options struct { DBPath string `env:"DB_PATH" default:"eve-pi.db" description:"Database file path"` Port string `env:"HTTP_SERVER_PORT" default:"3000" description:"HTTP server port"` ClientID string `env:"ESI_CLIENT_ID" description:"EVE SSO client ID"` RedirectURI string `env:"ESI_REDIRECT_URI" default:"http://localhost:3000/callback" description:"EVE SSO redirect URI"` Scopes []string `env:"ESI_SCOPES" default:"esi-planets.manage_planets.v1" description:"EVE SSO scopes (space-separated)"` CacheValidity string `env:"ESI_CACHE_VALIDITY" default:"PT20M" description:"ESI cache validity in duration (e.g. PT20M for 20 minutes)"` WebhookURL string `env:"WEBHOOK_URL" description:"Webhook URL for notifications"` WebhookEmail string `env:"WEBHOOK_EMAIL" description:"Webhook authentication email"` WebhookToken string `env:"WEBHOOK_TOKEN" description:"Webhook authentication token"` LogLevel string `env:"LOG_LEVEL" default:"info" description:"Logging level (dump, trace, debug, info, warning, error)"` } func LoadOptions() (Options, error) { err := godotenv.Load() if err != nil { return Options{}, fmt.Errorf("failed to load environment variables: %w", err) } opts := Options{} optsType := reflect.TypeOf(opts) optsValue := reflect.ValueOf(&opts).Elem() for i := 0; i < optsType.NumField(); i++ { field := optsType.Field(i) fieldValue := optsValue.Field(i) envKey := field.Tag.Get("env") if envKey == "" { continue } envValue := os.Getenv(envKey) defaultValue := field.Tag.Get("default") if envValue == "" { if defaultValue == "" { return Options{}, fmt.Errorf("required environment variable %s is not set", envKey) } envValue = defaultValue } // Set the field value based on its type if err := setFieldValue(fieldValue, envValue, field.Type); err != nil { return Options{}, fmt.Errorf("invalid value for %s: %w", envKey, err) } } return opts, nil } func setFieldValue(field reflect.Value, value string, fieldType reflect.Type) error { switch fieldType.Kind() { case reflect.String: field.SetString(value) case reflect.Slice: if fieldType.Elem().Kind() == reflect.String { // Handle []string by splitting on spaces items := strings.Fields(value) slice := reflect.MakeSlice(fieldType, len(items), len(items)) for i, item := range items { slice.Index(i).SetString(item) } field.Set(slice) } default: return fmt.Errorf("unsupported field type: %s", fieldType.Kind()) } return nil } // GenerateEnvExample creates a .env.example file from the Options struct func GenerateEnvExample() error { optsType := reflect.TypeOf(Options{}) // Generate the .env.example content dynamically content := "# EVE PI Configuration\n" for i := 0; i < optsType.NumField(); i++ { field := optsType.Field(i) envKey := field.Tag.Get("env") if envKey == "" { continue } defaultValue := field.Tag.Get("default") description := field.Tag.Get("description") // Generate example value exampleValue := generateExampleValue(field.Name, envKey, defaultValue) content += fmt.Sprintf("# %s\n", description) content += fmt.Sprintf("%s=%s\n\n", envKey, exampleValue) } file, err := os.Create(".env.example") if err != nil { return err } defer file.Close() _, err = file.WriteString(content) return err } func generateExampleValue(fieldName, envKey, defaultValue string) string { // If there's a default value, use it if defaultValue != "" { return defaultValue } // If no default value, use placeholder return "your_" + strings.ToLower(envKey) + "_here" }