Home

Awesome

thrift-gen-validator

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
}

Install

go install github.com/cloudwego/thrift-gen-validator@latest

Usage

Thriftgo

thriftgo -g go -p validator my.thrift

Kitex

kitex --thrift-plugin validator -service a.b.c my.thrift

Feature Matrix

prefix vt, short for "validation"

Numeric(i8/i16/i32/i64/double)

Rule
vt.constmust be specified value
vt.ltless than the specified value
vt.leless than or equal to specified value
vt.gtgreater than the specified value
vt.gegreater than or equal to specified value
vt.inmust be in specified values
vt.not_inmust not be in specified values

Bool

Rule
vt.constmust be specified value

String/Binary

Rule
vt.constmust be specified value
vt.patternregexp pattern
vt.prefixprefix must be specified value
vt.suffixsuffix must be specified value
vt.containsmust contain specified value
vt.not_containsmust not contain specified value
vt.min_sizemin size
vt.max_sizemax size

Enum

Rule
vt.constmust be specified value
vt.defined_onlymust be defined value

Set/List

Rule
vt.min_sizemin size
vt.max_sizemax size
vt.elemrule for list element

Map

Rule
vt.min_sizemin size
vt.max_sizemax size
vt.keyrule for map key
vt.valuerule for map value
vt.no_sparsemap value must be non-nil pointer

Struct Field

Rule
vt.skipskip struct recursive validation

Struct

Rule
vt.assertexpression should be true

Special Value

  1. Field Reference. We can use another field as a validation value.
  2. Validation Function. We can use those functions to provide extensive validation ability.

Field Reference Example

struct Example {
    1: string StringFoo (vt.max_size = "$MaxStringSize")
    2: i32 MaxStringSize
}

Validation Function

struct Example {
    1: string MaxString
    2: list<string> StringList (vt.elem.max_size = "@len($MaxString)")
}
function nameargumentsresultsremarks
len1: container field1: length of container (integer)just like len of go
sprintf1: format string <br /> 2+: arguments matching format1: formatted string (string)just like fmt.Sprintf of go
now_unix_nanonone1: nano seconds (int64)just like time.Now().UnixNano() of go
equal1, 2: comparable values1: whether two arguments is equal (bool)just like == of go
mod1, 2: integer1: remainder of $1 / $2 (integer)just like % of go
add1, 2: both are numeric or string1: sum of two arguments (integer or float64 or string)just like + of go

Customized Validation Function

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 namemeaningtype
Sourcevariable name that rule will refer tostring
StructLikeast of current struct/union/exception*"github.com/cloudwego/thriftgo/generator/golang".StructLike
Functiondata of current function*"github.com/cloudwego/thrift-gen-validator/parser".ToolFunction

Example

Kitex Middleware Example

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())
	}
}

Customize Validation Function Example

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.