diff --git a/lazy.go b/lazy.go new file mode 100644 index 000000000..af6cc2195 --- /dev/null +++ b/lazy.go @@ -0,0 +1,14 @@ +//go:build go1.21 + +package validator + +import ( + "regexp" + "sync" +) + +func lazyRegexCompile(str string) func() *regexp.Regexp { + return sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(str) + }) +} diff --git a/lazy_compat.go b/lazy_compat.go new file mode 100644 index 000000000..bd0bf4e94 --- /dev/null +++ b/lazy_compat.go @@ -0,0 +1,46 @@ +//go:build !go1.21 + +package validator + +import ( + "regexp" + "sync" +) + +// Copied and adapted from go1.21 stdlib's sync.OnceValue for backwards compatibility: +// OnceValue returns a function that invokes f only once and returns the value +// returned by f. The returned function may be called concurrently. +// +// If f panics, the returned function will panic with the same value on every call. +func onceValue(f func() *regexp.Regexp) func() *regexp.Regexp { + var ( + once sync.Once + valid bool + p interface{} + result *regexp.Regexp + ) + g := func() { + defer func() { + p = recover() + if !valid { + panic(p) + } + }() + result = f() + f = nil + valid = true + } + return func() *regexp.Regexp { + once.Do(g) + if !valid { + panic(p) + } + return result + } +} + +func lazyRegexCompile(str string) func() *regexp.Regexp { + return onceValue(func() *regexp.Regexp { + return regexp.MustCompile(str) + }) +} diff --git a/lazy_test.go b/lazy_test.go new file mode 100644 index 000000000..4c7c74a17 --- /dev/null +++ b/lazy_test.go @@ -0,0 +1,83 @@ +package validator + +import ( + "regexp" + "sync" + "testing" +) + +// TestLazyRegexCompile_Basic tests that lazyRegexCompile compiles the regex only once and caches the result. +func TestLazyRegexCompile_Basic(t *testing.T) { + alphaRegexString := "^[a-zA-Z]+$" + alphaRegex := lazyRegexCompile(alphaRegexString) + + callCount := 0 + originalFunc := alphaRegex + alphaRegex = func() *regexp.Regexp { + callCount++ + return originalFunc() + } + + // Call the function multiple times + for i := 0; i < 10; i++ { + result := alphaRegex() + if result == nil { + t.Fatalf("Expected non-nil result") + } + if !result.MatchString("test") { + t.Fatalf("Expected regex to match 'test'") + } + } + + if callCount != 10 { + t.Fatalf("Expected call count to be 10, got %d", callCount) + } +} + +// TestLazyRegexCompile_Concurrent tests that lazyRegexCompile works correctly when called concurrently. +func TestLazyRegexCompile_Concurrent(t *testing.T) { + alphaRegexString := "^[a-zA-Z]+$" + alphaRegex := lazyRegexCompile(alphaRegexString) + + var wg sync.WaitGroup + const numGoroutines = 100 + + // Use a map to ensure all results point to the same instance + results := make(map[*regexp.Regexp]bool) + var mu sync.Mutex + + // Call the function concurrently + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + result := alphaRegex() + if result == nil { + t.Errorf("Expected non-nil result") + } + mu.Lock() + results[result] = true + mu.Unlock() + }() + } + wg.Wait() + + if len(results) != 1 { + t.Fatalf("Expected one unique regex instance, got %d", len(results)) + } +} + +// TestLazyRegexCompile_Panic tests that if the regex compilation panics, the panic value is propagated consistently. +func TestLazyRegexCompile_Panic(t *testing.T) { + faultyRegexString := "[a-z" + alphaRegex := lazyRegexCompile(faultyRegexString) + + defer func() { + if r := recover(); r == nil { + t.Fatalf("Expected a panic, but none occurred") + } + }() + + // Call the function, which should panic + alphaRegex() +} diff --git a/regexes.go b/regexes.go index 7e1dd5a03..df0cfde71 100644 --- a/regexes.go +++ b/regexes.go @@ -1,10 +1,5 @@ package validator -import ( - "regexp" - "sync" -) - const ( alphaRegexString = "^[a-zA-Z]+$" alphaNumericRegexString = "^[a-zA-Z0-9]+$" @@ -79,17 +74,6 @@ const ( spicedbTypeRegexString = "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)?[a-z][a-z0-9_]{1,62}[a-z0-9]$" ) -func lazyRegexCompile(str string) func() *regexp.Regexp { - var regex *regexp.Regexp - var once sync.Once - return func() *regexp.Regexp { - once.Do(func() { - regex = regexp.MustCompile(str) - }) - return regex - } -} - var ( alphaRegex = lazyRegexCompile(alphaRegexString) alphaNumericRegex = lazyRegexCompile(alphaNumericRegexString)