// Package conf defines utils to simplify configuration management. // // It provides functions to load and save config files. // // Several formats are supported. The encoders/decoders are selected according // to the extension of the paths passed to functions: // // - JSON: ".json" // - TOML: ".toml" package conf import ( "errors" "fmt" "os" "path/filepath" ) // ErrUnsupportedFileType is returned when the type of the config file is not // supported. var ErrUnsupportedFileType = errors.New("unsupported config type") // LoadFile reads the file at path, parses its json content and fills the struct // with the content of the file. func LoadFile(path string, data any) error { return read(path, data) } // LoadFiles tries to load all the given paths in the given order. // // If a path does not exist, it is ignored. // // It returns an error only if the content of a file is invalid, i.e. it // cannot be unmarshaled to the struct. func LoadFiles(data any, paths ...string) error { for _, p := range paths { err := read(p, data) if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("cannot load %q: %w", p, err) } } return nil } // SaveFile writes the given data serialized in JSON in the given path. func SaveFile(path string, data any) error { return write(path, data) } // LoadAndUpdateFile reads the config fileat path and // updates it, meaning that it adds new options, removes // old ones, and update it by calling the Update method of // data if it implements the interface [Updater]. // // If no file is found at path, it is created and // initialized with the default values. // // An error is returned only if the config file cannot be // written. func LoadAndUpdateFile(path string, data any) error { if _, err := os.Stat(path); !os.IsNotExist(err) { err2 := read(path, data) if err2 != nil { return err2 } if data, ok := data.(Updater); ok { data.Update() } } return write(path, data) } // Updater is the interface that can be implemented by // config structs. If it is implemented, Update() is // called by [LoadAndUpdateFile]. It allows one to modify // the data and persist those changes, for example to // change default values. type Updater interface { // Update is called by LoadAndUpdateFile Update() } func read(path string, data any) error { content, err := os.ReadFile(filepath.Clean(path)) if err != nil { return fmt.Errorf("cannot read config file: %w", err) } return unmarshal(getType(path), content, data) } func write(path string, data any) error { content, err := marshal(getType(path), data) if err != nil { return err } err = os.WriteFile(path, content, 0o600) if err != nil { return fmt.Errorf("cannot write config file '%s': %w", path, err) } return nil }