protovalidate

package module
v0.6.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 16, 2024 License: Apache-2.0 Imports: 8 Imported by: 96

README

The Buf logo protovalidate-go

CI Conformance Report Card GoDoc BSR

protovalidate-go is the Go language implementation of protovalidate designed to validate Protobuf messages at runtime based on user-defined validation constraints. Powered by Google's Common Expression Language (CEL), it provides a flexible and efficient foundation for defining and evaluating custom validation rules. The primary goal of protovalidate is to help developers ensure data consistency and integrity across the network without requiring generated code.

The protovalidate project

Head over to the core protovalidate repository for:

Other protovalidate runtime implementations:

And others coming soon:

  • TypeScript: protovalidate-ts

For Connect see connectrpc/validate-go.

Installation

To install the package, use the go get command from within your Go module:

go get github.com/bufbuild/protovalidate-go

Import the package into your Go project:

import "github.com/bufbuild/protovalidate-go"

Remember to always check for the latest version of protovalidate-go on the project's GitHub releases page to ensure you're using the most up-to-date version.

Usage

Implementing validation constraints

Validation constraints are defined directly within .proto files. Documentation for adding constraints can be found in the protovalidate project README and its comprehensive docs.

syntax = "proto3";

package my.package;

import "google/protobuf/timestamp.proto";
import "buf/validate/validate.proto";

message Transaction {
  uint64 id = 1 [(buf.validate.field).uint64.gt = 999];
  google.protobuf.Timestamp purchase_date = 2;
  google.protobuf.Timestamp delivery_date = 3;
  
  string price = 4 [(buf.validate.field).cel = {
    id: "transaction.price",
    message: "price must be positive and include a valid currency symbol ($ or £)",
    expression: "(this.startsWith('$') || this.startsWith('£')) && double(this.substring(1)) > 0"
  }];

  option (buf.validate.message).cel = {
    id: "transaction.delivery_date",
    message: "delivery date must be after purchase date",
    expression: "this.delivery_date > this.purchase_date"
  };
}
Buf managed mode

protovalidate-go assumes the constraint extensions are imported into the generated code via buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go.

If you are using Buf managed mode to augment Go code generation, ensure that the protovalidate module is excluded in your buf.gen.yaml:

version: v1
# <snip>
managed:
  enabled: true
  go_package_prefix:
    except:
      - buf.build/bufbuild/protovalidate
# <snip>
Example
package main

import (
	"fmt"
	"time"
	
	pb "github.com/path/to/generated/protos"
	"github.com/bufbuild/protovalidate-go"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func main() {
	msg := &pb.Transaction{
		Id:           1234,
		Price:        "$5.67",
		PurchaseDate: timestamppb.New(time.Now()),
		DeliveryDate: timestamppb.New(time.Now().Add(time.Hour)),
	}

	v, err := protovalidate.New()
	if err != nil {
		fmt.Println("failed to initialize validator:", err)
	}

	if err = v.Validate(msg); err != nil {
		fmt.Println("validation failed:", err)
	} else {
		fmt.Println("validation succeeded")
	}
}
Lazy mode

protovalidate-go defaults to lazily construct validation logic for Protobuf message types the first time they are encountered. A validator's internal cache can be pre-warmed with the WithMessages or WithDescriptors options during initialization:

validator, err := protovalidate.New(
  protovalidate.WithMessages(
    &pb.MyFoo{}, 
    &pb.MyBar{}, 
  ),
)

Lazy mode uses a copy on write cache stategy to reduce the required locking. While performance is sub-microsecond, the overhead can be further reduced by disabling lazy mode with the WithDisableLazy option. Note that all expected messages must be provided during initialization of the validator:

validator, err := protovalidate.New(
  protovalidate.WithDisableLazy(true),
  protovalidate.WithMessages(
    &pb.MyFoo{},
    &pb.MyBar{},
  ),
)
Support legacy protoc-gen-validate constraints

The protovalidate-go module comes with a legacy package which adds opt-in support for existing protoc-gen-validate constraints. Provide thelegacy.WithLegacySupport option when initializing the validator:

validator, err := protovalidate.New(
  legacy.WithLegacySupport(legacy.ModeMerge),
)

protoc-gen-validate code generation is not used by protovalidate-go. The legacy package assumes the protoc-gen-validate extensions are imported into the generated code via github.com/envoyproxy/protoc-gen-validate/validate.

A migration tool is also available to incrementally upgrade legacy constraints in .proto files.

Performance

Benchmarks are provided to test a variety of use-cases. Generally, after the initial cold start, validation on a message is sub-microsecond and only allocates in the event of a validation error.

[circa 14 September 2023]
goos: darwin
goarch: arm64
pkg: github.com/bufbuild/protovalidate-go
BenchmarkValidator
BenchmarkValidator/ColdStart-10              4192  246278 ns/op  437698 B/op  5955 allocs/op
BenchmarkValidator/Lazy/Valid-10         11816635   95.08 ns/op       0 B/op     0 allocs/op
BenchmarkValidator/Lazy/Invalid-10        2983478   380.5 ns/op     649 B/op    15 allocs/op
BenchmarkValidator/Lazy/FailFast-10      12268683   98.22 ns/op     168 B/op     3 allocs/op
BenchmarkValidator/PreWarmed/Valid-10    12209587   90.36 ns/op       0 B/op     0 allocs/op
BenchmarkValidator/PreWarmed/Invalid-10   3098940   394.1 ns/op     649 B/op    15 allocs/op
BenchmarkValidator/PreWarmed/FailFast-10 12291523   99.27 ns/op     168 B/op     3 allocs/op
PASS

Ecosystem

Offered under the Apache 2 license.

Documentation

Overview

Example
validator, err := New()
if err != nil {
	log.Fatal(err)
}

person := &pb.Person{
	Id:    1234,
	Email: "protovalidate@buf.build",
	Name:  "Buf Build",
	Home: &pb.Coordinates{
		Lat: 27.380583333333334,
		Lng: 33.631838888888886,
	},
}

err = validator.Validate(person)
fmt.Println("valid:", err)

person.Email = "not an email"
err = validator.Validate(person)
fmt.Println("invalid:", err)
Output:

valid: <nil>
invalid: validation error:
 - email: value must be a valid email address [string.email]

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CompilationError

type CompilationError = errors.CompilationError

A CompilationError is returned if a CEL expression cannot be compiled & type-checked or if invalid standard constraints are applied to a field.

type RuntimeError

type RuntimeError = errors.RuntimeError

A RuntimeError is returned if a valid CEL expression evaluation is terminated, typically due to an unknown or mismatched type.

type StandardConstraintInterceptor

type StandardConstraintInterceptor func(res StandardConstraintResolver) StandardConstraintResolver

StandardConstraintInterceptor can be provided to WithStandardConstraintInterceptor to allow modifying a StandardConstraintResolver.

type StandardConstraintResolver

type StandardConstraintResolver interface {
	ResolveMessageConstraints(desc protoreflect.MessageDescriptor) *validate.MessageConstraints
	ResolveOneofConstraints(desc protoreflect.OneofDescriptor) *validate.OneofConstraints
	ResolveFieldConstraints(desc protoreflect.FieldDescriptor) *validate.FieldConstraints
}

StandardConstraintResolver is responsible for resolving the standard constraints from the provided protoreflect.Descriptor. The default resolver can be intercepted and modified using WithStandardConstraintInterceptor.

type ValidationError

type ValidationError = errors.ValidationError

A ValidationError is returned if one or more constraints on a message are violated. This error type can be converted into a validate.Violations message via ToProto.

err = validator.Validate(msg)
var valErr *ValidationError
if ok := errors.As(err, &valErr); ok {
  pb := valErr.ToProto()
  // ...
}
Example
validator, err := New()
if err != nil {
	log.Fatal(err)
}

loc := &pb.Coordinates{Lat: 999.999}
err = validator.Validate(loc)
var valErr *ValidationError
if ok := errors.As(err, &valErr); ok {
	msg := valErr.ToProto()
	fmt.Println(msg.GetViolations()[0].GetFieldPath(), msg.GetViolations()[0].GetConstraintId())
}
Output:

lat double.gte_lte

type Validator

type Validator struct {
	// contains filtered or unexported fields
}

Validator performs validation on any proto.Message values. The Validator is safe for concurrent use.

func New

func New(options ...ValidatorOption) (*Validator, error)

New creates a Validator with the given options. An error may occur in setting up the CEL execution environment if the configuration is invalid. See the individual ValidatorOption for how they impact the fallibility of New.

func (*Validator) Validate

func (v *Validator) Validate(msg proto.Message) error

Validate checks that message satisfies its constraints. Constraints are defined within the Protobuf file as options from the buf.validate package. An error is returned if the constraints are violated (ValidationError), the evaluation logic for the message cannot be built (CompilationError), or there is a type error when attempting to evaluate a CEL expression associated with the message (RuntimeError).

type ValidatorOption

type ValidatorOption func(*config)

A ValidatorOption modifies the default configuration of a Validator. See the individual options for their defaults and affects on the fallibility of configuring a Validator.

func WithDescriptors

func WithDescriptors(descriptors ...protoreflect.MessageDescriptor) ValidatorOption

WithDescriptors allows warming up the Validator with message descriptors that are expected to be validated. Messages included transitively (i.e., fields with message values) are automatically handled.

Example
pbType, err := protoregistry.GlobalTypes.FindMessageByName("tests.example.v1.Person")
if err != nil {
	log.Fatal(err)
}

validator, err := New(
	WithDescriptors(
		pbType.Descriptor(),
	),
)
if err != nil {
	log.Fatal(err)
}

person := &pb.Person{
	Id:    1234,
	Email: "protovalidate@buf.build",
	Name:  "Protocol Buffer",
}
err = validator.Validate(person)
fmt.Println(err)
Output:

<nil>

func WithDisableLazy

func WithDisableLazy(disable bool) ValidatorOption

WithDisableLazy prevents the Validator from lazily building validation logic for a message it has not encountered before. Disabling lazy logic additionally eliminates any internal locking as the validator becomes read-only.

Note: All expected messages must be provided by WithMessages or WithDescriptors during initialization.

Example
person := &pb.Person{
	Id:    1234,
	Email: "protovalidate@buf.build",
	Name:  "Buf Build",
	Home: &pb.Coordinates{
		Lat: 27.380583333333334,
		Lng: 33.631838888888886,
	},
}

validator, err := New(
	WithMessages(&pb.Coordinates{}),
	WithDisableLazy(true),
)
if err != nil {
	log.Fatal(err)
}

err = validator.Validate(person.GetHome())
fmt.Println("person.Home:", err)
err = validator.Validate(person)
fmt.Println("person:", err)
Output:

person.Home: <nil>
person: compilation error: no evaluator available for tests.example.v1.Person

func WithFailFast

func WithFailFast(failFast bool) ValidatorOption

WithFailFast specifies whether validation should fail on the first constraint violation encountered or if all violations should be accumulated. By default, all violations are accumulated.

Example
loc := &pb.Coordinates{Lat: 999.999, Lng: -999.999}

validator, err := New()
if err != nil {
	log.Fatal(err)
}
err = validator.Validate(loc)
fmt.Println("default:", err)

validator, err = New(WithFailFast(true))
if err != nil {
	log.Fatal(err)
}
err = validator.Validate(loc)
fmt.Println("fail fast:", err)
Output:

default: validation error:
 - lat: value must be greater than or equal to -90 and less than or equal to 90 [double.gte_lte]
 - lng: value must be greater than or equal to -180 and less than or equal to 180 [double.gte_lte]
fail fast: validation error:
 - lat: value must be greater than or equal to -90 and less than or equal to 90 [double.gte_lte]

func WithMessages

func WithMessages(messages ...proto.Message) ValidatorOption

WithMessages allows warming up the Validator with messages that are expected to be validated. Messages included transitively (i.e., fields with message values) are automatically handled.

Example
validator, err := New(
	WithMessages(&pb.Person{}),
)
if err != nil {
	log.Fatal(err)
}

person := &pb.Person{
	Id:    1234,
	Email: "protovalidate@buf.build",
	Name:  "Protocol Buffer",
}
err = validator.Validate(person)
fmt.Println(err)
Output:

<nil>

func WithStandardConstraintInterceptor

func WithStandardConstraintInterceptor(interceptor StandardConstraintInterceptor) ValidatorOption

WithStandardConstraintInterceptor allows intercepting the StandardConstraintResolver used by the Validator to modify or replace it.

func WithUTC

func WithUTC(useUTC bool) ValidatorOption

WithUTC specifies whether timestamp operations should use UTC or the OS's local timezone for timestamp related values. By default, the local timezone is used.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL