From a66b6843306c9e661fe6870761d3ea9e1664a30d60b3baf764ff412219bb7bb7 Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Fri, 3 Oct 2025 10:01:49 -0400 Subject: [PATCH] e6.2.0 Common validator functions added. --- validator.go | 123 ++++++++++++++++++++++++++++++++++ validator_test.go | 163 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 validator.go create mode 100644 validator_test.go diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..15e3a8a --- /dev/null +++ b/validator.go @@ -0,0 +1,123 @@ +// FILE: lixenwraith/config/validator.go +package config + +import ( + "fmt" + "net" + "regexp" + "strings" +) + +// Common validators for configuration values + +// Port validates TCP/UDP port range +func Port(p int64) error { + if p < 1 || p > 65535 { + return fmt.Errorf("must be 1-65535, got %d", p) + } + return nil +} + +// Positive validates positive numbers +func Positive[T int64 | float64](n T) error { + if n <= 0 { + return fmt.Errorf("must be positive, got %v", n) + } + return nil +} + +// NonNegative validates non-negative numbers +func NonNegative[T int64 | float64](n T) error { + if n < 0 { + return fmt.Errorf("must be non-negative, got %v", n) + } + return nil +} + +// IPAddress validates IP address format +func IPAddress(s string) error { + if s == "" || s == "0.0.0.0" || s == "::" { + return nil // Allow common defaults + } + if net.ParseIP(s) == nil { + return fmt.Errorf("invalid IP address: %s", s) + } + return nil +} + +// IPv4Address validates IPv4 address format +func IPv4Address(s string) error { + if s == "" || s == "0.0.0.0" { + return nil // Allow common defaults + } + ip := net.ParseIP(s) + if ip == nil || ip.To4() == nil { + return fmt.Errorf("invalid IPv4 address: %s", s) + } + return nil +} + +// IPv6Address validates IPv6 address format +func IPv6Address(s string) error { + if s == "" || s == "::" { + return nil // Allow common defaults + } + ip := net.ParseIP(s) + if ip == nil { + return fmt.Errorf("invalid IPv6 address: %s", s) + } + // Valid net.ParseIP with nil ip.To4 indicates IPv6 + if ip.To4() != nil { + return fmt.Errorf("invalid IPv6 address (is an IPv4 address): %s", s) + } + return nil +} + +// URLPath validates URL path format +func URLPath(s string) error { + if s != "" && !strings.HasPrefix(s, "/") { + return fmt.Errorf("must start with /: %s", s) + } + return nil +} + +// OneOf creates a validator for allowed values +func OneOf[T comparable](allowed ...T) func(T) error { + return func(val T) error { + for _, a := range allowed { + if val == a { + return nil + } + } + return fmt.Errorf("must be one of %v, got %v", allowed, val) + } +} + +// Range creates a min/max validator +func Range[T int64 | float64](min, max T) func(T) error { + return func(val T) error { + if val < min || val > max { + return fmt.Errorf("must be %v-%v, got %v", min, max, val) + } + return nil + } +} + +// Pattern creates a regex validator +func Pattern(pattern string) func(string) error { + re := regexp.MustCompile(pattern) + return func(s string) error { + if !re.MatchString(s) { + return fmt.Errorf("must match pattern %s", pattern) + } + return nil + } +} + +// NonEmpty validates non-empty strings +func NonEmpty(s string) error { + if strings.TrimSpace(s) == "" { + return fmt.Errorf("must not be empty") + } + return nil +} \ No newline at end of file diff --git a/validator_test.go b/validator_test.go new file mode 100644 index 0000000..adf6777 --- /dev/null +++ b/validator_test.go @@ -0,0 +1,163 @@ +// FILE: lixenwraith/config/validator_test.go +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestPortValidator tests the Port validator +func TestPortValidator(t *testing.T) { + tests := []struct { + name string + port int64 + wantErr bool + }{ + {"ValidLowPort", 1, false}, + {"ValidCommonPort", 8080, false}, + {"ValidHighPort", 65535, false}, + {"InvalidZeroPort", 0, true}, + {"InvalidNegativePort", -1, true}, + {"InvalidTooHighPort", 65536, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Port(tt.port) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +// TestPositiveValidator tests the Positive validator +func TestPositiveValidator(t *testing.T) { + t.Run("Int64", func(t *testing.T) { + assert.NoError(t, Positive(int64(1))) + assert.Error(t, Positive(int64(0))) + assert.Error(t, Positive(int64(-1))) + }) + + t.Run("Float64", func(t *testing.T) { + assert.NoError(t, Positive(0.001)) + assert.Error(t, Positive(0.0)) + assert.Error(t, Positive(-0.001)) + }) +} + +// TestNonNegativeValidator tests the NonNegative validator +func TestNonNegativeValidator(t *testing.T) { + t.Run("Int64", func(t *testing.T) { + assert.NoError(t, NonNegative(int64(1))) + assert.NoError(t, NonNegative(int64(0))) + assert.Error(t, NonNegative(int64(-1))) + }) + + t.Run("Float64", func(t *testing.T) { + assert.NoError(t, NonNegative(0.001)) + assert.NoError(t, NonNegative(0.0)) + assert.Error(t, NonNegative(-0.001)) + }) +} + +// TestIPAddressValidators tests all IP-related validators +func TestIPAddressValidators(t *testing.T) { + t.Run("IPAddress", func(t *testing.T) { + assert.NoError(t, IPAddress("192.168.1.1")) + assert.NoError(t, IPAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334")) + assert.NoError(t, IPAddress("")) + assert.NoError(t, IPAddress("0.0.0.0")) + assert.NoError(t, IPAddress("::")) + assert.Error(t, IPAddress("not-an-ip")) + assert.Error(t, IPAddress("192.168.1.256")) + }) + + t.Run("IPv4Address", func(t *testing.T) { + assert.NoError(t, IPv4Address("192.168.1.1")) + assert.NoError(t, IPv4Address("")) + assert.NoError(t, IPv4Address("0.0.0.0")) + assert.Error(t, IPv4Address("::1")) // Is not IPv4 + assert.Error(t, IPv4Address("not-an-ip")) + }) + + t.Run("IPv6Address", func(t *testing.T) { + assert.NoError(t, IPv6Address("2001:db8::1")) + assert.NoError(t, IPv6Address("")) + assert.NoError(t, IPv6Address("::")) + assert.Error(t, IPv6Address("127.0.0.1")) // Is not IPv6 + assert.Error(t, IPv6Address("not-an-ip")) + }) +} + +// TestURLPathValidator tests the URLPath validator +func TestURLPathValidator(t *testing.T) { + assert.NoError(t, URLPath("/api/v1")) + assert.NoError(t, URLPath("/")) + assert.NoError(t, URLPath("")) + assert.Error(t, URLPath("api/v1")) + assert.Error(t, URLPath("no-slash")) +} + +// TestOneOfValidator tests the OneOf validator +func TestOneOfValidator(t *testing.T) { + t.Run("String", func(t *testing.T) { + validator := OneOf("prod", "dev", "staging") + assert.NoError(t, validator("prod")) + assert.NoError(t, validator("dev")) + err := validator("test") + assert.Error(t, err) + assert.Contains(t, err.Error(), "must be one of") + }) + + t.Run("Int", func(t *testing.T) { + validator := OneOf(200, 404, 500) + assert.NoError(t, validator(404)) + err := validator(302) + assert.Error(t, err) + assert.Contains(t, err.Error(), "must be one of") + }) +} + +// TestRangeValidator tests the Range validator +func TestRangeValidator(t *testing.T) { + t.Run("Int64", func(t *testing.T) { + validator := Range[int64](10, 100) + assert.NoError(t, validator(10)) + assert.NoError(t, validator(50)) + assert.NoError(t, validator(100)) + assert.Error(t, validator(9)) + assert.Error(t, validator(101)) + }) + + t.Run("Float64", func(t *testing.T) { + validator := Range[float64](-1.5, 1.5) + assert.NoError(t, validator(-1.5)) + assert.NoError(t, validator(0.0)) + assert.NoError(t, validator(1.5)) + assert.Error(t, validator(-1.51)) + assert.Error(t, validator(1.51)) + }) +} + +// TestPatternValidator tests the Pattern validator +func TestPatternValidator(t *testing.T) { + // Simple email regex + validator := Pattern(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + assert.NoError(t, validator("test@example.com")) + assert.NoError(t, validator("user.name+alias@domain.co.uk")) + assert.Error(t, validator("not-an-email")) + assert.Error(t, validator("test@example")) +} + +// TestNonEmptyValidator tests the NonEmpty validator +func TestNonEmptyValidator(t *testing.T) { + assert.NoError(t, NonEmpty("hello")) + assert.NoError(t, NonEmpty(" a ")) + assert.Error(t, NonEmpty("")) + assert.Error(t, NonEmpty(" ")) + assert.Error(t, NonEmpty(" \t\n ")) +} \ No newline at end of file