package config import ( "fmt" "os" "time" "gopkg.in/yaml.v3" ) type Config struct { Server ServerConfig `yaml:"server"` Origin OriginConfig `yaml:"origin"` Site SiteConfig `yaml:"site"` Wedding WeddingConfig `yaml:"wedding"` Storage StorageConfig `yaml:"storage"` Email EmailConfig `yaml:"email"` Upload UploadConfig `yaml:"upload"` Webhook WebhookConfig `yaml:"webhook"` } type OriginConfig struct { ServiceKey string `yaml:"service_key"` URL string `yaml:"url"` Version string `yaml:"version"` } type ServerConfig struct { Port int `yaml:"port"` } type SiteConfig struct { URL string `yaml:"url"` } type WeddingConfig struct { Date time.Time `yaml:"date"` CeremonyTime string `yaml:"ceremony_time"` RSVPBy time.Time `yaml:"rsvp_by"` VenueName string `yaml:"venue_name"` VenueAddress string `yaml:"venue_address"` RSVPPhone string `yaml:"rsvp_phone"` } type StorageConfig struct { DataDir string `yaml:"data_dir"` } type EmailConfig struct { SMTPHost string `yaml:"smtp_host"` SMTPPort int `yaml:"smtp_port"` SMTPUser string `yaml:"smtp_user"` SMTPPass string `yaml:"smtp_pass"` From string `yaml:"from"` To string `yaml:"to"` } type UploadConfig struct { MaxSizeMB int `yaml:"max_size_mb"` MaxFiles int `yaml:"max_files"` } // WebhookConfig is entirely optional: each URL is only called if set, so a // deployment can wire up RSVP notifications without touching photo uploads // (or either at all). type WebhookConfig struct { RSVPURL string `yaml:"rsvp_url"` PhotoURL string `yaml:"photo_url"` Secret string `yaml:"secret"` // signs the payload via HMAC-SHA256, sent in X-Webhook-Signature Headers map[string]string `yaml:"headers"` // extra headers sent with every webhook request, e.g. Authorization TimeoutSeconds int `yaml:"timeout_seconds"` } // Path resolves the config file location: $CONFIG_PATH, or ./config.yaml. func Path() string { if p := os.Getenv("CONFIG_PATH"); p != "" { return p } return "config.yaml" } func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config %s: %w", path, err) } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parsing config %s: %w", path, err) } if cfg.Server.Port == 0 { cfg.Server.Port = 8080 } if cfg.Storage.DataDir == "" { cfg.Storage.DataDir = "./data" } if cfg.Upload.MaxSizeMB == 0 { cfg.Upload.MaxSizeMB = 15 } if cfg.Upload.MaxFiles == 0 { cfg.Upload.MaxFiles = 10 } if cfg.Webhook.TimeoutSeconds == 0 { cfg.Webhook.TimeoutSeconds = 5 } if cfg.Wedding.Date.IsZero() { return nil, fmt.Errorf("wedding.date is required in %s", path) } if cfg.Wedding.RSVPBy.IsZero() { return nil, fmt.Errorf("wedding.rsvp_by is required in %s", path) } return &cfg, nil }