bufbuild / protovalidate

Protocol Buffer Validation - Go, Java, Python, and C++ Beta Releases!
https://buf.build/bufbuild/protovalidate
Apache License 2.0
915 stars 37 forks source link

[Feature Request] Is it possible to add validated input parameters and rule values in the error Violation? #17

Open shuqingzai opened 1 year ago

shuqingzai commented 1 year ago

Feature description:

Example Proto

syntax = "proto3";

package project.api.v1;

import "buf/validate/validate.proto";

option go_package = "project/api/v1;v1";

// 分页参数
message PageParams {
  // 当前页, 从1开始(默认: 1, 最大: 1000)
  uint32 page = 1 [
    (buf.validate.field).uint32 = {gt: 0, lte: 1000}
  ];

  // 每页数量(默认: 20, 最大: 500)
  uint32 pageSize = 2 [
    (buf.validate.field).uint32 = {gt: 0, lte: 500}
  ];
}

go validate

func main() {
    msg := &pb.PageParams{
        Page:     0,
        PageSize: 10,
    }

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

    if err = v.Validate(msg); err != nil {
        var valErr *protovalidate.ValidationError
        if ok := errors.As(err, &valErr); ok {
            errPb := valErr.ToProto()
            fmt.Printf("\n%+v\n\n", errPb.GetViolations())
        }
    }
}

// ouput
//
// [field_path:"page"  constraint_id:"uint32.gt_lte"  message:"value must be greater than 0 and less than or equal to 1000"]
//

When I validate, the error returned by page is *validate.Violation, but I can’t get the request value 0 and the rule values: 0 and 1000, can the package add new fields (value and attributes ?? ) return?

Problem it solves or use case:

Users can get these values for more custom operations

I need to perform i18n now, because the semantics of each language are different when translating, and I need to customize the translation, but I can’t get these values, which makes the work very difficult

Proposed implementation or solution:

Contribution:

Examples or references:

Additional context:

rodaine commented 1 year ago

Hi @shuqingzai, and thanks for the feedback! If you only need to support a single language, for now I'd recommend creating a custom constraint so that you can control the message. Will explore options with the team (exposing the FieldConstraints shouldn't be too difficult).

MarcusMogg commented 1 year ago

you can get it, but with ugly reflection code

func main() {

    msg := &pb.PageParams{
        Page:     0,
        PageSize: 10,
    }

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

    if err = v.Validate(msg); err != nil {
        var valErr *protovalidate.ValidationError
        if ok := errors.As(err, &valErr); ok {
            errPb := valErr.ToProto()
            fmt.Printf("\n%+v\n\n", errPb.GetViolations())
            a := errPb.GetViolations()[0]

            fieldDesc := msg.ProtoReflect().Descriptor().Fields().ByTextName(a.FieldPath)
            // import "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
            typeDesc := (*validate.E_Field).TypeDescriptor()
            x := fieldDesc.Options().ProtoReflect().Get(typeDesc).Message().Interface().(*validate.FieldConstraints)

            fmt.Println(x.Type) // type is oneof
            fmt.Println(*(x.Type.(*validate.FieldConstraints_Uint32).Uint32.Lte))
        }
    }
}
shuqingzai commented 1 year ago

you can get it, but with ugly reflection code

func main() {

  msg := &pb.PageParams{
      Page:     0,
      PageSize: 10,
  }

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

  if err = v.Validate(msg); err != nil {
      var valErr *protovalidate.ValidationError
      if ok := errors.As(err, &valErr); ok {
          errPb := valErr.ToProto()
          fmt.Printf("\n%+v\n\n", errPb.GetViolations())
          a := errPb.GetViolations()[0]

          fieldDesc := msg.ProtoReflect().Descriptor().Fields().ByTextName(a.FieldPath)
          // import "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
          typeDesc := (*validate.E_Field).TypeDescriptor()
          x := fieldDesc.Options().ProtoReflect().Get(typeDesc).Message().Interface().(*validate.FieldConstraints)

          fmt.Println(x.Type) // type is oneof
          fmt.Println(*(x.Type.(*validate.FieldConstraints_Uint32).Uint32.Lte))
      }
  }
}

@MarcusMogg

Your approach is possible, but when v.Validate() is used, reflection has already been performed. If protovalidate natively supports it, it will definitely be better for performance and scalability. Other languages can also enjoy such convenience

MarcusMogg commented 1 year ago

you can get it, but with ugly reflection code

func main() {

    msg := &pb.PageParams{
        Page:     0,
        PageSize: 10,
    }

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

    if err = v.Validate(msg); err != nil {
        var valErr *protovalidate.ValidationError
        if ok := errors.As(err, &valErr); ok {
            errPb := valErr.ToProto()
            fmt.Printf("\n%+v\n\n", errPb.GetViolations())
            a := errPb.GetViolations()[0]

            fieldDesc := msg.ProtoReflect().Descriptor().Fields().ByTextName(a.FieldPath)
            // import "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
            typeDesc := (*validate.E_Field).TypeDescriptor()
            x := fieldDesc.Options().ProtoReflect().Get(typeDesc).Message().Interface().(*validate.FieldConstraints)

            fmt.Println(x.Type) // type is oneof
            fmt.Println(*(x.Type.(*validate.FieldConstraints_Uint32).Uint32.Lte))
        }
    }
}

@MarcusMogg

Your approach is possible, but when v.Validate() is used, reflection has already been performed. If protovalidate natively supports it, it will definitely be better for performance and scalability. Other languages can also enjoy such convenience

Using reflection is inevitable when you append fields (so many rules here). Perhaps a custom error message formatter interface for rules would suit your needs better?

m-d-z-z commented 1 year ago

I18n for fields validating is very necessary. I hope this package can handle it gracefully.