145 lines
3.9 KiB
Go
145 lines
3.9 KiB
Go
// 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"
|
|
// - HCL: ".hcl"
|
|
package conf
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
)
|
|
|
|
var (
|
|
// ErrUnsupportedFileType is returned when the type of the config file is
|
|
// not supported.
|
|
ErrUnsupportedFileType = errors.New("unsupported config type")
|
|
|
|
// ErrInvalidMarshalData is returned when the marshaled value is not a
|
|
// struct.
|
|
ErrInvalidMarshalData = errors.New("the marshaled value must be a struct")
|
|
|
|
// ErrInvalidUnmarshalData is returned when the marshaled value is not a
|
|
// struct.
|
|
ErrInvalidUnmarshalData = errors.New(
|
|
"the unmarshaled value must be a pointer to a struct",
|
|
)
|
|
)
|
|
|
|
// LoadFile reads the file at path, parses its json content and fills the struct
|
|
// with the content of the file.
|
|
//
|
|
// LoadFile panics with [ErrInvalidUnmarshalData] if data is not a pointer to a
|
|
// struct, as this error should be caught during dev.
|
|
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.
|
|
//
|
|
// LoadFiles panics with [ErrInvalidUnmarshalData] if data is not a pointer to a
|
|
// struct, as this error should be caught during dev.
|
|
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.
|
|
//
|
|
// SaveFile panics with [ErrInvalidMarshalData] if data is not a struct, as
|
|
// this error should be caught during dev.
|
|
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.
|
|
//
|
|
// LoadAndUpdateFile panics with [ErrInvalidUnmarshalData] if data is not a
|
|
// pointer to a struct, as this error should be caught during dev.
|
|
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 {
|
|
val := reflect.ValueOf(data)
|
|
indVal := reflect.Indirect(val)
|
|
|
|
if val.Kind() != reflect.Ptr || indVal.Kind() != reflect.Struct {
|
|
panic(ErrInvalidUnmarshalData)
|
|
}
|
|
|
|
content, err := os.ReadFile(filepath.Clean(path))
|
|
if err != nil {
|
|
return fmt.Errorf("cannot read config file: %w", err)
|
|
}
|
|
|
|
return unmarshal(path, content, data)
|
|
}
|
|
|
|
func write(path string, data any) error {
|
|
v := reflect.Indirect(reflect.ValueOf(data))
|
|
if v.Kind() != reflect.Struct {
|
|
panic(ErrInvalidMarshalData)
|
|
}
|
|
|
|
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
|
|
}
|