feat: add strict TOML configuration validation
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// ClientOptions controls platform-specific static validation. Production uses
|
||||
// CheckFilesystem; documentation tests may perform syntax-only validation for
|
||||
// a non-native target.
|
||||
type ClientOptions struct {
|
||||
Platform Platform
|
||||
CheckFilesystem bool
|
||||
}
|
||||
|
||||
func LoadServer(path string) (*Server, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read server config: %w", err)
|
||||
}
|
||||
return DecodeServer(data)
|
||||
}
|
||||
|
||||
func DecodeServer(data []byte) (*Server, error) {
|
||||
return DecodeServerWithOverrides(data, nil)
|
||||
}
|
||||
|
||||
// DecodeServerWithOverrides applies only explicitly present flag values after
|
||||
// TOML decoding. Keys come from the single registry in overrides.go.
|
||||
func DecodeServerWithOverrides(data []byte, overrides map[string]string) (*Server, error) {
|
||||
var raw = defaultServerFile()
|
||||
if err := strictDecode(data, &raw); err != nil {
|
||||
return nil, fmt.Errorf("decode server config: %w", err)
|
||||
}
|
||||
if err := applyServerOverrides(&raw, overrides); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return validateServer(raw)
|
||||
}
|
||||
|
||||
func LoadClient(path string, platform Platform) (*Client, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read client config: %w", err)
|
||||
}
|
||||
return DecodeClient(data, ClientOptions{Platform: platform, CheckFilesystem: true})
|
||||
}
|
||||
|
||||
func DecodeClient(data []byte, options ClientOptions) (*Client, error) {
|
||||
return DecodeClientWithOverrides(data, options, nil)
|
||||
}
|
||||
|
||||
// DecodeClientWithOverrides applies explicitly present flags after the file.
|
||||
func DecodeClientWithOverrides(data []byte, options ClientOptions, overrides map[string]string) (*Client, error) {
|
||||
var raw = defaultClientFile()
|
||||
if err := strictDecode(data, &raw); err != nil {
|
||||
return nil, fmt.Errorf("decode client config: %w", err)
|
||||
}
|
||||
if err := applyClientOverrides(&raw, overrides); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return validateClient(raw, options)
|
||||
}
|
||||
|
||||
func strictDecode(data []byte, target any) error {
|
||||
if !utf8.Valid(data) {
|
||||
return fmt.Errorf("configuration is not valid UTF-8")
|
||||
}
|
||||
decoder := toml.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(target); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDuration(name, value string, allowZero bool) (time.Duration, error) {
|
||||
duration, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
if duration < 0 || (!allowZero && duration == 0) {
|
||||
return 0, fmt.Errorf("%s must be %s", name, map[bool]string{true: "non-negative", false: "positive"}[allowZero])
|
||||
}
|
||||
return duration, nil
|
||||
}
|
||||
Reference in New Issue
Block a user