Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/validation with fieldmasks #57

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ Session.vim
.Trash-*

# Created by .ignore support plugin (hsz.mobi)

# Test Coverage files
cover.*
31 changes: 21 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,29 @@

export PATH := ${GOPATH}/bin:${PATH}

init:
go get -u "github.com/gogo/protobuf/protoc-gen-gogo"

install:
@echo "--- Installing govalidators to GOPATH"
go install github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
go install github.com/TheThingsIndustries/go-proto-validators/protoc-gen-govalidators

regenerate_test_gogo:
@echo "Regenerating test .proto files with gogo imports"

regenerate_test_generic: install
@echo "Regenerating the generic test .proto files (with gogo)"
(protoc \
--proto_path=${GOPATH}/src \
--proto_path=test \
--gogo_out=test/gogo \
--govalidators_out=gogoimport=true:test/gogo test/*.proto)
--gogo_out=test \
--govalidators_out=gogoimport=true:test test/generic/*.proto)

regenerate_test_golang:
@echo "--- Regenerating test .proto files with golang imports"
regenerate_test_gogo: install
@echo "Regenerating test .proto files with gogo imports"
(protoc \
--proto_path=${GOPATH}/src \
--proto_path=test \
--go_out=test/golang \
--govalidators_out=test/golang test/*.proto)
--gogo_out=test/gogo \
--govalidators_out=gogoimport=true:test/gogo test/*.proto)

regenerate_example: install
@echo "--- Regenerating example directory"
Expand All @@ -31,7 +35,14 @@ regenerate_example: install
--go_out=. \
--govalidators_out=. examples/*.proto)

test: install regenerate_test_gogo regenerate_test_golang
errors: install
@echo "--- Regenerating error definitions"
(protoc \
--proto_path=${GOPATH}/src \
--proto_path=errors \
--go_out=errors errors/*.proto)

test: regenerate_test_generic regenerate_test_gogo
@echo "Running tests"
go test -v ./...

Expand Down
74 changes: 30 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
[![Travis Build](https://travis-ci.org/mwitkow/go-proto-validators.svg)](https://travis-ci.org/mwitkow/go-proto-validators)
[![Apache 2.0 License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)

A `protoc` plugin that generates `Validate() error` functions on Go proto `struct`s based on field options inside `.proto`
files. The validation functions are code-generated and thus don't suffer on performance from tag-based reflection on
deeply-nested messages.
A `protoc` plugin that generates `Validate([]paths) error` functions on Go proto `struct`s based on field options inside `.proto`
files.

It incorporates [fieldmasks](https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/field-mask) into the validators thereby providing optional validation on fields. By default, no fields are validated. This allows for each field that needs to be validated to be specified by the fieldmask.

## Paint me a code picture

Expand Down Expand Up @@ -38,34 +39,40 @@ Second, the expected values in fields are now part of the contract `.proto` file
Third, the generated code is understandable and has clear understandable error messages. Take a look:

```go
func (this *InnerMessage) Validate() error {
if !(this.SomeInteger > 0) {
return fmt.Errorf("validation error: InnerMessage.SomeInteger must be greater than '0'")
}
if !(this.SomeInteger < 100) {
return fmt.Errorf("validation error: InnerMessage.SomeInteger must be less than '100'")
func (this *InnerMessage) Validate(paths []string) error {
toBeValidated, err := github_com_TheThingsIndustries_go_proto_validators_util.GetFieldsToValidate(this, paths)
if err != nil {
return err
}
if !(this.SomeFloat >= 0) {
return fmt.Errorf("validation error: InnerMessage.SomeFloat must be greater than or equal to '0'")
_ = toBeValidated

if !(this.SomeInteger > 0) && (github_com_TheThingsIndustries_go_proto_validators_util.ShouldBeValidated("this.SomeInteger", toBeValidated)) {
return github_com_TheThingsIndustries_go_proto_validators_errors.FieldError(github_com_TheThingsIndustries_go_proto_validators_util.GetProtoNameForField("SomeInteger", toBeValidated), github_com_TheThingsIndustries_go_proto_validators_errors.Types_INT_GT, fmt.Errorf(`field must be greater than '0'`))
}
if !(this.SomeFloat <= 1) {
return fmt.Errorf("validation error: InnerMessage.SomeFloat must be less than or equal to '1'")
if !(this.SomeInteger < 100) && (github_com_TheThingsIndustries_go_proto_validators_util.ShouldBeValidated("this.SomeInteger", toBeValidated)) {
return github_com_TheThingsIndustries_go_proto_validators_errors.FieldError(github_com_TheThingsIndustries_go_proto_validators_util.GetProtoNameForField("SomeInteger", toBeValidated), github_com_TheThingsIndustries_go_proto_validators_errors.Types_INT_LT, fmt.Errorf(`field must be lesser than '100'`))
}
return nil
}

var _regex_OuterMessage_ImportantString = regexp.MustCompile("^[a-z]{2,5}$")
var _regex_OuterMessage_ImportantString = regexp.MustCompile(`^[a-z]{2,5}$`)

func (this *OuterMessage) Validate(paths []string) error {
toBeValidated, err := github_com_TheThingsIndustries_go_proto_validators_util.GetFieldsToValidate(this, paths)
if err != nil {
return err
}
_ = toBeValidated

func (this *OuterMessage) Validate() error {
if !_regex_OuterMessage_ImportantString.MatchString(this.ImportantString) {
return fmt.Errorf("validation error: OuterMessage.ImportantString must conform to regex '^[a-z]{2,5}$'")
if !_regex_OuterMessage_ImportantString.MatchString(this.ImportantString) && (github_com_TheThingsIndustries_go_proto_validators_util.ShouldBeValidated("this.ImportantString", toBeValidated)) {
return github_com_TheThingsIndustries_go_proto_validators_errors.FieldError(github_com_TheThingsIndustries_go_proto_validators_util.GetProtoNameForField("ImportantString", toBeValidated), github_com_TheThingsIndustries_go_proto_validators_errors.Types_STRING_REGEX, fmt.Errorf(`field must be a string conforming to the regex "^[a-z]{2,5}$"`))
}
if nil == this.Inner {
return fmt.Errorf("validation error: OuterMessage.Inner message must exist")
return github_com_TheThingsIndustries_go_proto_validators_errors.FieldError(github_com_TheThingsIndustries_go_proto_validators_util.GetProtoNameForField("Inner", toBeValidated), github_com_TheThingsIndustries_go_proto_validators_errors.Types_MSG_EXISTS, fmt.Errorf("message must exist"))
}
if this.Inner != nil {
if err := validators.CallValidatorIfExists(this.Inner); err != nil {
return err
if (this.Inner != nil) && (github_com_TheThingsIndustries_go_proto_validators_util.ShouldBeValidated("this.Inner", toBeValidated)) {
if err := github_com_TheThingsIndustries_go_proto_validators_util.CallValidatorIfExists(this.Inner, github_com_TheThingsIndustries_go_proto_validators_util.GetProtoNameForField("this.Inner", toBeValidated), paths); err != nil {
return github_com_TheThingsIndustries_go_proto_validators_errors.GetErrorWithTopField(github_com_TheThingsIndustries_go_proto_validators_util.GetProtoNameForField("this.Inner", toBeValidated), err)
}
}
return nil
Expand All @@ -86,30 +93,9 @@ Then, do the usual
go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
```

Your `protoc` builds probably look very simple like:

```sh
protoc \
--proto_path=. \
--go_out=. \
*.proto
```

That's fine, until you encounter `.proto` includes. Because `go-proto-validators` uses field options inside the `.proto`
files themselves, it's `.proto` definition (and the Google `descriptor.proto` itself) need to on the `protoc` include
path. Hence the above becomes:

```sh
protoc \
--proto_path=${GOPATH}/src \
--proto_path=${GOPATH}/src/github.com/google/protobuf/src \
--proto_path=. \
--go_out=. \
--govalidators_out=. \
*.proto
```
Check the [Makefile](/Makefile) for installing this plugin.

Or with gogo protobufs:
The following is an example of using this plugin as part of your proto generation.

```sh
protoc \
Expand Down
59 changes: 59 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package errors

import (
"fmt"
"strings"
)

// ValidatorFieldError is a generic struct that can be used for better error usage in tests and in code.
type ValidatorFieldError struct {
nestedErr error
fieldName string
errType Types
}

// Error returns the error as a string
func (f *ValidatorFieldError) Error() string {
return fmt.Sprintf("%s: %s: %s", f.fieldName, f.errType.String(), f.nestedErr.Error())
}

// GetFieldName extracts the field name from the error message.
func GetFieldName(err string) string {
s := strings.Split(err, ": ")
if len(s) != 3 {
return ""
}
return s[0]
}

// GetType extracts the errors.Types name from the error message.
func GetType(err string) string {
s := strings.Split(err, ": ")
if len(s) != 3 {
return ""
}
return s[1]
}

// GetErrorDescripton extracts the error stack from the error message.
func GetErrorDescripton(err string) string {
s := strings.Split(err, ": ")
if len(s) != 3 {
return ""
}
return s[2]
}

// GetErrorWithTopField ...
func GetErrorWithTopField(name string, err error) error {
return fmt.Errorf(fmt.Sprintf("%s.%s", name, err.Error()))
}

// FieldError wraps a given Validator error providing a message call stack.
func FieldError(fieldName string, Type Types, err error) error {
return &ValidatorFieldError{
nestedErr: err,
fieldName: fieldName,
errType: Type,
}
}
47 changes: 47 additions & 0 deletions errors/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package errors

import (
"errors"
"reflect"
"testing"
)

func TestErrorMethods(t *testing.T) {
errString := "SomeInt: INT_GT: field must be greater than '500'"

errType := GetType(errString)
if errType != "INT_GT" {
t.Fatal(t)
}

errFieldName := GetFieldName(errString)
if errFieldName != "SomeInt" {
t.Fatal(t)
}

if errFieldName := GetFieldName("errString"); errFieldName != "" {
t.Fatal(errFieldName)
}
if errDesc := GetErrorDescripton("errString:Type"); errDesc != "" {
t.Fatal(errDesc)
}

errDesc := GetErrorDescripton(errString)
if errDesc != "field must be greater than '500'" {
t.Fatal(t)
}

nilErrType := GetType("")
if nilErrType != "" {
t.Fatal(t)
}

if err := GetErrorWithTopField("top_field", errors.New("inner_field: INT_GT: field must be greater than '500'")); err.Error() != "top_field.inner_field: INT_GT: field must be greater than '500'" {
t.Fatal(err)
}

if err := FieldError("top_field", Types_INT_GT, errors.New("field must be greater than '500'")); reflect.DeepEqual(err, ValidatorFieldError{nestedErr: errors.New("field must be greater than '500'"), fieldName: "top_field", errType: Types_INT_GT}) {
t.Fatal(err)
}

}
Loading