goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/config/config.go (about) 1 package config 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/fs" 7 "os" 8 "reflect" 9 "strings" 10 "sync" 11 12 "goyave.dev/goyave/v5/util/errors" 13 "goyave.dev/goyave/v5/util/fsutil/osfs" 14 ) 15 16 type object map[string]any 17 18 type readFunc func(string) (object, error) 19 20 // Config structure holding a configuration that should be used for a single 21 // instance of `goyave.Server`. 22 // 23 // This structure is not protected for safe concurrent access in order to increase 24 // performance. Therefore, you should never use the `Set()` function when the configuration 25 // is in use by an already running server. 26 type Config struct { 27 config object 28 } 29 30 // Error returned when the configuration could not 31 // be loaded or is invalid. 32 // Can be unwraped to get the original error. 33 type Error struct { 34 err error 35 } 36 37 func (e *Error) Error() string { 38 return fmt.Sprintf("Config error: %s", e.err.Error()) 39 } 40 41 func (e *Error) Unwrap() error { 42 return e.err 43 } 44 45 type loader struct { 46 defaults object 47 mu sync.RWMutex 48 } 49 50 var defaultLoader = &loader{ 51 defaults: configDefaults, 52 } 53 54 // Register a new config entry and its validation. 55 // 56 // Each module should register its config entries in an "init()" 57 // function, even if they don't have a default value, in order to 58 // ensure they will be validated. 59 // Each module should use its own category and use a name both expressive 60 // and unique to avoid collisions. 61 // For example, the "auth" package registers, among others, "auth.basic.username" 62 // and "auth.jwt.expiry", thus creating a category for its package, and two subcategories 63 // for its features. 64 // 65 // To register an entry without a default value (only specify how it 66 // will be validated), set "Entry.Value" to "nil". 67 // 68 // Panics if an entry already exists for this key and is not identical to the 69 // one passed as parameter of this function. On the other hand, if the entries 70 // are identical, no conflict is expected so the configuration is left in its 71 // current state. 72 func Register(key string, entry Entry) { 73 defaultLoader.register(key, entry) 74 } 75 76 func (l *loader) register(key string, entry Entry) { 77 l.mu.Lock() 78 defer l.mu.Unlock() 79 category, entryKey, exists := walk(l.defaults, key) 80 if exists { 81 if !reflect.DeepEqual(&entry, category[entryKey].(*Entry)) { 82 panic(errors.Errorf("attempted to override registered config entry %q", key)) 83 } 84 } else { 85 category[entryKey] = &entry 86 } 87 } 88 89 func (l *loader) loadFrom(fs fs.FS, path string) (*Config, error) { 90 return l.load(func(_ string) (object, error) { return l.readConfigFile(fs, path) }, path) 91 } 92 93 func (l *loader) loadJSON(cfg string) (*Config, error) { 94 return l.load(l.readString, cfg) 95 } 96 97 func (l *loader) load(readFunc readFunc, source string) (*Config, error) { 98 l.mu.Lock() 99 defer l.mu.Unlock() 100 config := make(object, len(l.defaults)) 101 loadDefaults(l.defaults, config) 102 103 if readFunc != nil { 104 conf, err := readFunc(source) 105 if err != nil { 106 return nil, errors.New(&Error{err}) 107 } 108 109 if err := override(conf, config); err != nil { 110 return nil, errors.New(&Error{err}) 111 } 112 } 113 114 if err := config.validate(""); err != nil { 115 return nil, errors.New(&Error{err}) 116 } 117 118 return &Config{ 119 config: config, 120 }, nil 121 } 122 123 // Load loads the config.json file in the current working directory. 124 // If the "GOYAVE_ENV" env variable is set, the config file will be picked like so: 125 // - "production": "config.production.json" 126 // - "test": "config.test.json" 127 // - By default: "config.json" 128 func Load() (*Config, error) { 129 return defaultLoader.loadFrom(&osfs.FS{}, getConfigFilePath()) 130 } 131 132 // LoadDefault loads default config. 133 func LoadDefault() *Config { 134 cfg, _ := defaultLoader.load(nil, "") 135 return cfg 136 } 137 138 // LoadFrom loads a config file from the given path. 139 func LoadFrom(path string) (*Config, error) { 140 return defaultLoader.loadFrom(&osfs.FS{}, path) 141 } 142 143 // LoadJSON load a configuration file from raw JSON. Can be used in combination with 144 // Go's embed directive. 145 // 146 // var ( 147 // //go:embed config.json 148 // cfgJSON string 149 // ) 150 // 151 // func main() { 152 // cfg, err := config.LoadJSON(cfgJSON) 153 // if err != nil { 154 // fmt.Fprintln(os.Stderr, err.(*errors.Error).String()) 155 // os.Exit(1) 156 // } 157 // 158 // server, err := goyave.New(goyave.Options{Config: cfg}) 159 // if err != nil { 160 // fmt.Fprintln(os.Stderr, err.(*errors.Error).String()) 161 // os.Exit(1) 162 // } 163 // 164 // // ... 165 // } 166 func LoadJSON(cfg string) (*Config, error) { 167 return defaultLoader.loadJSON(cfg) 168 } 169 170 func getConfigFilePath() string { 171 env := strings.ToLower(os.Getenv("GOYAVE_ENV")) 172 if env == "local" || env == "localhost" || env == "" { 173 return "config.json" 174 } 175 return "config." + env + ".json" 176 } 177 178 func (l *loader) readConfigFile(filesystem fs.FS, file string) (o object, err error) { 179 var configFile fs.File 180 o = make(object, len(l.defaults)) 181 configFile, err = filesystem.Open(file) 182 183 if err == nil { 184 defer func() { 185 e := configFile.Close() 186 if err == nil && e != nil { 187 err = errors.New(e) 188 } 189 }() 190 jsonParser := json.NewDecoder(configFile) 191 err = errors.New(jsonParser.Decode(&o)) 192 } else { 193 err = errors.New(err) 194 } 195 196 return 197 } 198 199 func (l *loader) readString(str string) (object, error) { 200 conf := make(object, len(l.defaults)) 201 if err := json.NewDecoder(strings.NewReader(str)).Decode(&conf); err != nil { 202 return nil, err 203 } 204 return conf, nil 205 } 206 207 // walk the config using the key. Returns the deepest category, the entry key 208 // with its path stripped ("app.name" -> "name") and true if the entry already 209 // exists, false if it's not registered. 210 func walk(currentCategory object, key string) (object, string, bool) { 211 if key == "" { 212 panic(errors.New("empty key is not allowed")) 213 } 214 215 if key[len(key)-1:] == "." { 216 panic(errors.New("keys ending with a dot are not allowed")) 217 } 218 219 start := 0 220 dotIndex := strings.Index(key, ".") 221 if dotIndex == -1 { 222 dotIndex = len(key) 223 } 224 for catKey := key[start:dotIndex]; ; catKey = key[start:dotIndex] { 225 entry, ok := currentCategory[catKey] 226 if !ok { 227 // If categories are missing, create them 228 currentCategory = createMissingCategories(currentCategory, key[start:]) 229 i := strings.LastIndex(key, ".") 230 if i == -1 { 231 catKey = key 232 } else { 233 catKey = key[i+1:] 234 } 235 236 // Entry doesn't exist and is not registered 237 return currentCategory, catKey, false 238 } 239 240 if category, ok := entry.(object); ok { 241 currentCategory = category 242 } else { 243 if dotIndex < len(key) { 244 panic(errors.Errorf("attempted to add an entry to non-category %q", key[:dotIndex])) 245 } 246 247 // Entry exists 248 return currentCategory, catKey, true 249 } 250 251 if dotIndex+1 <= len(key) { 252 start = dotIndex + 1 253 newDotIndex := strings.Index(key[start:], ".") 254 if newDotIndex == -1 { 255 dotIndex = len(key) 256 } else { 257 dotIndex = newDotIndex + start 258 } 259 } else { 260 break 261 } 262 } 263 264 panic(errors.Errorf("attempted to replace the %q category with an entry", key)) 265 } 266 267 // createMissingCategories based on the key path, starting at the given index. 268 // Doesn't create anything is not needed. 269 // Returns the deepest category created, or the provided object if nothing has 270 // been created. 271 func createMissingCategories(currentCategory object, path string) object { 272 start := 0 273 dotIndex := strings.Index(path, ".") 274 if dotIndex == -1 { 275 return currentCategory 276 } 277 for catKey := path[start:dotIndex]; ; catKey = path[start:dotIndex] { 278 newCategory := object{} 279 currentCategory[catKey] = newCategory 280 currentCategory = newCategory 281 282 if dotIndex+1 <= len(path) { 283 start = dotIndex + 1 284 newDotIndex := strings.Index(path[start:], ".") 285 if newDotIndex == -1 { 286 return currentCategory 287 } 288 dotIndex = newDotIndex + start 289 } 290 } 291 } 292 293 func override(src object, dst object) error { 294 for k, v := range src { 295 if obj, ok := v.(map[string]any); ok { 296 if dstObj, ok := dst[k]; !ok { 297 dst[k] = make(object, len(obj)) 298 } else if _, ok := dstObj.(object); !ok { 299 // Conflict: destination is not a category 300 return fmt.Errorf("\n\t- cannot override entry %q with a category", k) 301 } 302 if err := override(obj, dst[k].(object)); err != nil { 303 return err 304 } 305 } else if entry, ok := dst[k]; ok { 306 e, ok := entry.(*Entry) 307 if !ok { 308 // Conflict: override category with an entry 309 return fmt.Errorf("\n\t- cannot override category %q with an entry", k) 310 } 311 e.Value = v 312 } else { 313 // If entry doesn't exist (and is not registered), 314 // register it with the type of the type given here 315 // and "any" authorized values. 316 dst[k] = makeEntryFromValue(v) 317 } 318 } 319 return nil 320 } 321 322 func (o object) validate(key string) error { 323 message := "" 324 valid := true 325 for k, entry := range o { 326 var subKey string 327 if key == "" { 328 subKey = k 329 } else { 330 subKey = key + "." + k 331 } 332 if category, ok := entry.(object); ok { 333 if err := category.validate(subKey); err != nil { 334 message += err.Error() 335 valid = false 336 } 337 } else if err := entry.(*Entry).validate(subKey); err != nil { 338 message += "\n\t- " + err.Error() 339 valid = false 340 } 341 } 342 343 if !valid { 344 return fmt.Errorf(message) 345 } 346 return nil 347 } 348 349 // Get a config entry using a dot-separated path. 350 // Panics if the entry doesn't exist. 351 func (c *Config) Get(key string) any { 352 if val, ok := c.get(key); ok { 353 return val 354 } 355 356 panic(errors.Errorf("config entry \"%s\" doesn't exist", key)) 357 } 358 359 func (c *Config) get(key string) (any, bool) { 360 currentCategory := c.config 361 start := 0 362 dotIndex := strings.Index(key, ".") 363 if dotIndex == -1 { 364 dotIndex = len(key) 365 } 366 for path := key[start:dotIndex]; ; path = key[start:dotIndex] { 367 entry, ok := currentCategory[path] 368 if !ok { 369 break 370 } 371 372 if category, ok := entry.(object); ok { 373 currentCategory = category 374 } else { 375 val := entry.(*Entry).Value 376 return val, val != nil // nil means unset 377 } 378 379 if dotIndex+1 <= len(key) { 380 start = dotIndex + 1 381 newDotIndex := strings.Index(key[start:], ".") 382 if newDotIndex == -1 { 383 dotIndex = len(key) 384 } else { 385 dotIndex = newDotIndex + start 386 } 387 } 388 } 389 return nil, false 390 } 391 392 // GetString a config entry as string. 393 // Panics if entry is not a string or if it doesn't exist. 394 func (c *Config) GetString(key string) string { 395 str, ok := c.Get(key).(string) 396 if !ok { 397 panic(errors.Errorf("config entry \"%s\" is not a string", key)) 398 } 399 return str 400 } 401 402 // GetBool a config entry as bool. 403 // Panics if entry is not a bool or if it doesn't exist. 404 func (c *Config) GetBool(key string) bool { 405 val, ok := c.Get(key).(bool) 406 if !ok { 407 panic(errors.Errorf("config entry \"%s\" is not a bool", key)) 408 } 409 return val 410 } 411 412 // GetInt a config entry as int. 413 // Panics if entry is not an int or if it doesn't exist. 414 func (c *Config) GetInt(key string) int { 415 val, ok := c.Get(key).(int) 416 if !ok { 417 panic(errors.Errorf("config entry \"%s\" is not an int", key)) 418 } 419 return val 420 } 421 422 // GetFloat a config entry as float64. 423 // Panics if entry is not a float64 or if it doesn't exist. 424 func (c *Config) GetFloat(key string) float64 { 425 val, ok := c.Get(key).(float64) 426 if !ok { 427 panic(errors.Errorf("config entry \"%s\" is not a float64", key)) 428 } 429 return val 430 } 431 432 // GetStringSlice a config entry as []string. 433 // Panics if entry is not a string slice or if it doesn't exist. 434 func (c *Config) GetStringSlice(key string) []string { 435 str, ok := c.Get(key).([]string) 436 if !ok { 437 panic(errors.Errorf("config entry \"%s\" is not a string slice", key)) 438 } 439 return str 440 } 441 442 // GetBoolSlice a config entry as []bool. 443 // Panics if entry is not a bool slice or if it doesn't exist. 444 func (c *Config) GetBoolSlice(key string) []bool { 445 str, ok := c.Get(key).([]bool) 446 if !ok { 447 panic(errors.Errorf("config entry \"%s\" is not a bool slice", key)) 448 } 449 return str 450 } 451 452 // GetIntSlice a config entry as []int. 453 // Panics if entry is not an int slice or if it doesn't exist. 454 func (c *Config) GetIntSlice(key string) []int { 455 str, ok := c.Get(key).([]int) 456 if !ok { 457 panic(errors.Errorf("config entry \"%s\" is not an int slice", key)) 458 } 459 return str 460 } 461 462 // GetFloatSlice a config entry as []float64. 463 // Panics if entry is not a float slice or if it doesn't exist. 464 func (c *Config) GetFloatSlice(key string) []float64 { 465 str, ok := c.Get(key).([]float64) 466 if !ok { 467 panic(errors.Errorf("config entry \"%s\" is not a float64 slice", key)) 468 } 469 return str 470 } 471 472 // Has check if a config entry exists. 473 func (c *Config) Has(key string) bool { 474 _, ok := c.get(key) 475 return ok 476 } 477 478 // Set a config entry. 479 // The change is temporary and will not be saved for next boot. 480 // Use "nil" to unset a value. 481 // 482 // - A category cannot be replaced with an entry. 483 // - An entry cannot be replaced with a category. 484 // - New categories can be created with they don't already exist. 485 // - New entries can be created if they don't already exist. This new entry 486 // will be subsequently validated using the type of its initial value and 487 // have an empty slice as authorized values (meaning it can have any value of its type) 488 // 489 // Panics and revert changes in case of error. 490 // 491 // This operation is not concurrently safe and should not be used when the configuration 492 // is in use by an already running server. 493 func (c *Config) Set(key string, value any) { 494 category, entryKey, exists := walk(c.config, key) 495 if exists { 496 entry := category[entryKey].(*Entry) 497 previous := entry.Value 498 entry.Value = value 499 if err := entry.validate(key); err != nil { 500 entry.Value = previous 501 panic(err) 502 } 503 category[entryKey] = entry 504 } else { 505 category[entryKey] = makeEntryFromValue(value) 506 } 507 }