Package xconf provides a configuration registry for an application.
Configurations can be extracted from a file / env / flag set / remote system.
Supported formats are json, yaml, ini, (java) properties, toml, plain.
$ go get github.com/actforgood/xconfYou can create your own configuration retriever implementing Loader interface.
Package provides these Loaders for you:
EnvLoader- loads environment variables.DotEnvFileLoader,DotEnvReaderLoader- loads configuration from a .env file /io.Reader.JSONFileLoader,JSONReaderLoader- loads json configuration from a file /io.Reader.YAMLFileLoader,YAMLReaderLoader- loads yaml configuration from a file /io.Reader.IniFileLoader- loads ini configuration from a file.PropertiesFileLoader,PropertiesBytesLoader- loads java style properties configuration from a file / bytes slice.TOMLFileLoader,TOMLReaderLoader- loads toml configuration from a file /io.Reader.ConsulLoader- loads json/yaml/plain configuration from a remote Consul KV Store.EtcdLoader- loads json/yaml/plain configuration from a remote Etcd KV Store.PlainLoader- explicit configuration provider.FileLoader- factory for<JSON|YAML|Ini|DotEnv|Properties|TOML>FileLoaders based on file extension.FlagSetLoader- extracts configuration from aflag.FlagSet.MultiLoader- loads (and merges, if configured) configuration from multiple loaders.
Upon above loaders there are available decorators which can help you achieve more sophisticated outcome:
FilterKVLoader- filters other loader's configurations (based on keys and or their values).
Example of applicability: I load configurations from environment, but I only want the ones prefixed with "MY_APP_" - I can apply this loader withFilterKVWhitelistFunc(FilterKeyWithPrefix("MY_APP_")filter function.AlterValueLoader- changes the value for a configuration key.
Example of applicability: I load configurations from environment and for a given key I want its value to be a slice (not a string as envs are read/stored by default) - I can apply this loader withToStringListaltering function.IgnoreErrorLoader- ignores the error returned by another loader.
Example of applicability: I load configuration from environment and from file (using aMultiLoader), but it's not mandatory for that file to exist (file it's just an auxiliary source for my configurations, that may exist) - I can use this loader to ignore "file does not exist" error.FileCacheLoader- caches configuration from a[X]FileLoaderuntil file gets modified (to be used if loader is called multiple times).FlattenLoader- creates easy to access nested configuration leaf keys symlinks.AliasLoader- creates aliases for other keys.
The main configuration contract this package provides looks like:
type Config interface {
Get(key string, def ...any) any
}with a default implementation obtained with:
// NewDefaultConfig instantiates a new default config object.
// The first parameter is the loader used as a source of getting the key-value configuration map.
// The second parameter represents a list of optional functions to configure the object.
func NewDefaultConfig(loader Loader, opts ...DefaultConfigOption) (*DefaultConfig, error)The DefaultConfig has an option of reloading configurations (interval based), if you want to retrieve updated configuration
at runtime.
There are 2 (proposed) ways of working with it:
- injecting a
Configreference and callingGet(key)every time you need a configuration. - registering your class as an observer to get notified about config changes.
Example of usage (first case) (note: code does not compile):
// cart_service.go
const (
defaultMaxQtyCfgVal uint = 100
maxQtyCfgKey = "MAX_ALLOWED_QTY_TO_ORDER"
)
type CartService struct {
config xconf.Config
}
func NewCartService(config xconf.Config) *CartService {
return &CartService{
config: config,
}
}
func (cartSvc *CartService) AddProduct(sku string, qty uint) error {
// ...
if customerType != B2B {
totalQty := currentQty + qty
maxQty := cartSvc.config.Get(maxQtyCfgKey, defaultMaxQtyCfgVal).(uint)
if totalQty > maxQty {
return ErrMaxQtyExceeded
}
}
// ...
return nil
}
func main() {
// somewhere in the bootstrap of your application ...
var (
loader xconf.Loader // = ... your desired source(s)
config xconf.Config
cartSvc *CartService
)
config, err := xconf.NewDefaultConfig(
loader,
xconf.DefaultConfigWithReloadInterval(time.Minute), // reload every minute
)
if err != nil {
panic(err)
}
cartSvc = NewCartService(config)
// somewhere in the application business flow ...
_ = cartSvc.AddProduct("IPHONE", 1)
// somewhere in the shutdown of your application ...
if closableConfig, ok := config.(io.Closer); ok {
_ = closableConfig.Close()
}
}Example of usage (second case) (note: code does not compile):
// redis_wrapper.go
const (
RedisHostCfgKey = "REDIS_HOST"
DefaultRedisHostCfgVal = "127.0.0.1:6379"
)
type RedisClient interface {
Ping() error
Get(key string) (string, error)
Set(key string, value any, expiration time.Duration) (string, error)
Close() error
}
type RedisClientWrapper struct {
client *redis.Client // official client
mu sync.RWMutex
}
func NewRedisClientWrapper(host string) *RedisClientWrapper {
officialClient = ...
return &RedisClientWrapper {
client: officialClient,
}
}
func (wrapper *RedisClientWrapper) Get(key string) (string, error) {
wrapper.mu.RLock()
defer wrapper.mu.RUnlock()
return wrapper.client.Get(key).Result()
}
func (wrapper *RedisClientWrapper) OnConfigChange(config xconf.Config, changedKeys ...string) {
for _, changedKey := range changedKeys {
if changedKey == RedisHostCfgKey { // or use strings.EqualFold() if you enabled DefaultConfigWithIgnoreCaseSensitivity.
wrapper.mu.Lock()
_ = wrapper.client.Close() // close previous client
newClient := ... // reinitialize client based on config.Get(RedisHostCfgKey).(string)
wrapper.client = newClient
wrapper.mu.Unlock()
}
}
}
func main() {
// somewhere in the bootstrap of your application ...
var (
loader xconf.Loader // = ... your desired source(s)
config xconf.Config
redisClient RedisClient
)
config, err := xconf.NewDefaultConfig(
loader,
xconf.DefaultConfigWithReloadInterval(30 * time.Second), // reload every 30 seconds
)
if err != nil {
panic(err)
}
redisHost := config.Get(RedisHostCfgKey, DefaultRedisHostCfgVal).(string)
redisClient = NewRedisClient(redisHost)
config.RegisterObserver(redisClient.OnConfigChange) // register redis wrapper as an observer
// somewhere in the application business flow ...
_, _ = redisClient.Get("something")
// somewhere in the shutdown of your application ...
if closableConfig, ok := config.(io.Closer); ok {
_ = closableConfig.Close()
}
_ = redisClient.Close()
}This is not the subject of this package, but as a mention, you can achieve that if needed, with a package like github.com/mitchellh/mapstructure.
Example:
package main
import (
"bytes"
"fmt"
"github.com/actforgood/xconf"
"github.com/mitchellh/mapstructure"
)
type DBConfig struct {
Host string
Port int
Auth Auth
}
type Auth struct {
Username string
Password string
}
func main() {
var (
jsonConfig = `{
"db": {
"host": "127.0.0.1",
"port": 3306,
"auth": {
"username": "JohnDoe",
"password": "verySecretPwd"
}
}
}`
dbConfig DBConfig // the struct to populate with configuration
dbConfigMap map[string]any // the configuration map for "db" key
loader = xconf.JSONReaderLoader(bytes.NewReader([]byte(jsonConfig)))
)
// example using directly a Loader:
configMap, err := loader.Load()
if err != nil {
panic(err)
}
dbConfigMap = configMap["db"].(map[string]any)
if err := mapstructure.Decode(dbConfigMap, &dbConfig); err != nil {
panic(err)
}
fmt.Printf("%+v", dbConfig)
// example using the Config contract:
config, err := xconf.NewDefaultConfig(loader)
if err != nil {
panic(err)
}
dbConfigMap = config.Get("db").(map[string]any)
if err := mapstructure.Decode(dbConfigMap, &dbConfig); err != nil {
panic(err)
}
fmt.Printf("%+v", dbConfig)
// both Printf will produce: {Host:127.0.0.1 Port:3306 Auth:{Username:JohnDoe Password:verySecretPwd}}
}Things that can be added to package, extended:
- Support more formats (like HCL)
- Add also a writer/persister functionality (currently you can only read configurations) to different sources and formats (JSONFileWriter/YAMLFileWriter/EtcdWriter/ConsulWriter/...) implementing a common contract like:
type ConfigWriter interface {
Write(configMap map[string]any) error
}- Add a typed struct with methods like
GetString,GetInt...
- Feel free to use this pkg if you like it and fits your needs. Check also other packages like spf13/viper ...
- To run unit tests:
make test/make cover. - To run integration tests:
make test-integration/make cover-integration: will setup Consul and Etcd docker containers with some keys in them, run./scripts/teardown_dockers.shat the end to stop and remove containers). - To run benchmarks:
make bench. - Project's class diagram can be found here.
This package is released under a MIT license. See LICENSE.
Other 3rd party packages directly used by this package are released under their own licenses.
- github.com/joho/godotenv - MIT License
- github.com/magiconair/properties - BSD (2 Clause) License
- gopkg.in/ini.v1 - Apache 2.0 License
- gopkg.in/yaml.v3 - MIT And Apache License
- github.com/pelletier/go-toml/v2 - MIT License
- go.etcd.io/etcd/client/v3 - Apache 2.0 License
- github.com/spf13/cast - MIT License
- github.com/actforgood/xerr - MIT License
- github.com/actforgood/xlog - MIT License