Valley is tool for generating plain Go validation code based on your Go code.
You can install the latest version of Valley using the following command. Alternatively, you can use a tagged version at the end for a specific release:
$ GO111MODULE=on go get -v github.com/seeruk/valley/cmd/valley@latest
Valley reads Go source code, and generates validation code based upon it. Valley will look at a
given file, pick out it's types and methods and identify types that appear to be configuring
validation constraints. That can be any struct type defined in a file, as long as it has any method
that returns nothing, and accepts a valley.Type
as it's only argument:
package example
import (
"github.com/seeruk/valley"
"github.com/seeruk/valley/validation/constraints"
)
// Request ...
type Request struct {
Inputs []string `valley:"inputs"`
Page int `valley:"page"`
}
// Constraints ...
func (r Request) Constraints(t valley.Type) {
t.Field(r.Inputs).
Constraints(constraints.MaxLength(256)). // Applies to the whole []string
Elements(constraints.MaxLength(16)) // Applies to each string in the []string
t.Field(r.Page).
Constraints(constraints.Min(1), constraints.Max(99))
}
See ./example/example.go
for a more comprehensive example of usage.
Once you've prepared you Go file, execute Valley, passing the file path as an argument:
$ valley ./example.go
By default this will produce another file alongside the input file (in the above example that would
./example_validate.go
). You can customise where the file is output using the -o
or --output
flag.
If any validation constraints are violated, the generated Validate
method will return those
violations. They contain a path, the kind of thing they're referencing, a message, and some misc
details that vary depending on which constraint was violated. For example:
[
{
"path": ".inputs.[0]",
"path_kind": "element",
"message": "a value is required"
}
]
You may have noticed the struct tags on the example Request
struct earlier. Those can be used to
customise the output in the "path"
key in the constraint violation. By default it will use the
field name as it's written in the Go source code. You can choose to use existing tags (e.g. a json
struct tag) by passing the -t
or --tag
flag with the name of the struct tag you'd like to use
instead. The json
struct tag is a very common use-case.
Currently the only option for extending Valley is to create a custom Valley binary. Don't worry though, this is really straightforward. The main function for Valley is a single line - and it's the only line you should need to use to create a custom binary.
os.Exit(cli.NewApplication(constraints.BuiltIn).Run(os.Args[1:], os.Environ()))
The only part that you need to change is the set of constraints you'd like to use. Valley uses it's
exposed BuiltIn
constraints which is a map. You can make a copy of this map and add your own, or
create your own entirely new set of constraints.
The map's key is the fully qualified name of the constraint function, mapped to the constraint generator (i.e. the function that returns the generated code and any other information like imports and variables to place in the generate file).
Take a look at the BuiltIn
constraints to see how they work. A straightforward one to look at is
the Valid
constraint.
Constraint generators are themselves constrained by the information that Valley is able to provide them. I hope that this information can be expanded upon in the future, but generally speaking this is all information from the source file that is read initially. Eventually I'd like to extend that to the package that file is in, and then further to any packages imported by that package, etc.
The built-in constraints may be used in your code by importing:
import "github.com/seeruk/valley/validation/constraints"
(Note: You can alias the import, and Valley should still successfully generate your validation code)
GoDoc documentation is available for the built-in constraints that should help with understanding how the constraints may be used.
Here's a quick list of all of the built-in constraints (more documentation below):
- AnyNRequired
- DeepEquals
- Equals
- ExactlyNRequired
- Length
- Max
- MaxLength
- Min
- MinLength
- MutuallyExclusive
- MutuallyInclusive
- Nil
- NotEquals
- NotNil
- OneOf
- Predicate
- Regexp
- RegexpString
- Required
- TimeAfter
- TimeBefore
- TimeStringAfter
- TimeStringBefore
- Valid
AnyNRequired:
Applicable to: Structs
Description: At least n
of the given fields must not be empty (uses the same logic as the
Required
constraint).
Usage:
t.Constraints(constraints.AnyNRequired(1, v.HomePhone, v.MobilePhone, v.WorkPhone))
DeepEquals
Applicable to: Fields
Description: Values must be deeply equal (i.e. reflect.DeepEqual
)
Usage:
t.Field(e.String).Constraints(constraints.DeepEquals("hello"))
t.Field(e.Int).Constraints(constraints.DeepEquals(12))
t.Field(e.Int).Constraints(constraints.DeepEquals(len(e.FloatSlice)*2))
t.Field(e.FloatSlice).Elements(constraints.DeepEquals(math.Pi))
Equals
Applicable to: Fields
Description: Values must be equal.
Usage:
t.Field(e.String).Constraints(constraints.Equals("hello"))
t.Field(e.Int).Constraints(constraints.Equals(12))
t.Field(e.Int).Constraints(constraints.Equals(len(e.FloatSlice)*2))
t.Field(e.FloatSlice).Elements(constraints.Equals(math.Pi))
ExactlyNRequired
Applicable to: Structs
Description: Exactly n
of the given fields must not be empty (uses the same logic as the
Required
constraint).
Usage:
t.Constraints(constraints.ExactlyNRequired(1, v.HomePhone, v.MobilePhone, v.WorkPhone))
Length
Applicable to: Fields
Description: Exactly length must be met.
Usage:
t.Field(e.SomeSlice).Constraints(constraints.Length(12))
t.Field(e.SomeString).Constraints(constraints.Length(8-(e.SomeInt-1)))
t.Field(e.SomeSomeMap).Constraints(constraints.Length(math.MaxInt64))
Max
Applicable to: Fields
Description: Maximum value must not be exceeded.
Usage:
t.Field(e.SomeInt).Constraints(constraints.Max(12))
t.Field(e.SomeFloat).Constraints(constraints.Max(8-(e.SomeInt-1)))
MaxLength
Applicable to: Fields
Description: Maximum length must not be exceeded.
Usage:
t.Field(e.SomeSlice).Constraints(constraints.MaxLength(12))
t.Field(e.SomeString).Constraints(constraints.MaxLength(8-(e.SomeInt-1)))
t.Field(e.SomeSomeMap).Constraints(constraints.MaxLength(math.MaxInt64))
Min
Applicable to: Fields
Description: Minimum value must be met.
Usage:
t.Field(e.SomeInt).Constraints(constraints.Min(12))
t.Field(e.SomeFloat).Constraints(constraints.Min(8-(e.SomeInt-1)))
MinLength
Applicable to: Fields
Description: Minimum length must be met.
Usage:
t.Field(e.SomeSlice).Constraints(constraints.MinLength(12))
t.Field(e.SomeString).Constraints(constraints.MinLength(8-(e.SomeInt-1)))
t.Field(e.SomeSomeMap).Constraints(constraints.MinLength(math.MaxInt8))
MutuallyExclusive
Applicable to: Structs
Description: Only one of the given fields must be set.
Usage:
t.Constraints(constraints.MutuallyExclusive(e.Username, e.EmailAddress))
MutuallyInclusive
Applicable to: Structs
Description: If any one of the given fields is set, then all of the given fields must be set.
Usage:
t.Constraints(constraints.MutuallyInclusive(e.ReceiveMarketing, e.EmailAddress))
Nil
Applicable to: Fields
Description: Value must be nil.
Usage:
t.Field(e.SomePtr).Constraints(constraints.Nil())
t.Field(e.SomeSlice).Constraints(constraints.Nil())
t.Field(e.SomeInterface).Constraints(constraints.Nil())
NotEquals
Applicable to: Fields
Description: Values must not be equal.
Usage:
t.Field(e.SomeInt).Constraints(constraints.Equals(12))
t.Field(e.SomeInt).Constraints(constraints.Equals(e.SomeOtherInt*23))
t.Field(e.SomeInt).Constraints(constraints.Equals(int(math.Max(e.SomeOtherInt, 23))))
NotNil
Applicable to: Fields
Description: Value must not be nil.
Usage:
t.Field(e.SomePtr).Constraints(constraints.NotNil())
t.Field(e.SomeSlice).Constraints(constraints.NotNil())
t.Field(e.SomeInterface).Constraints(constraints.NotNil())
One Of
Applicable to: Fields
Description: Value must be one of the given allowed values.
Usage:
t.Field(e.SomeString).Constraints(constraints.OneOf("Hello, World!", "Hello, GitHub!"))
Predicate
Applicable to: Fields
Description: Pass a custom predicate that will be rendered as a violation, returning a given message as the description of any violation.
Usage:
t.Field(e.String).Constraints(constraints.Predicate(
strings.HasPrefix(e.String, "custom") && len(e.String) == 32,
"value must be a valid custom ID",
))
Regexp
Applicable to: Fields
Description: Value must match the given reference to a compiled *regexp.Regexp instance.
Usage:
t.Field(e.String).Constraints(constraints.Regexp(valley.PatternUUID))
RegexpString
Applicable to: Fields
Description: Value must match the given regular expression string. The regular expression string will be used to create a package-local variable with a unique name that will compile when imported.
Usage:
t.Field(e.String).Constraints(constraints.RegexpString("^Example$"))
Required
Applicable to: Fields
Description: Value is required, behaves like (and sometimes uses) reflect.Value.IsZero()
.
Usage:
t.Field(e.Nested).Constraints(constraints.Required())
TimeAfter
Applicable to: Fields
Description: Value must be after the given time. The value may either be be an existing
time.Time
value, or you can pass in an expression using something like time.Date
.
Usage:
t.Field(e.Time).Constraints(constraints.TimeAfter(time.Date(1890, time.October, 1, 0, 0, 0, 0, time.UTC)))
t.Field(e.Time).Constraints(constraints.TimeAfter(timeYosemite))
TimeBefore
Applicable to: Fields
Description: Value must be before the given time. The value may either be be an existing
time.Time
value, or you can pass in an expression using something like time.Date
.
Usage:
t.Field(e.Time).Constraints(constraints.TimeBefore(time.Date(1890, time.October, 1, 0, 0, 0, 0, time.UTC)))
t.Field(e.Time).Constraints(constraints.TimeBefore(timeYosemite))
TimeStringAfter
Applicable to: Fields
Description: Value must be after the given time string. The value can be a string, or a reference to a string.
Usage:
t.Field(e.Time).Constraints(constraints.TimeAfter(time.Date(1890, time.October, 1, 0, 0, 0, 0, time.UTC)))
t.Field(e.Time).Constraints(constraints.TimeAfter(timeYosemite))
TimeStringBefore
Applicable to: Fields
Description: Value must be before the given time string. The value can be a string, or a reference to a string.
Usage:
t.Field(e.Time).Constraints(constraints.TimeBefore(time.Date(1890, time.October, 1, 0, 0, 0, 0, time.UTC)))
t.Field(e.Time).Constraints(constraints.TimeBefore(timeYosemite))
Valid
Applicable to: Fields
Description: Calls Validate()
on the value, used to validate nested structures.
Usage:
t.Field(e.Nested).Constraints(constraints.Valid())
t.Field(e.NestedSlice).Elements(constraints.Valid())
Previously I've implemented validation in Go using reflection, and while reflection isn't actually as slow as you might expect it does come with other issues. By generating validation code instead of resorting to reflection you regain the protection that the Go compiler gives you. Even if the output of Valley is wrong (or you misconfigure the constraints) your application would fail to compile, alerting you to the issue.
On the topic of performance, the code generated by Valley is still a lot faster than reflection.
This is for several reasons. One is that I've tried to be quite efficient doing things like building
up a path to fields (i.e. reusing memory where possible, and not adding to the path unless a
constraint violation occurs, or there's no choice not to). Another is that without using reflection,
the checks just become simple if
statements and loops - these checks are extremely fast.
Another issue I found with reflection-based approaches is that you have to pass in references to fields to validate as strings (i.e. the name of the field), rather than the fields themselves. This is because you can't retrieve a field name as far as I can tell from a value passed in using reflection. The configuration for Valley needs to be able to compile as Go code. If it's mis-used, Valley will do it's best to tell you what's wrong, and where. References to fields should exist, and your existing tooling, and Go toolchain will tell you if they don't - as well as Valley. On top of that, the generated code also has to compile, further protecting you from runtime panics.
- Assess output of all constraints. Many constraints should probably be optional. Are they?
Constraints that probably should only apply if a value is set:
- DeepEquals
- Equals
- Length
- Max
- MaxLength
- Min
- MinLength
- NotEquals
- OneOf
- Regexp
- RegexpString
- TimeAfter
- TimeBefore
- TimeStringAfter
- TimeStringBefore
- Add some benchmarks to the README, preferably against something open source using reflection.
- The ability to define constraints in a separate file (in the same package, i.e. read the whole
package and generate code for the one file based on the context provided by the whole package).
- Maybe also the ability to define constraints in a function instead of on a method. Maybe also in a function in a separate package... More complex CLI usage there though.
- Better resolution of underlying types. Right now if a type is imported from any other file or
package than the one we're generating code for we can't tell what type it really is (e.g. is it a
struct, slice, map, int really?). If we could figure out those underlying types, the tool would be a
little more flexible. In particular,
Elements
andKeys
currently only work on plain collection types because that's the only way we can figure out the key / value type to pass to constraint generators. - The ability to attach multiple constraints methods to a type, that generate different validate
functions (the
Valid
constraint would need an option to override which method is called).
MIT
Feel free to open a pull request, or file an issue on GitHub. I always welcome contributions as long as they're for the benefit of all (potential) users of this project.
If you're unsure about anything, feel free to ask about it in an issue before you get your heart set on fixing it yourself.