Simple, type‑specific fluent assertions for Go (initially inspired by PHP webmozart/assert).
Requires Go (1.18) or later.
assert.Str().Word().LenMax(5).Check(value)
If you’ve ever worked with PHP, you probably used the webmozart/assert package. It made it easy to validate method arguments using supplementary criteria in addition to type checking -- for example, verifying that integers were natural or strings were non‑empty. Moreover, combinations of such checks could be used to build more complex validations instead of using heavy validation frameworks that might impose compromises on project design.
In Go, there are already well‑known validation packages, but most of them are designed for broad or complex cases, such as struct validation by tags, validation with network requests, etc. -- this also can be overkill.
Some Go-packages also have not enough friendly interface and weak typing. For example, it can be possible to add length checks to an integer value validation flow, etc. Such things confuse, make interface more complex and increase chances to make an accidental mistake.
So something else was needed...
Something simpler... Something more type-specific... Something more friendly... Something like this package! 😊
This package un autre package de validation, mais avec les cartes à jouer et les femmes fatales
provides a simple type-specific fluent interface for building validations like
assert.Str().Word().LenMax(5).Check(value)
The package provides assertions for specific types as well as more abstract assertions for broader type support.
Each assertion supports only specific methods relevant to its value type -- this prevents accidental mistakes.
For example, it is impossible to validate integer value via some length-based rule,
because Num assertion does not have methods to add such validation to the chain.
Each assertion supports a Custom() check for cases not covered by built-ins.
BoolNum-- for all int, uint and float based types (seeNumericTypes)StrTime-- fortime.TimetypeTimeDur-- fortime.Durationtype
Any-- for any typeCmp-- for any comparable typeSliceAny-- for slice-based types with any type of elementsSliceCmp-- for slice-based types with comparable type of elements
Assertions support a few types of results:
- panic -- via
Must()orMustAll()methods - panic or returning value -- via
MustGet()orMustAllGet()methods - returning errors -- via
Check()orCheckAll()methods
Each rule and result method can optionally take custom message in the customErrMsg argument:
- If a rule method is customized, the custom message replaces the default message when rule fails.
- If a result method is customized, the custom message replaces any message when the chain fails.
The package also provides shortcuts
for the most popular assertions in package-level functions with ...Check, ...Must and ...MustGet result variants:
NotZero...NotNilDeep...True...False...
This is the initial use-case of this package -- ensuring without ugly boilerplate code that method works with valid values of the argument types, not with nil-pointers, etc.
package example
import (
"github.com/selyukovn/go-wm-assert"
"time"
)
type Account struct{ /* ... */ }
type EventCollection struct{ /* ... */ }
func (a *Account) Deactivate(deactivatedAt time.Time, evs *EventCollection) error {
assert.Time().NotZero().LessEq(time.Now()).Must(deactivatedAt)
assert.Cmp[*EventCollection]().NotEq(nil).Must(evs)
// or with popular shortcut for `evs`
assert.NotNilDeepMust(evs)
// ...
return nil
}package config
import (
"github.com/selyukovn/go-wm-assert"
"os"
)
func LoadEnv() *Env {
env = &Env{}
// ...
env.AppName = assert.Str().Word().MustGet(os.Getenv("APP_NAME"))
env.IsDebug = assert.Str().In([]string{"0", "1"}).MustGet(os.Getenv("IS_DEBUG"))
// ...
return env
}This package is not about a form validation, but customizing messages makes possible to use it as a "brick" to build the things you need in a simple and clean way.
package example
import (
"fmt"
"github.com/selyukovn/go-wm-assert"
)
type Name struct{ value string }
func NameFromString(value string) (Name, error) {
// custom error message that overrides any other in the chain */
err := assert.Str().Word().Check(value, fmt.Sprintf("Name %q is incorrect!", value))
if err != nil {
return Name{}, err
}
return Name{value: value}, nil
}package example
import "github.com/selyukovn/go-wm-assert"
type SignUpForm struct {
email string
name string // let it be optional
age uint
agreement bool
errors map[string][]error
}
// ...
func (f *SignUpForm) Validate() bool {
f.errors = map[string][]error{
"email": {},
"name": {},
"age": {},
"agreement": {},
}
f.errors["email"] = assert.Str().
NotEmpty("Email is required!").
Regexp(
emailRegexpCompiled,
// custom error message only for this rule
// instead of technical "value ... regexp ..."
"Email is incorrect!",
).
Custom(func(v string) error {
// e.g. check that it is not registered previously
return nil
}).
CheckAll(f.email)
// optional field, remember?
if f.name != "" {
f.errors["name"] = assert.Str().
Word("Only letters and '-' allowed!").
RunesMin(2, "Too short, isn't it?").
RunesMax(255, "Too long, isn't it?").
NotIn(
[]string{ /* e.g. some set of bad words or so */ },
"Is that your real name, friend?",
).
CheckAll(f.name)
}
f.errors["age"] = assert.Num[uint]().
GreaterEq(18, "Things are serious -- come back later!").
Less(65, "Take a rest, friend!").
CheckAll(f.age)
f.errors["agreement"] = assert.Bool().
True("This flag is required!").
CheckAll(f.agreement)
return len(f.errors["email"]) == 0 &&
len(f.errors["name"]) == 0 &&
len(f.errors["age"]) == 0 &&
len(f.errors["agreement"]) == 0
}
func (f *SignUpForm) NameErrors() []error {
return f.errors["name"]
}
// ...b_*.go— basic componentss_*.go— specific assertions (Str,Num, etc.)shortcuts.go— shortcuts for the most popular assertionsreadme_test.go— examples from the Readme (ensures correctness)