bring back yahoo pipes!
1package config
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strconv"
8
9 "github.com/joho/godotenv"
10 "gopkg.in/yaml.v3"
11)
12
13type Config struct {
14 // Server
15 Origin string `yaml:"origin"`
16 Host string `yaml:"host"`
17 Port int `yaml:"port"`
18 Env string `yaml:"env"`
19 LogLevel string `yaml:"log_level"`
20
21 // Database
22 DatabasePath string `yaml:"db_path"`
23
24 // OAuth (Indiko)
25 IndikoURL string `yaml:"indiko_url"`
26 IndikoClientID string `yaml:"indiko_client_id"`
27 IndikoClientSecret string `yaml:"indiko_client_secret"`
28 OAuthCallbackURL string `yaml:"oauth_callback_url"`
29
30 // Session
31 SessionSecret string `yaml:"session_secret"`
32 SessionCookieName string `yaml:"session_cookie_name"`
33}
34
35// Default returns a Config with sensible defaults
36func Default() *Config {
37 return &Config{
38 Origin: "http://localhost:3001",
39 Host: "localhost",
40 Port: 3001,
41 Env: "development",
42 LogLevel: "info",
43 DatabasePath: "pipes.db",
44 IndikoURL: "http://localhost:3000",
45 OAuthCallbackURL: "http://localhost:3001/auth/callback",
46 SessionCookieName: "pipes_session",
47 }
48}
49
50// Load loads configuration from YAML file (if provided) and environment variables
51func Load(path string) (*Config, error) {
52 cfg := Default()
53
54 // Load .env file if it exists (silently ignore if not found)
55 if envPath := findEnvFile(path); envPath != "" {
56 _ = godotenv.Load(envPath)
57 }
58
59 // Load from YAML config file if provided
60 if path != "" {
61 data, err := os.ReadFile(path)
62 if err != nil {
63 return nil, fmt.Errorf("failed to read config file: %w", err)
64 }
65
66 // Expand environment variables in YAML (e.g., ${DATABASE_PATH})
67 expanded := os.Expand(string(data), func(key string) string {
68 return os.Getenv(key)
69 })
70
71 if err := yaml.Unmarshal([]byte(expanded), cfg); err != nil {
72 return nil, fmt.Errorf("failed to parse config file: %w", err)
73 }
74 }
75
76 // Apply environment variable overrides
77 applyEnvOverrides(cfg)
78
79 if err := cfg.Validate(); err != nil {
80 return nil, err
81 }
82
83 return cfg, nil
84}
85
86func (c *Config) Validate() error {
87 if c.SessionSecret == "" {
88 return fmt.Errorf("session_secret is required (set SESSION_SECRET env var)")
89 }
90
91 if c.IndikoClientID == "" {
92 return fmt.Errorf("indiko_client_id is required (set INDIKO_CLIENT_ID env var)")
93 }
94
95 if c.IndikoURL == "" {
96 return fmt.Errorf("indiko_url is required (set INDIKO_URL env var)")
97 }
98
99 return nil
100}
101
102// findEnvFile looks for .env file in the config file's directory or current directory
103func findEnvFile(configPath string) string {
104 // If config path provided, look in its directory
105 if configPath != "" {
106 dir := filepath.Dir(configPath)
107 envPath := filepath.Join(dir, ".env")
108 if _, err := os.Stat(envPath); err == nil {
109 return envPath
110 }
111 }
112
113 // Look in current directory
114 if _, err := os.Stat(".env"); err == nil {
115 return ".env"
116 }
117
118 return ""
119}
120
121// applyEnvOverrides applies environment variable overrides to config
122func applyEnvOverrides(cfg *Config) {
123 if v := os.Getenv("ORIGIN"); v != "" {
124 cfg.Origin = v
125 }
126 if v := os.Getenv("HOST"); v != "" {
127 cfg.Host = v
128 }
129 if v := os.Getenv("PORT"); v != "" {
130 if port, err := strconv.Atoi(v); err == nil {
131 cfg.Port = port
132 }
133 }
134 if v := os.Getenv("NODE_ENV"); v != "" {
135 cfg.Env = v
136 }
137 if v := os.Getenv("LOG_LEVEL"); v != "" {
138 cfg.LogLevel = v
139 }
140 if v := os.Getenv("DATABASE_PATH"); v != "" {
141 cfg.DatabasePath = v
142 }
143 if v := os.Getenv("INDIKO_URL"); v != "" {
144 cfg.IndikoURL = v
145 }
146 if v := os.Getenv("INDIKO_CLIENT_ID"); v != "" {
147 cfg.IndikoClientID = v
148 }
149 if v := os.Getenv("INDIKO_CLIENT_SECRET"); v != "" {
150 cfg.IndikoClientSecret = v
151 }
152 if v := os.Getenv("OAUTH_CALLBACK_URL"); v != "" {
153 cfg.OAuthCallbackURL = v
154 }
155 if v := os.Getenv("SESSION_SECRET"); v != "" {
156 cfg.SessionSecret = v
157 }
158 if v := os.Getenv("SESSION_COOKIE_NAME"); v != "" {
159 cfg.SessionCookieName = v
160 }
161}