docs: explain what distinguishes validate (+ comment audit) #11

Merged
aneurin merged 4 commits from docs-positioning into main 2026-09-07 16:35:55 +00:00
3 changed files with 96 additions and 3 deletions
Showing only changes of commit 164dcbf367 - Show all commits
+1 -1
View File
@@ -64,7 +64,7 @@ validate.Collect(rules...)
|---|---| |---|---|
| Compose | `All`, `Collect` | | Compose | `All`, `Collect` |
| Presence / equality | `Required`, `Equal`, `In`, `NotIn` | | Presence / equality | `Required`, `Equal`, `In`, `NotIn` |
| String length & content | `MinLength`, `MaxLength`, `Chars`, `ExceptChars`, `Prefix`, `Suffix`, `Contains`, `Match` | | String length & content | `MinLength`, `MaxLength` (runes), `MinLengthBytes`, `MaxLengthBytes`, `Chars`, `ExceptChars`, `Prefix`, `Suffix`, `Contains`, `Match` |
| Formats | `Email`, `URL`, `UUID` | | Formats | `Email`, `URL`, `UUID` |
| Numbers | `Min`, `Max`, `MinFloat32`, `MaxFloat32`, `MinFloat64`, `MaxFloat64` | | Numbers | `Min`, `Max`, `MinFloat32`, `MaxFloat32`, `MinFloat64`, `MaxFloat64` |
| Slices | `MinSize`, `MaxSize` | | Slices | `MinSize`, `MaxSize` |
+29 -2
View File
@@ -3,8 +3,10 @@ package validate
import "unicode/utf8" import "unicode/utf8"
var ( var (
ErrMustBeLonger = NewError("must contain at least %d characters") ErrMustBeLonger = NewError("must contain at least %d characters")
ErrMustBeShorter = NewError("must contain no more than %d characters") ErrMustBeShorter = NewError("must contain no more than %d characters")
ErrMustHaveMoreBytes = NewError("must have at least %d bytes")
ErrMustHaveFewerBytes = NewError("must have no more than %d bytes")
Outdated
Review

The original intention was to measure text length. Does this make it more correct to measure runes rather than bytes?

The original intention was to measure text length. Does this make it more correct to measure runes rather than bytes?
) )
// MaxLength validates that a string is no longer than a given maximum. // MaxLength validates that a string is no longer than a given maximum.
@@ -28,3 +30,28 @@ func MinLength(l int) func(string) error {
return nil return nil
} }
} }
// MaxLengthBytes validates that a string is no longer than a given maximum
// number of bytes (len). Prefer [MaxLength] for a limit on visible
// characters; use this when the budget is genuinely a byte count, such as
// a fixed-width column or a wire-format field.
func MaxLengthBytes(l int) func(string) error {
return func(value string) error {
if len(value) > l {
return ErrMustHaveFewerBytes.With(l)
}
return nil
}
}
// MinLengthBytes validates that a string is at least a given minimum
// number of bytes (len). See [MaxLengthBytes] on when to prefer this over
// [MinLength].
func MinLengthBytes(l int) func(string) error {
return func(value string) error {
if len(value) < l {
return ErrMustHaveMoreBytes.With(l)
}
return nil
}
}
+66
View File
@@ -73,3 +73,69 @@ func TestMinLength(t *testing.T) {
} }
} }
} }
func ExampleMaxLengthBytes() {
testMaxLengthBytes := MaxLengthBytes(8)
fmt.Println(testMaxLengthBytes("cafés round the world"))
// Output: must have no more than 8 bytes
}
func ExampleMinLengthBytes() {
testMinLengthBytes := MinLengthBytes(8)
fmt.Println(testMinLengthBytes("2short"))
// Output: must have at least 8 bytes
}
func TestMaxLengthBytes(t *testing.T) {
testCases := map[int]map[string]error{
8: {
"abcd": nil,
"abcdefgh": nil,
"abcdéfg": nil, // 7 runes, 8 bytes
"abcdéfgh": ErrMustHaveFewerBytes.With(8), // 8 runes, 9 bytes
"abcdefghi": ErrMustHaveFewerBytes.With(8),
},
}
for setup, values := range testCases {
testMaxLengthBytes := MaxLengthBytes(setup)
for input, want := range values {
t.Run(fmt.Sprintf("%d/%s", setup, input), func(t *testing.T) {
got := testMaxLengthBytes(input)
if !errors.Is(got, want) {
t.Error("got", got)
t.Error("want", want)
}
})
}
}
}
func TestMinLengthBytes(t *testing.T) {
testCases := map[int]map[string]error{
8: {
"abcd": ErrMustHaveMoreBytes.With(8),
"abcdefg": ErrMustHaveMoreBytes.With(8),
"abcdéf": ErrMustHaveMoreBytes.With(8), // 6 runes, 7 bytes
"abcdéfg": nil, // 7 runes, 8 bytes
"abcdefgh": nil,
},
}
for setup, values := range testCases {
testMinLengthBytes := MinLengthBytes(setup)
for input, want := range values {
t.Run(fmt.Sprintf("%d/%s", setup, input), func(t *testing.T) {
got := testMinLengthBytes(input)
if !errors.Is(got, want) {
t.Error("got", got)
t.Error("want", want)
}
})
}
}
}