thrift-gen-validator is a thriftgo plugin to generate struct validators.
Users can define validation rule for struct-like(struct/union/exception) in Thrift file, then the generator will generate IsValid() error
method for those structs.
for example:
enum MapKey {
A, B, C, D, E, F
}
struct Example {
1: string Message (vt.min_size = "30") // length of Message should be greater than or equal to 30
2: i32 ID (vt.ge = "10000") // ID must be greater than or euqal to 10000
3: list<double> Values (vt.elem.gt = "0.25") // element of Values must be greater than 0.25
4: map<MapKey, string> KeyValues (vt.key.defined_only = "true") // value of KeyValues'key must be defined in MapKey
}
generated method:
func (p *Example) IsValid() error {
if len(p.Message) < int(30) {
return fmt.Errorf("field Message min_len rule failed, current value: %d", len(p.Message))
}
if p.ID < int32(10000) {
return fmt.Errorf("field ID ge rule failed, current value: %v", p.ID)
}
for i := 0; i < len(p.Values); i++ {
_elem := p.Values[i]
if _elem <= float64(0.25) {
return fmt.Errorf("field _elem gt rule failed, current value: %v", _elem)
}
}
for k := range p.KeyValues {
if k.String() == "<UNSET>" {
return fmt.Errorf("field k defined_only rule failed")
}
}
return nil
}
go install github.com/cloudwego/thrift-gen-validator@latest
thriftgo -g go -p validator my.thrift
kitex --thrift-plugin validator -service a.b.c my.thrift
prefix vt
, short for "validation"
Rule | |
---|---|
vt.const | must be specified value |
vt.lt | less than the specified value |
vt.le | less than or equal to specified value |
vt.gt | greater than the specified value |
vt.ge | greater than or equal to specified value |
vt.in | must be in specified values |
vt.not_in | must not be in specified values |
Rule | |
---|---|
vt.const | must be specified value |
Rule | |
---|---|
vt.const | must be specified value |
vt.pattern | regexp pattern |
vt.prefix | prefix must be specified value |
vt.suffix | suffix must be specified value |
vt.contains | must contain specified value |
vt.not_contains | must not contain specified value |
vt.min_size | min size |
vt.max_size | max size |
Rule | |
---|---|
vt.const | must be specified value |
vt.defined_only | must be defined value |
Rule | |
---|---|
vt.min_size | min size |
vt.max_size | max size |
vt.elem | rule for list element |
Rule | |
---|---|
vt.min_size | min size |
vt.max_size | max size |
vt.key | rule for map key |
vt.value | rule for map value |
vt.no_sparse | map value must be non-nil pointer |
Rule | |
---|---|
vt.skip | skip struct recursive validation |
Rule | |
---|---|
vt.assert | expression should be true |
- Field Reference. We can use another field as a validation value.
- Validation Function. We can use those functions to provide extensive validation ability.
struct Example {
1: string StringFoo (vt.max_size = "$MaxStringSize")
2: i32 MaxStringSize
}
struct Example {
1: string MaxString
2: list<string> StringList (vt.elem.max_size = "@len($MaxString)")
}
function name | arguments | results | remarks |
---|---|---|---|
len | 1: container field | 1: length of container (integer) | just like len of go |
sprintf | 1: format string 2+: arguments matching format |
1: formatted string (string) | just like fmt.Sprintf of go |
now_unix_nano | none | 1: nano seconds (int64) | just like time.Now().UnixNano() of go |
equal | 1, 2: comparable values | 1: whether two arguments is equal (bool) | just like == of go |
mod | 1, 2: integer | 1: remainder of $1 / $2 (integer) | just like % of go |
add | 1, 2: both are numeric or string | 1: sum of two arguments (integer or float64 or string) | just like + of go |
Now you can use parameter func
to customize your validation function. Like below:
thriftgo -g go -p validator:func=my_func=path_to_template.txt my.thrift
my_func
is the function name, path_to_template.txt
is the path to template file which should be a go template.
Available template variables:
variable name | meaning | type |
---|---|---|
Source | variable name that rule will refer to | string |
StructLike | ast of current struct/union/exception | *"github.com/cloudwego/thriftgo/generator/golang".StructLike |
Function | data of current function | *"github.com/cloudwego/thrift-gen-validator/parser".ToolFunction |
package main
import (
"context"
"fmt"
"log"
"github.com/cloudwego/kitex-examples/kitex_gen/api"
"github.com/cloudwego/kitex-examples/kitex_gen/api/echo"
"github.com/cloudwego/kitex/client"
"github.com/cloudwego/kitex/pkg/endpoint"
"github.com/cloudwego/kitex/server"
)
func ValidatorMW(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, args, result interface{}) (err error) {
if gfa, ok := args.(interface{ GetFirstArgument() interface{} }); ok {
req := gfa.GetFirstArgument()
if rv, ok := req.(interface{ IsValid() error }); ok {
if err := rv.IsValid(); err != nil {
return fmt.Errorf("request data is not valid:%w", err)
}
}
}
err = next(ctx, args, result)
if err != nil {
return err
}
if gr, ok := result.(interface{ GetResult() interface{} }); ok {
resp := gr.GetResult()
if rv, ok := resp.(interface{ IsValid() error }); ok {
if err := rv.IsValid(); err != nil {
return fmt.Errorf("response data is not valid:%w", err)
}
}
}
return nil
}
}
// for client
func main() {
cli := echo.MustNewClient("service_name", client.WithMiddleware(ValidatorMW))
resp, err := client.Echo(context.Background(), &api.Request{Message: "my request"})
if err != nil {
log.Println(err.Error())
} else {
log.Println(resp)
}
}
// for server
func main() {
svr := echo.NewServer(new(EchoImpl), server.WithMiddleware(ValidatorMW))
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}
If we have a my.thrift
like below:
struct Example {
1: string Message (vt.max_size = "@my_length()")
}
And assumes that we want to limit the max length of Message
to 10, we can write a template file my_length.txt
like below:
{{- .Source}} := 10 /*my length*/
Then we can use command below to generate a validator file:
thriftgo -g go -p validator:func=my_length=my_length.txt my.thrift
We will get a IsValid() error
like below:
func (p *ValidatorExample) IsValid() error {
_src := 10 /*my length*/
if len(p.Message) > int(_src) {
return fmt.Errorf("field Message max_len rule failed, current value: %d", len(p.Message))
}
return nil
}
{{.Source}}
indicates _src
which will be used in if len(p.Message) > int(_src) {
, so all the thing the function template need to do is assign a value to _src
aka {{.Source}}
. In the above example, {{- .Source}} := + 10 /*my length*/
will do.
Now let's see a more complex example. Assumes that we have a my.thrift
like below:
struct Example {
1: string Message (vt.max_size = "@fix_length($MaxLength)")
2: i64 MaxLength
}
And assumes that we want to limit the max length of Message
to the sum of MaxLength and 10, we can write a template file fix_length.txt
like below:
{{- $arg0 := index .Function.Arguments 0}}
{{- $reference := $arg0.TypedValue.GetFieldReferenceName "p." .StructLike}}
{{- .Source}} := {{$reference}} + 10 /*length fix*/
Then we can use command below to generate a validator file:
thriftgo -g go -p validator:func=fix_length=fix_length.txt my.thrift
We will get a IsValid() error
like below:
func (p *ValidatorExample) IsValid() error {
_src := p.MaxLength + 10 /*length fix*/
if len(p.Message) > int(_src) {
return fmt.Errorf("field Message max_len rule failed, current value: %d", len(p.Message))
}
return nil
}
{{$arg0 := index .Function.Arguments 0}}
is used to get the first argument of the function. {{$reference := $arg0.TypedValue.GetFieldReferenceName "p." .StructLike}}
is used to get the reference name of the first argument, for there p.MaxLength
.
In some scenarios, we might want to import some extra packages, for example, if we want to get some enviroment variables, we need to import os
package which is not in the default import list. In this case, we can add following statement to function template file:
{{define "Import"}}
"os"
{{end}}
{{define "ImportGuard"}}
_ = os.Getenv
{{end}}
Then we can get a validator go file header like below:
import (
"bytes"
"fmt"
"os"
"reflect"
"regexp"
"strings"
"time"
)
// unused protection
var (
_ = fmt.Formatter(nil)
_ = (*bytes.Buffer)(nil)
_ = (*strings.Builder)(nil)
_ = reflect.Type(nil)
_ = (*regexp.Regexp)(nil)
_ = time.Nanosecond
_ = os.Getenv
)
You can go to examples/custom-function to see the complete example. And you can view the generated code in examples/custom-function/gen-go/my.