Skip to content

Latest commit

 

History

158 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ValidGen

Validator is an amazing project. But in applications with high frequency use, the way that validator works (i.e. reflection) can cause performance penalty.

ValidGen born to solve that gap. Instead of use reflection, ValidGen uses the code generating approach.

At this time it is an unstable project and should not be used in production environments.

Internals

ValidGen internals follows the current generator from the parser through the analyzer, code generator, and package writer, and says where TestGen fits.

How to build ValidGen

The following requirements are needed to build the project:

  • Git
  • Go >= 1.24
  • Make

The steps to build are:

# Clone the project repository
git clone git@github.com:opencodeco/validgen.git

# Enter in the project root folder
cd validgen

# Build the binary
make build

After that the executable will be in bin/validgen.

Releases

Push a tag named vMAJOR.MINOR.PATCH, with an optional pre-release suffix such as -rc.1. That tag push runs Release on a GitHub-hosted runner. The workflow cross-compiles ValidGen with CGO_ENABLED=0 and attaches the binaries to the GitHub release for that tag. Pull requests do not publish releases.

The release assets are:

  • validgen_v1.2.3_linux_amd64
  • validgen_v1.2.3_linux_arm64
  • validgen_v1.2.3_darwin_amd64
  • validgen_v1.2.3_darwin_arm64
  • SHA256SUMS

amd64 is the Intel 64-bit build. arm64 is the Arm 64-bit build.

Optional JSON unmarshaling

By default ValidGen only generates TValidate functions. Pass -unmarshal-json to also generate encoding/json.Unmarshaler methods that decode with an alias (to avoid recursion) and then validate:

./bin/validgen -unmarshal-json ./mypackage

Generated methods look like:

func (obj *User) UnmarshalJSON(b []byte) error {
	type alias User
	if err := json.Unmarshal(b, (*alias)(obj)); err != nil {
		return err
	}
	if errs := UserValidate(obj); len(errs) > 0 {
		return errors.Join(errs...)
	}
	return nil
}

Malformed JSON still returns the usual encoding/json decode error. Validation failures are returned via errors.Join over the []error from UserValidate.

Validations

Struct tags may use valid or validate. When both keys are present, valid is the one that is read. The common go-playground example uses validate.

The following validations will be implemented:

  • eq (equal): must be equal to the specified value
  • eq_ignore_case (equal ignoring case): must be equal to the specified value (ignoring case)
  • gt (greater than): must be > to the specified value
  • gte (greater than or equal): must be >= to the specified value
  • lt (less than): must be < to the specified value
  • lte (less than or equal): must be <= to the specified value
  • neq (not equal): must not be equal to the specified value
  • neq_ignore_case (not equal ignoring case): must not be equal to the specified value (ignoring case)
  • len (length): must have the following length
  • max (max): must have no more than max characters
  • min (min): must have no less than min characters
  • in (in): must be one of the following values
  • nin (not in): must not be one of the following values
  • required (required): is required
  • email (email): must be a valid email format (empty is valid for optional fields)
  • oneof (one of): string or integer must be one of the space-separated values
  • hexcolor, rgb, rgba, hsl, hsla: string must match that color format
  • iscolor: alias for hexcolor, rgb, rgba, hsl, or hsla
  • eqfield (equal field): field must be equal to another field
  • neqfield (not equal field): field must not be equal to another field
  • gtefield (greater than or equal field): field must be greater than or equal to another field
  • gtfield (greater than field): field must be greater than another field
  • ltefield (less than or equal field): field must be less than or equal to another field
  • ltfield (less than field): field must be less than another field
  • dive: tags after dive apply to each slice element, array element, or map value
  • keys, endkeys: after dive on a map, tags between keys and endkeys apply to each key and tags after endkeys apply to each value

dive

Tags before dive apply to the collection. Tags after dive apply to each element. A struct element is validated with that struct's generated function. Without dive, a slice or map is checked only as a collection, including a slice of structs.

type User struct {
	Addresses []*Address        `valid:"required,dive,required"`
	Labels    map[string]string `valid:"dive,required"`
}

required before dive checks that Addresses is not empty. After dive, each pointer must be non-nil and Address field tags run. Each Labels value must be non-empty. Plain dive validates map values.

keys and endkeys

keys follows dive immediately and applies only to maps. Tags between keys and endkeys validate each map key. Tags after endkeys validate each map value.

type User struct {
	Labels map[string]string `valid:"dive,keys,min=2,endkeys,required"`
	Scores map[uint8]string  `valid:"dive,keys,gte=1,endkeys,required"`
}

min=2 checks each Labels key. required checks each Labels value. gte=1 checks each Scores key. A missing endkeys, an extra endkeys, keys on a non-map, dive inside keys, another keys block, and a non-scalar map key are rejected.

The following table shows the validations and possible types, where:

  • "I" means "Implemented"
  • "W" means "Will be implemented"
  • "P" means "Partially implemented"
  • "-" means "Will not be implemented"
Validation/Type String Numeric types (integers and floats) Complex Boolean Slice Array Map Time Duration
eq I I I I - - - W W
eq_ignore_case I - - - - - - - -
gt - I - - - - - W W
gte - I - - - - - W W
lt - I - - - - - W W
lte - I - - - - - W W
neq I I I I - - - W W
neq_ignore_case I - - - - - - - -
len I - - - I - W - -
max I - - - I - W W W
min I - - - I - W W W
in I I I - I I W - W
nin I I I - I I W - W
required I I I - I - W W W
email I - - - - - - - -
oneof I I - - - - - - -
hexcolor I - - - - - - - -
rgb I - - - - - - - -
rgba I - - - - - - - -
hsl I - - - - - - - -
hsla I - - - - - - - -
iscolor I - - - - - - - -
eqfield I I I I - - - W W
neqfield I I I I - - - W W
gtefield - I - - - - - W W
gtfield - I - - - - - W W
ltefield - I - - - - - W W
ltfield - I - - - - - W W

Complex (complex64, complex128) supports eq, neq, in, nin, eqfield, and neqfield via Go == / !=, and required via != 0 (the zero value 0+0i). Tag values are Go imaginary literals without spaces, for example eq=1+2i and in=1+2i 5+6i. Ordering tags (gt, gte, lt, lte, gtfield, gtefield, ltfield, ltefield) are rejected: Go has no < / > / <= / >= for complex values.

Steps to run the unit tests

The steps to run the unit tests are:

# Enter in the project root folder
cd validgen

# Run the unit tests
make unittests

Benchmarks

Comparisons with go-playground/validator live in validgen-benchmarks. That module holds the small three-way benchmark, the generated comparison suite, and the color helper check.

git clone git@github.com:opencodeco/validgen-benchmarks.git
cd validgen-benchmarks
make bench
make color

make cmp runs the generated suite. The default bench time is 5 seconds per benchmark. make cmp BENCH_TIME=100ms is a shorter pass.

Steps to run the end-to-end tests

The steps to run the end-to-end tests are:

# Enter in the project root folder
cd validgen

# Run the end-to-end tests
make endtoendtests

Examples

Examples live in opencodeco/validgen-samples.

That repository is a separate Go module. It shows the ValidGen CLI, the generated import of github.com/opencodeco/validgen/types, and a caller importing the package that owns the structs.

Build the CLI from this repository with make build. The binary is bin/validgen. It takes one path and writes validator__.go next to the structs.

./bin/validgen /path/to/package

Pass -unmarshal-json to also generate UnmarshalJSON methods. The samples repository shows that flag on signup.

Recorded comparison results

These numbers are from an Apple M4 Pro, 12 cores, running go test -bench=. -benchmem -benchtime=5s on the generated suite:

goos: darwin
goarch: arm64
pkg: github.com/opencodeco/validgen-benchmarks/cmp
cpu: Apple M4 Pro (12 Cores used)

The following table as the performance results:

Test name ValidGen GoValidator Performance
StringRequired 5.000 ns/op 40.12 ns/op 8.02x
StringEq 5.000 ns/op 39.90 ns/op 7.98x
StringEqIC 14.96 ns/op 40.63 ns/op 2.71x
StringNeq 5.000 ns/op 40.65 ns/op 8.13x
StringNeqIC 5.000 ns/op 41.05 ns/op 8.21x
StringLen 5.000 ns/op 44.35 ns/op 8.87x
StringMax 5.000 ns/op 45.15 ns/op 9.03x
StringMin 5.000 ns/op 45.10 ns/op 9.02x
StringIn 5.000 ns/op 49.75 ns/op 9.95x
StringEmail 167.4 ns/op 436.3 ns/op 2.60x

The following table as the raw results:

Test name Iterations Nanoseconds per operation Number os bytes allocated per operation Number of allocations per operation
BenchmarkValidGenStringRequired-12 1.000.000.000 5.000 ns/op 0 B/op 0 allocs/op
BenchmarkValidatorStringRequired-12 149.656.641 40.12 ns/op 0 B/op 0 allocs/op
BenchmarkValidGenStringEq-12 1.000.000.000 5.000 ns/op 0 B/op 0 allocs/op
BenchmarkValidatorStringEq-12 150.419.842 39.90 ns/op 0 B/op 0 allocs/op
BenchmarkValidGenStringEqIC-12 401.652.991 14.96 ns/op 8 B/op 1 allocs/op
BenchmarkValidatorStringEqIC-12 147.517.011 40.63 ns/op 0 B/op 0 allocs/op
BenchmarkValidGenStringNeq-12 1.000.000.000 5.000 ns/op 0 B/op 0 allocs/op
BenchmarkValidatorStringNeq-12 147.375.966 40.65 ns/op 0 B/op 0 allocs/op
BenchmarkValidGenStringNeqIC-12 1.000.000.000 5.000 ns/op 0 B/op 0 allocs/op
BenchmarkValidatorStringNeqIC-12 146.089.116 41.05 ns/op 0 B/op 0 allocs/op
BenchmarkValidGenStringLen-12 1.000.000.000 5.000 ns/op 0 B/op 0 allocs/op
BenchmarkValidatorStringLen-12 135.221.859 44.35 ns/op 0 B/op 0 allocs/op
BenchmarkValidGenStringMax-12 1.000.000.000 5.000 ns/op 0 B/op 0 allocs/op
BenchmarkValidatorStringMax-12 133.947.687 45.15 ns/op 0 B/op 0 allocs/op
BenchmarkValidGenStringMin-12 1.000.000.000 5.000 ns/op 0 B/op 0 allocs/op
BenchmarkValidatorStringMin-12 133.335.433 45.10 ns/op 0 B/op 0 allocs/op
BenchmarkValidGenStringIn-12 1.000.000.000 5.000 ns/op 0 B/op 0 allocs/op
BenchmarkValidatorStringIn-12 120.405.889 49.75 ns/op 0 B/op 0 allocs/op
BenchmarkValidGenStringEmail-12 35.513.684 167.4 ns/op 0 B/op 0 allocs/op
BenchmarkValidatorStringEmail-12 13.740.566 436.3 ns/op 88 B/op 5 allocs/op

Steps to run linters

The steps to run all linters are:

# Enter in the project root folder
cd validgen

# Download and install golang-ci-lint (run just once)
make setup

# Run all enabled linters
make lint

Contribute

You can find instructions on how to contribute code and bug reports in the CONTRIBUTING guide.

License

ValidGen uses MIT License.

About

ValidGen is a high-performance alternative to validator, generating validation code at build time instead of using reflection. It aims to be compatible with validator tags but is currently experimental and not recommended for production use.

Resources

Contributing

Stars

13 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages