initial commit
This commit is contained in:
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# config
|
||||
|
||||
config is a tiny library to streamline configuration loading and validation using [viper](https://github.com/spf13/viper) and [validator](https://github.com/go-playground/validator).
|
||||
159
config.go
Normal file
159
config.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Manager is the top-level configuration schema and viper instance.
|
||||
type Manager[T struct{}] struct {
|
||||
// PreLoad is an optional function that is called before loading the
|
||||
// configuration file.
|
||||
PreLoad func(*Manager[T]) error
|
||||
|
||||
// PostLoad is an optional function that is called after loading the
|
||||
// configuration file.
|
||||
PostLoad func(*Manager[T]) error
|
||||
|
||||
// R is a raw access to the configuration schema. WARNING: This does not
|
||||
// perform ANY validation or unmarshalling, so it should only be used if you
|
||||
// have already manually called the Load method and are sure that no errors
|
||||
// occured.
|
||||
R *T
|
||||
|
||||
// loaded indicates whether the configuration has been loaded.
|
||||
loaded bool
|
||||
// mu is a mutex to ensure thread-safe access to the configuration.
|
||||
mu sync.RWMutex
|
||||
|
||||
viper *viper.Viper
|
||||
}
|
||||
|
||||
// NewManager creates a new configuration manager for schema T. Fields in T are
|
||||
// responsible for including any necessary `mapstructure` and `validate` tags
|
||||
// to ensure proper unmarshalling and validation of the configuration schema.
|
||||
func NewManager[T struct{}](configName, configType, configPath string) *Manager[T] {
|
||||
m := &Manager[T]{
|
||||
viper: viper.New(),
|
||||
}
|
||||
|
||||
m.viper.SetConfigName(configName)
|
||||
m.viper.SetConfigType(configType)
|
||||
m.viper.AddConfigPath(configPath)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// WithPrefix sets the environment variable prefix for the viper instance. Must
|
||||
// be set in order to use environment variables for configuration.
|
||||
func (m *Manager[T]) WithPrefix(prefix string) *Manager[T] {
|
||||
m.viper.SetEnvPrefix(prefix)
|
||||
m.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
m.viper.AutomaticEnv()
|
||||
return m
|
||||
}
|
||||
|
||||
// C returns the configuration schema. If the configuration has not been
|
||||
// loaded, C will load the configuration file before returning the schema.
|
||||
// Any errors encountered during loading will result in a panic.
|
||||
func (m *Manager[T]) C() *T {
|
||||
// First, try to acquire a read lock
|
||||
m.mu.RLock()
|
||||
if m.loaded {
|
||||
m.mu.RUnlock()
|
||||
return m.R // Already loaded, return the cached instance
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Need to load the configuration, so acquire a write lock
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Double-check if loaded after acquiring the write lock
|
||||
if m.loaded {
|
||||
return m.R // Already loaded, return the cached instance
|
||||
}
|
||||
|
||||
// Proceed to load the configuration
|
||||
if err := m.loadUnsafe(); err != nil {
|
||||
panic(fmt.Errorf("failed to load configuration: %w", err))
|
||||
}
|
||||
|
||||
return m.R // Return the loaded configuration
|
||||
}
|
||||
|
||||
// ConfigPath returns the absolute path to the configuration file that was used.
|
||||
// If absolute path cannot be determined, the relative path is returned.
|
||||
func (m *Manager[T]) ConfigPath() string {
|
||||
if m.viper.ConfigFileUsed() == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(m.viper.ConfigFileUsed())
|
||||
if err != nil {
|
||||
return m.viper.ConfigFileUsed()
|
||||
}
|
||||
|
||||
return absPath
|
||||
}
|
||||
|
||||
// Viper returns the viper instance used by the configuration manager.
|
||||
func (m *Manager[T]) Viper() *viper.Viper {
|
||||
return m.viper
|
||||
}
|
||||
|
||||
// Load reads and validates the configuration file, returning an error if any.
|
||||
func (m *Manager[T]) Load() error {
|
||||
// Make sure we have lock before checking if loaded
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.loaded {
|
||||
return nil // Already loaded, no need to load again
|
||||
}
|
||||
|
||||
return m.loadUnsafe()
|
||||
}
|
||||
|
||||
// loadUnsafe performs the actual loading without acquiring locks. Caller MUST
|
||||
// hold the write lock.
|
||||
func (m *Manager[T]) loadUnsafe() error {
|
||||
// Initialize R to a new instance of T
|
||||
m.R = new(T)
|
||||
|
||||
// Run the pre-load function if it is set
|
||||
if m.PreLoad != nil {
|
||||
if err := m.PreLoad(m); err != nil {
|
||||
return fmt.Errorf("pre-load function failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load the configuration file using viper
|
||||
if err := m.viper.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("viper failed to read config file: %w", err)
|
||||
}
|
||||
if err := m.viper.Unmarshal(m.R); err != nil {
|
||||
return fmt.Errorf("viper failed to unmarshal config file: %w", err)
|
||||
}
|
||||
|
||||
// Validate the configuration schema using the validator package
|
||||
validate := validator.New(validator.WithRequiredStructEnabled())
|
||||
if err := validate.Struct(m.R); err != nil {
|
||||
return fmt.Errorf("validator failed to validate config file: %w", err)
|
||||
}
|
||||
|
||||
// Run the post-load function if it is set
|
||||
if m.PostLoad != nil {
|
||||
if err := m.PostLoad(m); err != nil {
|
||||
return fmt.Errorf("post-load function failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.loaded = true
|
||||
return nil
|
||||
}
|
||||
52
directory.go
Normal file
52
directory.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// RootDir checks the current working directory and its parent directories for
|
||||
// a given filename and returns the absolute path to the directory. If the file
|
||||
// is not found within the specified depth or any other error occurs, it will
|
||||
// panic. If depth is zero or negative, RootDir checks the current directory only.
|
||||
func RootDir(searchName string, depth ...int) string {
|
||||
// Get the current working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Apply default depth if not provided
|
||||
depthVal := 0
|
||||
if len(depth) > 0 {
|
||||
depthVal = depth[0]
|
||||
}
|
||||
|
||||
// Walk directories up to the specified depth to find the file
|
||||
path := walkRootDir(searchName, cwd, depthVal)
|
||||
if path == "" {
|
||||
panic(fmt.Errorf("RootDir checked %d directories, no '%s' file found", depthVal+1, searchName))
|
||||
}
|
||||
|
||||
// Try to get the absolute path of the found directory
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil || abs == "" {
|
||||
panic(fmt.Errorf("RootDir failed to get absolute path: %v", err))
|
||||
}
|
||||
|
||||
return abs
|
||||
}
|
||||
|
||||
// walkRootDir recursively checks directories up to the specified reverseDepth.
|
||||
func walkRootDir(searchName, path string, reverseDepth int) string {
|
||||
if _, err := os.Stat(filepath.Join(path, searchName)); err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
if reverseDepth > 0 {
|
||||
return walkRootDir(searchName, path+"/..", reverseDepth-1)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
34
go.mod
Normal file
34
go.mod
Normal file
@@ -0,0 +1,34 @@
|
||||
module gitea.auvem.com/go-toolkit/config
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/spf13/viper v1.20.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
56
go.sum
Normal file
56
go.sum
Normal file
@@ -0,0 +1,56 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
Reference in New Issue
Block a user