diff --git a/main.go b/main.go index 12c83bc..073d9a5 100644 --- a/main.go +++ b/main.go @@ -6,9 +6,10 @@ import ( "os" "os/signal" - logger "git.site.quack-lab.dev/dave/cylogger" wh "go-eve-pi/webhook" + logger "git.site.quack-lab.dev/dave/cylogger" + "github.com/fasthttp/router" "github.com/valyala/fasthttp" ) @@ -16,7 +17,19 @@ import ( var webhook wh.Webhook func main() { + // Add flag for generating .env.example + help := flag.Bool("help", false, "Generate .env.example file") flag.Parse() + + if *help { + if err := GenerateEnvExample(); err != nil { + logger.Error("Failed to generate .env.example: %v", err) + os.Exit(1) + } + logger.Info("Generated .env.example file successfully") + os.Exit(0) + } + logger.InitFlag() logger.Info("Starting Eve PI") @@ -30,8 +43,8 @@ func main() { logger.Error("Failed to create SSO instance %v", err) return } - - webhook = wh.NewZulipWebhook(options.ZulipURL, options.ZulipEmail, options.ZulipToken) + + webhook = wh.NewZulipWebhook(options.WebhookURL, options.WebhookEmail, options.WebhookToken) // Setup fasthttp router r := router.New() diff --git a/options.go b/options.go index a04499a..56994be 100644 --- a/options.go +++ b/options.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "reflect" "strings" logger "git.site.quack-lab.dev/dave/cylogger" @@ -21,63 +22,122 @@ func init() { } type Options struct { - DBPath string - Port string + 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"` - ESIScopes []string - ESIRedirectURI string - ESIClientID string + ClientID string `env:"ESI_CLIENT_ID" required:"true" description:"EVE SSO client ID"` + RedirectURI string `env:"ESI_REDIRECT_URI" required:"true" description:"EVE SSO redirect URI"` + Scopes []string `env:"ESI_SCOPES" default:"esi-planets.manage_planets.v1" description:"EVE SSO scopes (space-separated)"` - WebhookURL string - WebhookEmail string - WebhookToken string + WebhookURL string `env:"WEBHOOK_URL" required:"true" description:"Webhook URL for notifications"` + WebhookEmail string `env:"WEBHOOK_EMAIL" required:"true" description:"Webhook authentication email"` + WebhookToken string `env:"WEBHOOK_TOKEN" required:"true" description:"Webhook authentication token"` + + LogLevel string `env:"LOG_LEVEL" default:"info" description:"Logging level (debug, info, warning, error)"` } func LoadOptions() (Options, error) { - // Load environment variables strictly from .env file (fail if there's an error loading) - if err := godotenv.Load(); err != nil { - return Options{}, fmt.Errorf("error loading .env file: %w", err) + err := godotenv.Load() + if err != nil { + return Options{}, fmt.Errorf("failed to load environment variables: %w", err) } - dbPath := getOrDefault("DB_PATH", "eve-pi.db") - port := getOrDefault("HTTP_SERVER_PORT", "3000") + opts := Options{} + optsType := reflect.TypeOf(opts) + optsValue := reflect.ValueOf(&opts).Elem() - clientID := getOrFatal("ESI_CLIENT_ID") - redirectURI := getOrFatal("ESI_REDIRECT_URI") - rawScopes := getOrDefault("ESI_SCOPES", "esi-planets.manage_planets.v1") - scopes := strings.Fields(rawScopes) + for i := 0; i < optsType.NumField(); i++ { + field := optsType.Field(i) + fieldValue := optsValue.Field(i) - webhookUrl := getOrFatal("WEBHOOK_URL") - webhookEmail := getOrFatal("WEBHOOK_EMAIL") - webhookToken := getOrFatal("WEBHOOK_TOKEN") + envKey := field.Tag.Get("env") + if envKey == "" { + continue + } - return Options{ - DBPath: dbPath, - Port: port, + envValue := os.Getenv(envKey) + required := field.Tag.Get("required") == "true" + defaultValue := field.Tag.Get("default") - ESIClientID: clientID, - ESIRedirectURI: redirectURI, - ESIScopes: scopes, + if envValue == "" { + if required { + return Options{}, fmt.Errorf("required environment variable %s is not set", envKey) + } + if defaultValue != "" { + envValue = defaultValue + } + } - WebhookURL: webhookUrl, - WebhookEmail: webhookEmail, - WebhookToken: webhookToken, - }, nil -} - -func getOrFatal(key string) string { - value := os.Getenv(key) - if value == "" { - logger.Error("Environment variable %s is required", key) - os.Exit(1) + // 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 value + + return opts, nil } -func getOrDefault(key, defaultValue string) string { - value := os.Getenv(key) - if value == "" { +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 + } + + required := field.Tag.Get("required") == "true" + defaultValue := field.Tag.Get("default") + description := field.Tag.Get("description") + + // Generate example value + exampleValue := generateExampleValue(field.Name, envKey, defaultValue, required) + + 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, required bool) string { + // If there's a default value, use it + if defaultValue != "" { return defaultValue } - return value + + // If no default value, use placeholder + return "your_" + strings.ToLower(envKey) + "_here" }