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  }