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  }