e3.1.0 Validator added.
This commit is contained in:
72
builder.go
72
builder.go
@ -7,23 +7,29 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ValidatorFunc defines the signature for a function that can validate a Config instance.
|
||||||
|
// It receives the fully loaded *Config object and should return an error if validation fails.
|
||||||
|
type ValidatorFunc func(c *Config) error
|
||||||
|
|
||||||
// Builder provides a fluent interface for building configurations
|
// Builder provides a fluent interface for building configurations
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
cfg *Config
|
cfg *Config
|
||||||
opts LoadOptions
|
opts LoadOptions
|
||||||
defaults any
|
defaults any
|
||||||
prefix string
|
prefix string
|
||||||
file string
|
file string
|
||||||
args []string
|
args []string
|
||||||
err error
|
err error
|
||||||
|
validators []ValidatorFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBuilder creates a new configuration builder
|
// NewBuilder creates a new configuration builder
|
||||||
func NewBuilder() *Builder {
|
func NewBuilder() *Builder {
|
||||||
return &Builder{
|
return &Builder{
|
||||||
cfg: New(),
|
cfg: New(),
|
||||||
opts: DefaultLoadOptions(),
|
opts: DefaultLoadOptions(),
|
||||||
args: os.Args[1:],
|
args: os.Args[1:],
|
||||||
|
validators: make([]ValidatorFunc, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +63,7 @@ func (b *Builder) WithArgs(args []string) *Builder {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithSources sets the precedence order for configuration sources
|
// WithSources sets the precedent order for configuration sources
|
||||||
func (b *Builder) WithSources(sources ...Source) *Builder {
|
func (b *Builder) WithSources(sources ...Source) *Builder {
|
||||||
b.opts.Sources = sources
|
b.opts.Sources = sources
|
||||||
return b
|
return b
|
||||||
@ -80,6 +86,15 @@ func (b *Builder) WithEnvWhitelist(paths ...string) *Builder {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithValidator adds a validation function that runs at the end of the build process
|
||||||
|
// Multiple validators can be added and are executed in the order they are added
|
||||||
|
func (b *Builder) WithValidator(fn ValidatorFunc) *Builder {
|
||||||
|
if fn != nil {
|
||||||
|
b.validators = append(b.validators, fn)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// Build creates the Config instance with all specified options
|
// Build creates the Config instance with all specified options
|
||||||
func (b *Builder) Build() (*Config, error) {
|
func (b *Builder) Build() (*Config, error) {
|
||||||
if b.err != nil {
|
if b.err != nil {
|
||||||
@ -94,13 +109,21 @@ func (b *Builder) Build() (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
if err := b.cfg.LoadWithOptions(b.file, b.args, b.opts); err != nil {
|
loadErr := b.cfg.LoadWithOptions(b.file, b.args, b.opts)
|
||||||
// The error might be non-fatal (e.g., file not found).
|
if loadErr != nil && !errors.Is(loadErr, ErrConfigNotFound) {
|
||||||
// Return the config object so it can be used with other sources.
|
// Return on fatal load errors. ErrConfigNotFound is not fatal.
|
||||||
return b.cfg, err
|
return nil, loadErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.cfg, nil
|
// Run validators
|
||||||
|
for _, validator := range b.validators {
|
||||||
|
if err := validator(b.cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("configuration validation failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrConfigNotFound or nil
|
||||||
|
return b.cfg, loadErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustBuild is like Build but panics on error
|
// MustBuild is like Build but panics on error
|
||||||
@ -114,4 +137,21 @@ func (b *Builder) MustBuild() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAndScan builds and unmarshals the final configuration into the provided target struct pointer
|
||||||
|
func (b *Builder) BuildAndScan(target any) error {
|
||||||
|
cfg, err := b.Build()
|
||||||
|
if err != nil && !errors.Is(err, ErrConfigNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Scan to populate the target struct.
|
||||||
|
// The prefix used during registration is the base path for scanning.
|
||||||
|
if err := cfg.Scan(b.prefix, target); err != nil {
|
||||||
|
return fmt.Errorf("failed to scan final config into target: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrConfigNotFound or nil
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
package config_test
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -150,6 +151,73 @@ func TestBuilder(t *testing.T) {
|
|||||||
MustBuild()
|
MustBuild()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Builder with Validator", func(t *testing.T) {
|
||||||
|
type Config struct {
|
||||||
|
Server struct {
|
||||||
|
Host string `toml:"host"`
|
||||||
|
Port int `toml:"port"`
|
||||||
|
} `toml:"server"`
|
||||||
|
MaxConns int `toml:"max_conns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults := Config{}
|
||||||
|
defaults.Server.Host = "localhost"
|
||||||
|
defaults.Server.Port = 8080
|
||||||
|
defaults.MaxConns = 100
|
||||||
|
|
||||||
|
// Validator that fails
|
||||||
|
failingValidator := func(c *config.Config) error {
|
||||||
|
port, err := c.Int64("server.port")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if port == 8080 {
|
||||||
|
return errors.New("port 8080 is not allowed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator that succeeds
|
||||||
|
passingValidator := func(c *config.Config) error {
|
||||||
|
host, err := c.String("server.host")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
return errors.New("host cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case 1: Validator fails
|
||||||
|
_, err := config.NewBuilder().
|
||||||
|
WithDefaults(defaults).
|
||||||
|
WithValidator(failingValidator).
|
||||||
|
Build()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "port 8080 is not allowed")
|
||||||
|
|
||||||
|
// Test case 2: Validator passes
|
||||||
|
cfg, err := config.NewBuilder().
|
||||||
|
WithDefaults(defaults).
|
||||||
|
WithArgs([]string{"--server.port=9000"}). // Change the port so it passes
|
||||||
|
WithValidator(failingValidator).
|
||||||
|
WithValidator(passingValidator).
|
||||||
|
Build()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, cfg)
|
||||||
|
port, _ := cfg.Int64("server.port")
|
||||||
|
assert.Equal(t, int64(9000), port)
|
||||||
|
|
||||||
|
// Test case 3: MustBuild panics on validation failure
|
||||||
|
assert.PanicsWithError(t, "configuration validation failed: port 8080 is not allowed", func() {
|
||||||
|
config.NewBuilder().
|
||||||
|
WithDefaults(defaults).
|
||||||
|
WithValidator(failingValidator).
|
||||||
|
MustBuild()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQuickFunctions(t *testing.T) {
|
func TestQuickFunctions(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user