Добавить проверку параметров в proto

protobuf

Сцена приложения

Интерфейс прототипа, предоставленный извне, требует проверки параметров. Прошлая практика заключается в добавлении правил проверки в логику кода, и появится следующий пример:

if req.Caller == nil || equip == nil || req.Action == coll_common.Action_OP_UNKNOWN || len(equip.Name) == 0 || equip.CategoryId == 0 ||
   len(equip.LogoList) == 0 || len(equip.Model) == 0 || len(equip.TechnicalIndex) == 0 || len(equip.Description) == 0 ||
   equip.RentCount == 0 || equip.MinimumRental == 0 || equip.Rent == 0 || equip.ContactPerson == nil || equip.Location == nil {
   logrus.Errorf("[AddEquipment] required parameter can not be null")
   rsp.CommonRsp.Code = common.CodeInvalidParam
   rsp.CommonRsp.Msg = "required parameter can not be null"
   return nil
}

Код, написанный таким образом, менее читабелен, не может подробно обрабатывать каждую ошибку и возвращает унифицированный результат ошибки, его трудно устранить. Если обрабатывается каждое сообщение об ошибке, это занимает много времени, и есть много повторяющихся задач. Это проще. чтобы получить ошибки. Из этого я подумал, можно ли ограничить тип данных при определении proto, и привязать метод проверки к запрашиваемой информации через компиляцию, что может улучшить читаемость кода и уменьшить много повторяющейся работы. .

Сравнение инструментов go-proto-validators и protoc-gen-validate

Имя | участники | звезды | язык | в настоящее время | документация завершена :------------ | :------------: | ------------: go-proto-validators | 14 | 605 | go | стабильный | низкий protoc-gen-validate | 56 | 982 | go, java, python, c++ | альфа | высокий

protoc-gen-validate: Он поддерживает несколько языков, а активность сообщества немного выше, чем у go-proto-validators.Документация относительно полная, но она все еще находится в нестабильной версии, и последующие API, вероятно, изменятся.go-proto-validate: Поддерживает различные варианты использования и принципы, по сути мало чем отличается от protoc-gen-validate, но находится в относительно стабильной версии, поэтому это решение временно принято.

использовать

1. Добавьте каталог исполняемого файла под gopath в переменную среды.

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

2. Установка

go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators

3. Написать протобуф

syntax = "proto3";
package test_bid;

import "github.com/mwitkow/go-proto-validators/validator.proto";
import "git.code.oa.com/cloud_industry/proto/common/common.proto";
import "coll_common/common.proto";
import "google/protobuf/descriptor.proto";


option go_package = "git.code.oa.com/cloud_industry/colla_proto/test_bid";

// 投标管理
service BidManagement {
    //添加投标
    rpc DoBid (DoBidReq) returns (DoBidRsp) {
    }
}

enum Action {
    OP_UNKNOWN = 0; //未知
    OP_DRAFT = 1; //保存草稿
    OP_COMMIT = 2; //提交审核
    OP_REVOKE = 3; //撤销审核
    OP_PASS = 4; //通过
    OP_DENY = 5; //驳回
    OP_RELEASE = 6; //上架
    OP_OFF_RELEASE = 7; //下架
    OP_RECOMMEND = 8; //推荐
    OP_CANCEL_RECOMMEND = 9; //取消推荐
    OP_MOVE_UP = 10; //上移
    OP_MOVE_DOWN = 11; //下移
    OP_CLOSE = 12; //关闭
    OP_SUCC = 13; //交易成功
    OP_FAIL = 14; //交易失败
}

message DoBidReq {
    common.Caller Caller = 1 [(validator.field) = {msg_exists: true}];
    int64 DemandId = 2 [(validator.field) = {int_lt: 0}]; // 需求id
    int64 ServicePrice = 3; // 交付报价,单位分
    int32 ServicePeriod = 4; // 交付周期,单位天
    common.AppendixInfo Attachment = 5 [(validator.field) = {msg_exists: true}]; // 附件
    string Connect = 6 [(validator.field) = {string_not_empty: true}]; // 联系人
    bytes ConnectPhone = 7 [(validator.field) = {length_gt: 0, length_lt: 255}]; // 联系手机
    string SMSVerifyCode = 8; // 短信验证码
    Action Action = 9 [(validator.field) = {is_in_enum: true}]; // 操作
}

message DoBidRsp {
    common.CommonRsp CommonRsp = 1;
}

Соответствующую поддержку синтаксиса можно посмотреть в файле validator.proto.

// Copyright 2016 Michal Witkowski. All Rights Reserved.
// See LICENSE for licensing terms.

// Protocol Buffers extensions for defining auto-generateable validators for messages.

// TODO(mwitkow): Add example.


syntax = "proto2";
package validator;

import "google/protobuf/descriptor.proto";

option go_package = "github.com/mwitkow/go-proto-validators;validator";

// TODO(mwitkow): Email protobuf-global-extension-registry@google.com to get an extension ID.

extend google.protobuf.FieldOptions {
  optional FieldValidator field = 65020;
}

extend google.protobuf.OneofOptions {
  optional OneofValidator oneof = 65021;
}

message FieldValidator {
  // Uses a Golang RE2-syntax regex to match the field contents.
  optional string regex = 1;
  // Field value of integer strictly greater than this value.
  optional int64 int_gt = 2;
  // Field value of integer strictly smaller than this value.
  optional int64 int_lt = 3;
  // Used for nested message types, requires that the message type exists.
  optional bool msg_exists = 4;
  // Human error specifies a user-customizable error that is visible to the user.
  optional string human_error = 5;
  // Field value of double strictly greater than this value.
  // Note that this value can only take on a valid floating point
  // value. Use together with float_epsilon if you need something more specific.
  optional double float_gt = 6;
  // Field value of double strictly smaller than this value.
  // Note that this value can only take on a valid floating point
  // value. Use together with float_epsilon if you need something more specific.
  optional double float_lt = 7;
  // Field value of double describing the epsilon within which
  // any comparison should be considered to be true. For example,
  // when using float_gt = 0.35, using a float_epsilon of 0.05
  // would mean that any value above 0.30 is acceptable. It can be
  // thought of as a {float_value_condition} +- {float_epsilon}.
  // If unset, no correction for floating point inaccuracies in
  // comparisons will be attempted.
  optional double float_epsilon = 8;
  // Floating-point value compared to which the field content should be greater or equal.
  optional double float_gte = 9;
  // Floating-point value compared to which the field content should be smaller or equal.
  optional double float_lte = 10;
  // Used for string fields, requires the string to be not empty (i.e different from "").
  optional bool string_not_empty = 11;
  // Repeated field with at least this number of elements.
  optional int64 repeated_count_min = 12;
  // Repeated field with at most this number of elements.
  optional int64 repeated_count_max = 13;
  // Field value of length greater than this value.
  optional int64 length_gt = 14;
  // Field value of length smaller than this value.
  optional int64 length_lt = 15;
  // Field value of length strictly equal to this value.
  optional int64 length_eq = 16;
  // Requires that the value is in the enum.
  optional bool is_in_enum = 17;
  // Ensures that a string value is in UUID format.
  // uuid_ver specifies the valid UUID versions. Valid values are: 0-5.
  // If uuid_ver is 0 all UUID versions are accepted.
  optional int32 uuid_ver = 18;
}

message OneofValidator {
  // Require that one of the oneof fields is set.
  optional bool required = 1;
}

4. Скомпилируйте

Добавьте еще один параметр govalidators_out в синтаксис компиляции protoc.

protoc  \
  --proto_path=${GOPATH}/src \
  --proto_path=${GOPATH}/src/github.com/google/protobuf/src \
  --proto_path=. \
  --go_out=. \
  --govalidators_out=. \
  *.proto

После выполнения будет сгенерирован файл test_bid.validator.pb.go

// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: test_bid/test_bid.proto

package test_bid

import (
   fmt "fmt"
   math "math"
   proto "github.com/golang/protobuf/proto"
   _ "github.com/mwitkow/go-proto-validators"
   _ "git.code.oa.com/cloud_industry/proto/common"
   _ "git.code.oa.com/cloud_industry/colla_proto/coll_common"
   github_com_mwitkow_go_proto_validators "github.com/mwitkow/go-proto-validators"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

func (this *DoBidReq) Validate() error {
   if nil == this.Caller {
      return github_com_mwitkow_go_proto_validators.FieldError("Caller", fmt.Errorf("message must exist"))
   }
   if this.Caller != nil {
      if err := github_com_mwitkow_go_proto_validators.CallValidatorIfExists(this.Caller); err != nil {
         return github_com_mwitkow_go_proto_validators.FieldError("Caller", err)
      }
   }
   if !(this.DemandId < 0) {
      return github_com_mwitkow_go_proto_validators.FieldError("DemandId", fmt.Errorf(`value '%v' must be less than '0'`, this.DemandId))
   }
   if nil == this.Attachment {
      return github_com_mwitkow_go_proto_validators.FieldError("Attachment", fmt.Errorf("message must exist"))
   }
   if this.Attachment != nil {
      if err := github_com_mwitkow_go_proto_validators.CallValidatorIfExists(this.Attachment); err != nil {
         return github_com_mwitkow_go_proto_validators.FieldError("Attachment", err)
      }
   }
   if this.Connect == "" {
      return github_com_mwitkow_go_proto_validators.FieldError("Connect", fmt.Errorf(`value '%v' must not be an empty string`, this.Connect))
   }
   if !(len(this.ConnectPhone) > 0) {
      return github_com_mwitkow_go_proto_validators.FieldError("ConnectPhone", fmt.Errorf(`value '%v' must have a length greater than '0'`, this.ConnectPhone))
   }
   if !(len(this.ConnectPhone) < 255) {
      return github_com_mwitkow_go_proto_validators.FieldError("ConnectPhone", fmt.Errorf(`value '%v' must have a length smaller than '255'`, this.ConnectPhone))
   }
   if _, ok := Action_name[int32(this.Action)]; !ok {
      return github_com_mwitkow_go_proto_validators.FieldError("Action", fmt.Errorf(`value '%v' must be a valid Action field`, this.Action))
   }
   return nil
}
func (this *DoBidRsp) Validate() error {
   if this.CommonRsp != nil {
      if err := github_com_mwitkow_go_proto_validators.CallValidatorIfExists(this.CommonRsp); err != nil {
         return github_com_mwitkow_go_proto_validators.FieldError("CommonRsp", err)
      }
   }
   return nil
}

Этот файл привязывает метод проверки к запросу DoBidReq, и каждая проверка параметра имеет собственное сообщение об ошибке логики проверки.

использовать

func (c *CollaborativeAgent) DoBid(ctx context.Context, req *test_bid.DoBidReq, rsp *test_bid.DoBidRsp) error  {
   if err := req.Validate(); err != nil {
      logrus.Errorf("error is %v\n", err)
      rsp.CommonRsp = &coll_common.CommonRsp {
         Code: 400,
         Msg: err.Error(),
      }
   } else {
      rsp.CommonRsp = &coll_common.CommonRsp {
         Code: 200,
         Msg: "success",
      }
   }


   return nil
}

существующие проблемы

1. Когда is_in_enum использует тип перечисления для проверки, если ссылаются на тип перечисления других пакетов, возникнут проблемы при генерации кода проверки.

coll_common.Action Action = 9 [(validator.field) = {is_in_enum: true}]; // 操作

Скомпилированный код

if _, ok := Action_name[int32(this.Action)]; !ok {
   return github_com_mwitkow_go_proto_validators.FieldError("Action", fmt.Errorf(`value '%v' must be a valid Action field`, this.Action))
}

Карта Action_name генерируется в файле pb.go в общем пакете

package coll_common

type Action int32

const (
   Action_OP_UNKNOWN          Action = 0
   Action_OP_DRAFT            Action = 1
   Action_OP_COMMIT           Action = 2
   Action_OP_REVOKE           Action = 3
   Action_OP_PASS             Action = 4
   Action_OP_DENY             Action = 5
   Action_OP_RELEASE          Action = 6
   Action_OP_OFF_RELEASE      Action = 7
   Action_OP_RECOMMEND        Action = 8
   Action_OP_CANCEL_RECOMMEND Action = 9
   Action_OP_MOVE_UP          Action = 10
   Action_OP_MOVE_DOWN        Action = 11
   Action_OP_CLOSE            Action = 12
   Action_OP_SUCC             Action = 13
   Action_OP_FAIL             Action = 14
)

var Action_name = map[int32]string{
   0:  "OP_UNKNOWN",
   1:  "OP_DRAFT",
   2:  "OP_COMMIT",
   3:  "OP_REVOKE",
   4:  "OP_PASS",
   5:  "OP_DENY",
   6:  "OP_RELEASE",
   7:  "OP_OFF_RELEASE",
   8:  "OP_RECOMMEND",
   9:  "OP_CANCEL_RECOMMEND",
   10: "OP_MOVE_UP",
   11: "OP_MOVE_DOWN",
   12: "OP_CLOSE",
   13: "OP_SUCC",
   14: "OP_FAIL",
}

var Action_value = map[string]int32{
   "OP_UNKNOWN":          0,
   "OP_DRAFT":            1,
   "OP_COMMIT":           2,
   "OP_REVOKE":           3,
   "OP_PASS":             4,
   "OP_DENY":             5,
   "OP_RELEASE":          6,
   "OP_OFF_RELEASE":      7,
   "OP_RECOMMEND":        8,
   "OP_CANCEL_RECOMMEND": 9,
   "OP_MOVE_UP":          10,
   "OP_MOVE_DOWN":        11,
   "OP_CLOSE":            12,
   "OP_SUCC":             13,
   "OP_FAIL":             14,
}

Существует проблема, на которую здесь нельзя ссылаться, поэтому рекомендуется, чтобы определение перечисления было записано в том же прото-файле.

2. Как правильно оценить длину строки

string ConnectPhone = 7 [(validator.field) = {length_gt: 0, length_lt: 255}]

Сгенерированный проверочный код

if !(len(this.ConnectPhone) > 0) {
   return github_com_mwitkow_go_proto_validators.FieldError("ConnectPhone", fmt.Errorf(`value '%v' must have a length greater than '0'`, this.ConnectPhone))
}
if !(len(this.ConnectPhone) < 255) {
   return github_com_mwitkow_go_proto_validators.FieldError("ConnectPhone", fmt.Errorf(`value '%v' must have a length smaller than '255'`, this.ConnectPhone))
}

Для китайских и спецсимволов этот метод не может правильно судить о реальной длине строки.Кодировка голанга по умолчанию utf8, а в utf8 китайский занимает 3 байта, поэтому длина китайского символа должна быть 3, поэтому при проектировании прото Пора обратить на это внимание. В бизнес-коде вы можете передать

len([]rune(str))

Определите реальную длину строки, потому что rune основан на int32, а базовый байт типа строки основан на uint8.