add Collect: run every validator and join all errors #10

Merged
aneurin merged 1 commits from add-collect into main 2026-09-07 12:50:51 +00:00
2 changed files with 79 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
package validate
import (
"errors"
)
// Collect validates a value against a sequence of validation functions,
// like All, but runs every function instead of stopping at the first
// failure. All failures are returned together as one joined error (see
// errors.Join); errors.Is still matches each individual error contained in
// it. If nothing fails, Collect returns nil.
func Collect[T any](fs ...func(T) error) func(T) error {
return func(value T) error {
var errs []error
for _, f := range fs {
if err := f(value); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
}
+55
View File
@@ -0,0 +1,55 @@
package validate
import (
"errors"
"fmt"
"testing"
)
func ExampleCollect() {
v := Collect(MinLength(4), Chars("0123456789abcdef"))
fmt.Println(v("xy"))
// Output:
// must contain at least 4 characters
// contains disallowed characters
}
func TestCollect(t *testing.T) {
v := Collect(
MinLength(4),
MaxLength(8),
Chars("0123456789abcdef"),
)
t.Run("no failures returns nil", func(t *testing.T) {
if err := v("abc123"); err != nil {
t.Fatalf("got %v, want nil", err)
}
})
t.Run("every failure is reported", func(t *testing.T) {
// too long (>8) and contains disallowed characters; not too short.
err := v("xyz!!!!!!!!")
if errors.Is(err, ErrMustBeLonger) {
t.Error("did not expect ErrMustBeLonger in", err)
}
if !errors.Is(err, ErrMustBeShorter) {
t.Error("want ErrMustBeShorter in", err)
}
if !errors.Is(err, ErrDisallowedChars) {
t.Error("want ErrDisallowedChars in", err)
}
})
t.Run("a single failure still matches with errors.Is", func(t *testing.T) {
err := v("12") // only too short
if !errors.Is(err, ErrMustBeLonger) {
t.Error("want ErrMustBeLonger in", err)
}
if errors.Is(err, ErrDisallowedChars) {
t.Error("did not expect ErrDisallowedChars in", err)
}
})
}