From e65d7d66c192a60a5689a6b311c8d46c2f96fc17 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 7 Sep 2026 13:47:38 +0100 Subject: [PATCH] add Collect: run every validator and join all errors Collect is the all-errors-at-once counterpart to All. All still stops at the first failure (unchanged); Collect runs every function and returns the failures joined with errors.Join, which errors.Is still matches element by element. Kept as a separate function rather than a flag on All so All's signature and behaviour don't change. Closes #5 Co-Authored-By: Claude Sonnet 5 --- collect.go | 24 +++++++++++++++++++++ collect_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 collect.go create mode 100644 collect_test.go diff --git a/collect.go b/collect.go new file mode 100644 index 0000000..ade5108 --- /dev/null +++ b/collect.go @@ -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...) + } +} diff --git a/collect_test.go b/collect_test.go new file mode 100644 index 0000000..b6ae954 --- /dev/null +++ b/collect_test.go @@ -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) + } + }) +}