initial commit
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user