GitHub - envoyproxy/protoc-gen-validate: protoc plugin to generate polyglot message validators

PGV可以指定语言生成校验,readme也给出了规则的写法

protoc --proto_path=./ --cpp_out=./rex_test ./test2.proto --validate_out="lang=cc:./"

proto文件需要import validate/validator.proto,才可以使用pgv的校验规则

使用pgv,可以在proto文件中对字段定义校验规则,他会对每个message对象都执行校验,在接口代码中,我们只需要include这个头文件,使用校验函数即可

syntax = "proto3";

package examplepb;

import "validate/validate.proto";

message Person {
  uint64 id    = 1 [(validate.rules).uint64.gt    = 999];

  string email = 2 [(validate.rules).string.email = true];

  string name  = 3 [(validate.rules).string = {
                      pattern:   "^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$",
                      max_bytes: 256,
                   }];

  Location home = 4 [(validate.rules).message.required = true];

  message Location {
    double lat = 1 [(validate.rules).double = { gte: -90,  lte: 90 }];
    double lng = 2 [(validate.rules).double = { gte: -180, lte: 180 }];
  }
}

上面这个proto文件,pgv生成的桩代码为

// Code generated by protoc-gen-validate
// source: test2.proto
// DO NOT EDIT!!!
#include "test2.pb.validate.h"
#include <google/protobuf/message.h>
#include <google/protobuf/util/time_util.h>
#include "re2/re2.h"
namespace pgv {
namespace protobuf = google::protobuf;
namespace protobuf_wkt = google::protobuf;
namespace validate {
using std::string;
// define the regex for a UUID once up-front
const re2::RE2 _uuidPattern(
    "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{"
    "12}$");
pgv::Validator<::examplepb::Person> validator___examplepb__Person(
    static_cast<bool (*)(const ::examplepb::Person&, pgv::ValidationMsg*)>(
        ::examplepb::Validate));
pgv::Validator<::examplepb::Person_Location>
    validator___examplepb__Person_Location(
        static_cast<bool (*)(const ::examplepb::Person_Location&,
                             pgv::ValidationMsg*)>(::examplepb::Validate));
}  // namespace validate
}  // namespace pgv
namespace examplepb {
// Validate checks the field values on ::examplepb::Person with the rules
// defined in the proto definition for this message. If any rules are
// violated, the return value is false and an error message is written to the
// input string argument.
const re2::RE2 _Person_Name_Pattern(
    re2::StringPiece("^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$",
                     sizeof("^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$") - 1));
bool Validate(const ::examplepb::Person& m, pgv::ValidationMsg* err) {
  (void)m;
  (void)err;
  if (m.id() <= 999) {
    {
      std::ostringstream msg("invalid ");
      msg << "PersonValidationError"
          << "."
          << "Id";
      msg << ": "
          << "value must be greater than 999";
      *err = msg.str();
      return false;
    }
  }
  throw pgv::UnimplementedException(
      "C++ email address validation is not implemented");
  if (m.name().size() > 256) {
    {
      std::ostringstream msg("invalid ");
      msg << "PersonValidationError"
          << "."
          << "Name";
      msg << ": "
          << "value length must be at most 256 bytes";
      *err = msg.str();
      return false;
    }
  }
  {
    if (!RE2::FullMatch(re2::StringPiece(m.name().c_str(), m.name().size()),
                        _Person_Name_Pattern)) {
      {
        std::ostringstream msg("invalid ");
        msg << "PersonValidationError"
            << "."
            << "Name";
        msg << ": "
            << "value does not match regex pattern \"^[^[0-9]A-Za-z]+( "
               "[^[0-9]A-Za-z]+)*$\"";
        *err = msg.str();
        return false;
      }
    }
  }
  if (!m.has_home()) {
    {
      std::ostringstream msg("invalid ");
      msg << "PersonValidationError"
          << "."
          << "Home";
      msg << ": "
          << "value is required";
      *err = msg.str();
      return false;
    }
  }
  {
    pgv::ValidationMsg inner_err;
    if (m.has_home() &&
        !pgv::BaseValidator::AbstractCheckMessage(m.home(), &inner_err)) {
      {
        std::ostringstream msg("invalid ");
        msg << "PersonValidationError"
            << "."
            << "Home";
        msg << ": "
            << "embedded message failed validation";
        msg << " | caused by " << inner_err;
        *err = msg.str();
        return false;
      }
    }
  }
  return true;
}
// Validate checks the field values on ::examplepb::Person_Location with the
// rules defined in the proto definition for this message. If any rules are
// violated, the return value is false and an error message is written to the
// input string argument.
bool Validate(const ::examplepb::Person_Location& m, pgv::ValidationMsg* err) {
  (void)m;
  (void)err;
  if (m.lat() < -90 || m.lat() > 90) {
    {
      std::ostringstream msg("invalid ");
      msg << "LocationValidationError"
          << "."
          << "Lat";
      msg << ": "
          << "value must be inside range [-90, 90]";
      *err = msg.str();
      return false;
    }
  }
  if (m.lng() < -180 || m.lng() > 180) {
    {
      std::ostringstream msg("invalid ");
      msg << "LocationValidationError"
          << "."
          << "Lng";
      msg << ": "
          << "value must be inside range [-180, 180]";
      *err = msg.str();
      return false;
    }
  }
  return true;
}
}  // namespace examplepb

生成的文件依赖有re2正则表达式库,以及依赖validate.h;前者基本上是内置的,后者是pgv生成的,所以编译时需要包含pgv源文件所在的validate目录,这个目录下才有这个头文件

上面的错误信息ValidationMsg,其实就是std::string,通过using声明的别名而已;

protoc-gen-validate/rule_comparison.md at main · envoyproxy/protoc-gen-validate · GitHub

pgv对于不同语言支持的情况

不同语言支持的规则特性不一样,所以上面有一个throw异常的情况,是因为c++没有实现这部分规则

go语言下的protoc编译命令,-I是告诉protoc,我们引用的validator.proto文件在哪里

protoc \
  -I . \
  -I ${GOPATH}/src \
  -I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate \
  --go_out=":../generated" \
  --validate_out="lang=go:../generated" \
  example.proto

安装方法

# fetches this repo into $GOPATH
go get -d github.com/envoyproxy/protoc-gen-validate
# installs PGV into $GOPATH/bin
make build

# 或者
git clone https://github.com/envoyproxy/protoc-gen-validate.git
cd protoc-gen-validate/
make build
# 源代码自带protoc-gen-go二进制文件
# 如果没有go环境
# make时可以临时export GOPATH=/your/dir/protoc-gen-validate/

可以在Makefile中生成

.PHONY: validate
# generate validate proto
validate:
    protoc --proto_path=. \
           --cpp_out=./rex_test
           --validate_out="lang=cc:./"
           $(API_PROTO_FILES)
           
    #protoc --proto_path=. \
    #       --proto_path=./third_party \
    #       --go_out=paths=source_relative:. \
    #       --validate_out=paths=source_relative,lang=go:. \
    #       $(API_PROTO_FILES)