github.com/System-Glitch/goyave/v2@v2.10.3-0.20200819142921-51011e75d504/config/config.go (about) 1 package config 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "reflect" 8 "strconv" 9 "strings" 10 "sync" 11 12 "github.com/System-Glitch/goyave/v2/helper" 13 ) 14 15 type object map[string]interface{} 16 17 // Entry is the internal reprensentation of a config entry. 18 // It contains the entry value, its expected type (for validation) 19 // and a slice of authorized values (for validation too). If this slice 20 // is empty, it means any value can be used, provided it is of the correct type. 21 type Entry struct { 22 Value interface{} 23 Type reflect.Kind 24 AuthorizedValues []interface{} // Leave empty for "any" 25 } 26 27 var config object 28 29 var configDefaults object = object{ 30 "app": object{ 31 "name": &Entry{"goyave", reflect.String, []interface{}{}}, 32 "environment": &Entry{"localhost", reflect.String, []interface{}{}}, 33 "debug": &Entry{true, reflect.Bool, []interface{}{}}, 34 "defaultLanguage": &Entry{"en-US", reflect.String, []interface{}{}}, 35 }, 36 "server": object{ 37 "host": &Entry{"127.0.0.1", reflect.String, []interface{}{}}, 38 "domain": &Entry{"", reflect.String, []interface{}{}}, 39 "protocol": &Entry{"http", reflect.String, []interface{}{"http", "https"}}, 40 "port": &Entry{8080, reflect.Int, []interface{}{}}, 41 "httpsPort": &Entry{8081, reflect.Int, []interface{}{}}, 42 "timeout": &Entry{10, reflect.Int, []interface{}{}}, 43 "maxUploadSize": &Entry{10.0, reflect.Float64, []interface{}{}}, 44 "maintenance": &Entry{false, reflect.Bool, []interface{}{}}, 45 "tls": object{ 46 "cert": &Entry{nil, reflect.String, []interface{}{}}, 47 "key": &Entry{nil, reflect.String, []interface{}{}}, 48 }, 49 }, 50 "database": object{ 51 "connection": &Entry{"none", reflect.String, []interface{}{"none", "mysql", "postgres", "sqlite3", "mssql"}}, // TODO add a dialect ? 52 "host": &Entry{"127.0.0.1", reflect.String, []interface{}{}}, 53 "port": &Entry{3306, reflect.Int, []interface{}{}}, 54 "name": &Entry{"goyave", reflect.String, []interface{}{}}, 55 "username": &Entry{"root", reflect.String, []interface{}{}}, 56 "password": &Entry{"root", reflect.String, []interface{}{}}, 57 "options": &Entry{"charset=utf8&parseTime=true&loc=Local", reflect.String, []interface{}{}}, 58 "maxOpenConnections": &Entry{20, reflect.Int, []interface{}{}}, 59 "maxIdleConnections": &Entry{20, reflect.Int, []interface{}{}}, 60 "maxLifetime": &Entry{300, reflect.Int, []interface{}{}}, 61 "autoMigrate": &Entry{false, reflect.Bool, []interface{}{}}, 62 }, 63 } 64 65 var mutex = &sync.RWMutex{} 66 67 // Register a new config entry and its validation. 68 // 69 // Each module should register its config entries in an "init()" 70 // function, even if they don't have a default value, in order to 71 // ensure they will be validated. 72 // Each module should use its own category and use a name both expressive 73 // and unique to avoid collisions. 74 // For example, the "auth" package registers, among others, "auth.basic.username" 75 // and "auth.jwt.expiry", thus creating a category for its package, and two subcategories 76 // for its features. 77 // 78 // To register an entry without a default value (only specify how it 79 // will be validated), set "Entry.Value" to "nil". 80 // 81 // Panics if an entry already exists for this key and is not identical to the 82 // one passed as parameter of this function. On the other hand, if the entries 83 // are identical, no conflict is expected so the configuration is left in its 84 // current state. 85 func Register(key string, entry Entry) { 86 mutex.Lock() 87 defer mutex.Unlock() 88 category, entryKey, exists := walk(configDefaults, key) 89 if exists { 90 if !reflect.DeepEqual(&entry, category[entryKey].(*Entry)) { 91 panic(fmt.Sprintf("Attempted to override registered config entry %q", key)) 92 } 93 } else { 94 category[entryKey] = &entry 95 } 96 } 97 98 // Load loads the config.json file in the current working directory. 99 // If the "GOYAVE_ENV" env variable is set, the config file will be picked like so: 100 // - "production": "config.production.json" 101 // - "test": "config.test.json" 102 // - By default: "config.json" 103 func Load() error { 104 return LoadFrom(getConfigFilePath()) 105 } 106 107 // LoadFrom loads a config file from the given path. 108 func LoadFrom(path string) error { 109 mutex.Lock() 110 defer mutex.Unlock() 111 config = make(object, len(configDefaults)) 112 loadDefaults(configDefaults, config) 113 114 conf, err := readConfigFile(path) 115 if err != nil { 116 config = nil 117 return err 118 } 119 120 if err := override(conf, config); err != nil { 121 config = nil 122 return err 123 } 124 125 if err := config.validate(""); err != nil { 126 config = nil 127 return fmt.Errorf("Invalid config:%s", err.Error()) 128 } 129 130 return nil 131 } 132 133 // IsLoaded returns true if the config have been loaded. 134 func IsLoaded() bool { 135 mutex.RLock() 136 defer mutex.RUnlock() 137 return config != nil 138 } 139 140 // Clear unloads the config. 141 // DANGEROUS, should only be used for testing. 142 func Clear() { 143 mutex.Lock() 144 config = nil 145 mutex.Unlock() 146 } 147 148 // Get a config entry. Panics if the entry doesn't exist. 149 func Get(key string) interface{} { 150 if val, ok := get(key); ok { 151 return val 152 } 153 154 panic(fmt.Sprintf("Config entry \"%s\" doesn't exist", key)) 155 } 156 157 func get(key string) (interface{}, bool) { 158 mutex.RLock() 159 defer mutex.RUnlock() 160 if config == nil { 161 panic("Config is not loaded") 162 } 163 currentCategory := config 164 b := 0 165 e := strings.Index(key, ".") 166 if e == -1 { 167 e = len(key) 168 } 169 for path := key[b:e]; ; path = key[b:e] { 170 entry, ok := currentCategory[path] 171 if !ok { 172 break 173 } 174 175 if category, ok := entry.(object); ok { 176 currentCategory = category 177 } else { 178 val := entry.(*Entry).Value 179 return val, val != nil // nil means unset 180 } 181 182 if e+1 <= len(key) { 183 b = e + 1 184 newE := strings.Index(key[b:], ".") 185 if newE == -1 { 186 e = len(key) 187 } else { 188 e = newE + b 189 } 190 } 191 } 192 return nil, false 193 } 194 195 // GetString a config entry as string. 196 // Panics if entry is not a string or if it doesn't exist. 197 func GetString(key string) string { 198 str, ok := Get(key).(string) 199 if !ok { 200 panic(fmt.Sprintf("Config entry \"%s\" is not a string", key)) 201 } 202 return str 203 } 204 205 // GetBool a config entry as bool. 206 // Panics if entry is not a bool or if it doesn't exist. 207 func GetBool(key string) bool { 208 val, ok := Get(key).(bool) 209 if !ok { 210 panic(fmt.Sprintf("Config entry \"%s\" is not a bool", key)) 211 } 212 return val 213 } 214 215 // GetInt a config entry as int. 216 // Panics if entry is not an int or if it doesn't exist. 217 func GetInt(key string) int { 218 val, ok := Get(key).(int) 219 if !ok { 220 panic(fmt.Sprintf("Config entry \"%s\" is not an int", key)) 221 } 222 return val 223 } 224 225 // GetFloat a config entry as float64. 226 // Panics if entry is not a float64 or if it doesn't exist. 227 func GetFloat(key string) float64 { 228 val, ok := Get(key).(float64) 229 if !ok { 230 panic(fmt.Sprintf("Config entry \"%s\" is not a float64", key)) 231 } 232 return val 233 } 234 235 // Has check if a config entry exists. 236 func Has(key string) bool { 237 _, ok := get(key) 238 return ok 239 } 240 241 // Set a config entry. 242 // The change is temporary and will not be saved for next boot. 243 // Use "nil" to unset a value. 244 // 245 // - A category cannot be replaced with an entry. 246 // - An entry cannot be replaced with a category. 247 // - New categories can be created with they don't already exist. 248 // - New entries can be created if they don't already exist. This new entry 249 // will be subsequently validated using the type of its initial value and 250 // have an empty slice as authorized values (meaning it can have any value of its type) 251 // 252 // Panics and revert changes in case of error. 253 func Set(key string, value interface{}) { 254 mutex.Lock() 255 defer mutex.Unlock() 256 if config == nil { 257 panic("Config is not loaded") 258 } 259 category, entryKey, exists := walk(config, key) 260 if exists { 261 entry := category[entryKey].(*Entry) 262 previous := entry.Value 263 entry.Value = value 264 if err := entry.validate(key); err != nil { 265 entry.Value = previous 266 panic(err) 267 } 268 category[entryKey] = entry 269 } else { 270 category[entryKey] = &Entry{value, reflect.TypeOf(value).Kind(), []interface{}{}} 271 } 272 } 273 274 // walk the config using the key. Returns the deepest category, the entry key 275 // with its path stripped ("app.name" -> "name") and true if the entry already 276 // exists, false if it's not registered. 277 func walk(currentCategory object, key string) (object, string, bool) { 278 if key == "" { 279 panic("Empty key is not allowed") 280 } 281 282 if key[len(key)-1:] == "." { 283 panic("Keys ending with a dot are not allowed") 284 } 285 286 b := 0 287 e := strings.Index(key, ".") 288 if e == -1 { 289 e = len(key) 290 } 291 for catKey := key[b:e]; ; catKey = key[b:e] { 292 entry, ok := currentCategory[catKey] 293 if !ok { 294 // If categories are missing, create them 295 currentCategory = createMissingCategories(currentCategory, key[b:]) 296 i := strings.LastIndex(key, ".") 297 if i == -1 { 298 catKey = key 299 } else { 300 catKey = key[i+1:] 301 } 302 303 // Entry doesn't exist and is not registered 304 return currentCategory, catKey, false 305 } 306 307 if category, ok := entry.(object); ok { 308 currentCategory = category 309 } else { 310 if e < len(key) { 311 panic(fmt.Sprintf("Attempted to add an entry to non-category %q", key[:e])) 312 } 313 314 // Entry exists 315 return currentCategory, catKey, true 316 } 317 318 if e+1 <= len(key) { 319 b = e + 1 320 newE := strings.Index(key[b:], ".") 321 if newE == -1 { 322 e = len(key) 323 } else { 324 e = newE + b 325 } 326 } else { 327 break 328 } 329 } 330 331 panic(fmt.Sprintf("Attempted to replace the %q category with an entry", key)) 332 } 333 334 // createMissingCategories based on the key path, starting at the given index. 335 // Doesn't create anything is not needed. 336 // Returns the deepest category created, or the provided object if nothing has 337 // been created. 338 func createMissingCategories(currentCategory object, path string) object { 339 b := 0 340 e := strings.Index(path, ".") 341 if e == -1 { 342 return currentCategory 343 } 344 for catKey := path[b:e]; ; catKey = path[b:e] { 345 newCategory := object{} 346 currentCategory[catKey] = newCategory 347 currentCategory = newCategory 348 349 if e+1 <= len(path) { 350 b = e + 1 351 newE := strings.Index(path[b:], ".") 352 if newE == -1 { 353 return currentCategory 354 } 355 e = newE + b 356 } 357 } 358 } 359 360 func loadDefaults(src object, dst object) { 361 for k, v := range src { 362 if obj, ok := v.(object); ok { 363 sub := make(object, len(obj)) 364 loadDefaults(obj, sub) 365 dst[k] = sub 366 } else { 367 entry := v.(*Entry) 368 dst[k] = &Entry{entry.Value, entry.Type, entry.AuthorizedValues} 369 } 370 } 371 } 372 373 func override(src object, dst object) error { 374 for k, v := range src { 375 if obj, ok := v.(map[string]interface{}); ok { 376 if dstObj, ok := dst[k]; !ok { 377 dst[k] = make(object, len(obj)) 378 } else if _, ok := dstObj.(object); !ok { 379 // Conflict: destination is not a category 380 return fmt.Errorf("Invalid config:\n\t- Cannot override entry %q with a category", k) 381 } 382 if err := override(obj, dst[k].(object)); err != nil { 383 return err 384 } 385 } else if entry, ok := dst[k]; ok { 386 e, ok := entry.(*Entry) 387 if !ok { 388 // Conflict: override category with an entry 389 return fmt.Errorf("Invalid config:\n\t- Cannot override category %q with an entry", k) 390 } 391 e.Value = v 392 } else { 393 // If entry doesn't exist (and is not registered), 394 // register it with the type of the type given here 395 // and "any" authorized values. 396 dst[k] = &Entry{v, reflect.TypeOf(v).Kind(), []interface{}{}} 397 } 398 } 399 return nil 400 } 401 402 func readConfigFile(file string) (object, error) { 403 conf := make(object, len(configDefaults)) 404 configFile, err := os.Open(file) 405 406 if err == nil { 407 defer configFile.Close() 408 jsonParser := json.NewDecoder(configFile) 409 err = jsonParser.Decode(&conf) 410 } 411 return conf, err 412 } 413 414 func getConfigFilePath() string { 415 env := strings.ToLower(os.Getenv("GOYAVE_ENV")) 416 if env == "local" || env == "localhost" || env == "" { 417 return "config.json" 418 } 419 return "config." + env + ".json" 420 } 421 422 func (o object) validate(key string) error { 423 message := "" 424 valid := true 425 for k, entry := range o { 426 var subKey string 427 if key == "" { 428 subKey = k 429 } else { 430 subKey = key + "." + k 431 } 432 if category, ok := entry.(object); ok { 433 if err := category.validate(subKey); err != nil { 434 message += err.Error() 435 valid = false 436 } 437 } else if err := entry.(*Entry).validate(subKey); err != nil { 438 message += "\n\t- " + err.Error() 439 valid = false 440 } 441 } 442 443 if !valid { 444 return fmt.Errorf(message) 445 } 446 return nil 447 } 448 449 func (e *Entry) validate(key string) error { 450 if e.Value == nil { // nil values means unset 451 return nil 452 } 453 454 if err := e.tryEnvVarConversion(key); err != nil { 455 return err 456 } 457 458 kind := reflect.TypeOf(e.Value).Kind() 459 if kind != e.Type { 460 if !e.tryIntConversion(kind) { 461 return fmt.Errorf("%q type must be %s", key, e.Type) 462 } 463 return nil 464 } 465 466 if len(e.AuthorizedValues) > 0 && !helper.Contains(e.AuthorizedValues, e.Value) { 467 return fmt.Errorf("%q must have one of the following values: %v", key, e.AuthorizedValues) 468 } 469 470 return nil 471 } 472 473 func (e *Entry) tryIntConversion(kind reflect.Kind) bool { 474 if kind == reflect.Float64 && e.Type == reflect.Int { 475 intVal := int(e.Value.(float64)) 476 if e.Value == float64(intVal) { 477 e.Value = intVal 478 return true 479 } 480 } 481 482 return false 483 } 484 485 func (e *Entry) tryEnvVarConversion(key string) error { 486 str, ok := e.Value.(string) 487 if ok && strings.HasPrefix(str, "${") && strings.HasSuffix(str, "}") { 488 varName := str[2 : len(str)-1] 489 value, set := os.LookupEnv(varName) 490 if !set { 491 return fmt.Errorf("%q: %q environment variable is not set", key, varName) 492 } 493 494 switch e.Type { 495 case reflect.Int: 496 if i, err := strconv.Atoi(value); err == nil { 497 e.Value = i 498 } else { 499 return fmt.Errorf("%q could not be converted to int from environment variable %q of value %q", key, varName, value) 500 } 501 case reflect.Float64: 502 if f, err := strconv.ParseFloat(value, 64); err == nil { 503 e.Value = f 504 } else { 505 return fmt.Errorf("%q could not be converted to float64 from environment variable %q of value %q", key, varName, value) 506 } 507 case reflect.Bool: 508 if f, err := strconv.ParseBool(value); err == nil { 509 e.Value = f 510 } else { 511 return fmt.Errorf("%q could not be converted to bool from environment variable %q of value %q", key, varName, value) 512 } 513 default: 514 // Keep value as string if type is not supported and let validation do its job 515 e.Value = value 516 } 517 } 518 519 return nil 520 }