v0.2.0 toml-only, harden concurrency and env export, dep updates
Test / test (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-09-28 16:55:58 -04:00
1 parent 3c35601179
commit e08467bf38
42 files changed
+2533 -4274

No files matched your search

+18 -170
View File
@@ -1,175 +1,23 @@
# Validation
The config package provides flexible validation through function injection and bundled validators for common scenarios.
Registration defaults define conversion types. Set, SetSource and complete source
loads reject incompatible values before publication. Overflow, fractional integer
conversion, non-finite values and invalid textual duration/time/network values error.
Untyped nil defaults leave the value type open within the supported value graph.
## Validation Stages
`Validate(paths...)` requires a registered path to have a supplied value, even if it
equals the default. `RegisterRequired` and `required:"true"` fields are collected by
`Validate()` with no arguments and by Build. Defaults alone do not satisfy required
inputs. Registration of a struct is staged, so a duplicate/invalid field or invalid
explicit environment value cannot leave a partially registered schema.
The package supports two validation stages when using the Builder pattern:
Build supports `WithValidator(func(*Config) error)` and a matching
`WithTypedValidator(func(*T) error)`. They run at initialization. They are not rerun
on runtime changes: apply application-level constraints before adopting a new
snapshot when those constraints must hold after reconfiguration.
### Pre-Population Validation (`WithValidator`)
Runs on raw configuration values before struct population:
```go
cfg, _ := config.NewBuilder().
WithDefaults(defaults).
WithValidator(func(c *config.Config) error {
// Check required paths exist
return c.Validate("api.key", "database.url")
}).
Build()
```
### Post-Population Validation (`WithTypedValidator`)
Type-safe validation after struct is populated:
```go
type AppConfig struct {
Server struct {
Port int64 `toml:"port"`
} `toml:"server"`
}
cfg, _ := config.NewBuilder().
WithTarget(&AppConfig{}).
WithTypedValidator(func(cfg *AppConfig) error {
// No type assertion needed - cfg.Server.Port is int64
if cfg.Server.Port < 1024 || cfg.Server.Port > 65535 {
return fmt.Errorf("port %d outside valid range", cfg.Server.Port)
}
return nil
}).
Build()
```
## Bundled Validators
The package includes common validation functions in the `config` package:
### Numeric Validators
```go
// Port validates TCP/UDP port range (1-65535)
config.Port(8080) // returns nil
// Positive validates positive numbers
config.Positive(42) // returns nil
config.Positive(-1) // returns error
// NonNegative validates non-negative numbers
config.NonNegative(0) // returns nil
config.NonNegative(-5) // returns error
// Range creates min/max validators
portValidator := config.Range(1024, 65535)
err := portValidator(8080) // returns nil
```
### String Validators
```go
// NonEmpty validates non-empty strings
config.NonEmpty("hello") // returns nil
config.NonEmpty("") // returns error
// Pattern creates regex validators
emailPattern := config.Pattern(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)
err := emailPattern("user@example.com") // returns nil
// URLPath validates URL path format
config.URLPath("/api/v1") // returns nil
config.URLPath("api/v1") // returns error (must start with /)
```
### Network Validators
```go
// IPAddress validates any IP address
config.IPAddress("192.168.1.1") // returns nil
config.IPAddress("::1") // returns nil
// IPv4Address validates IPv4 only
config.IPv4Address("192.168.1.1") // returns nil
config.IPv4Address("::1") // returns error
// IPv6Address validates IPv6 only
config.IPv6Address("2001:db8::1") // returns nil
config.IPv6Address("192.168.1.1") // returns error
```
### Enum Validators
```go
// OneOf creates allowed values validator
logLevel := config.OneOf("debug", "info", "warn", "error")
err := logLevel("info") // returns nil
err = logLevel("trace") // returns error
```
## Integration Example
```go
type ServerConfig struct {
Host string `toml:"host"`
Port int64 `toml:"port"`
Mode string `toml:"mode"`
}
cfg, err := config.NewBuilder().
WithTarget(&ServerConfig{}).
WithFile("config.toml").
WithTypedValidator(func(c *ServerConfig) error {
// Use bundled validators
if err := config.IPAddress(c.Host); err != nil {
return fmt.Errorf("invalid host: %w", err)
}
if err := config.Port(c.Port); err != nil {
return fmt.Errorf("invalid port: %w", err)
}
modeValidator := config.OneOf("development", "production")
if err := modeValidator(c.Mode); err != nil {
return fmt.Errorf("invalid mode: %w", err)
}
return nil
}).
Build()
```
## Integration with go-playground/validator
The package works seamlessly with `go-playground/validator`:
```go
import "github.com/go-playground/validator/v10"
type ServerConfig struct {
Host string `toml:"host" validate:"required,hostname"`
Port int `toml:"port" validate:"required,min=1024,max=65535"`
TLS struct {
Enabled bool `toml:"enabled"`
Cert string `toml:"cert" validate:"required_if=Enabled true"`
Key string `toml:"key" validate:"required_if=Enabled true"`
} `toml:"tls"`
}
// Load configuration
cfg, _ := config.NewBuilder().
WithDefaults(&ServerConfig{}).
Build()
// Get populated struct
serverCfg, _ := cfg.AsStruct()
// Validate with go-playground/validator
validate := validator.New()
if err := validate.Struct(serverCfg); err != nil {
return err // Validation errors
}
```
Tags don't conflict since each package uses different tag names (`toml`/`json`/`yaml` for config, `validate` for validator).
Helpers include Port, Positive, NonNegative, IPAddress, IPv4Address, IPv6Address,
URLPath, OneOf, Range, Pattern and NonEmpty. Pattern compiles its expression when the
validator is created and panics on an invalid expression. Network validators allow
the documented empty/unspecified defaults. Use errors.Is with ErrValidation,
ErrTypeMismatch and ErrDecode to handle failures.