From dcc22a1bbe70d349fa4943a4782d492b34f1ea3b Mon Sep 17 00:00:00 2001 From: Tiernan Messmer Date: Mon, 1 Jul 2024 14:06:24 +1000 Subject: [PATCH] add support for buf validate --- cmd/protoc-gen-doc/flags.go | 4 +- cmd/protoc-gen-doc/main.go | 5 +- doc.go | 6 +- extensions/buf_validate/buf_validate.go | 98 ++++++++++++++++++++ extensions/buf_validate/buf_validate_test.go | 32 +++++++ go.mod | 3 +- go.sum | 6 +- renderer.go | 6 +- 8 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 extensions/buf_validate/buf_validate.go create mode 100644 extensions/buf_validate/buf_validate_test.go diff --git a/cmd/protoc-gen-doc/flags.go b/cmd/protoc-gen-doc/flags.go index 40aa8976..90d54c94 100644 --- a/cmd/protoc-gen-doc/flags.go +++ b/cmd/protoc-gen-doc/flags.go @@ -83,8 +83,8 @@ func (f *Flags) PrintVersion() { // ParseFlags parses the supplied options are returns a `Flags` object to the caller. // // Parameters: -// * `w` - the `io.Writer` to use for printing messages (help, version, etc.) -// * `args` - the set of args the program was invoked with (typically `os.Args`) +// - `w` - the `io.Writer` to use for printing messages (help, version, etc.) +// - `args` - the set of args the program was invoked with (typically `os.Args`) func ParseFlags(w io.Writer, args []string) *Flags { f := Flags{appName: args[0], writer: w} diff --git a/cmd/protoc-gen-doc/main.go b/cmd/protoc-gen-doc/main.go index 56e42ce4..31796382 100644 --- a/cmd/protoc-gen-doc/main.go +++ b/cmd/protoc-gen-doc/main.go @@ -4,11 +4,11 @@ // // Example: generate HTML documentation // -// protoc --doc_out=. --doc_opt=html,index.html protos/*.proto +// protoc --doc_out=. --doc_opt=html,index.html protos/*.proto // // Example: use a custom template // -// protoc --doc_out=. --doc_opt=custom.tmpl,docs.txt protos/*.proto +// protoc --doc_out=. --doc_opt=custom.tmpl,docs.txt protos/*.proto // // For more details, check out the README at https://github.com/pseudomuto/protoc-gen-doc package main @@ -20,6 +20,7 @@ import ( "os" gendoc "github.com/pseudomuto/protoc-gen-doc" + _ "github.com/pseudomuto/protoc-gen-doc/extensions/buf_validate" // imported for side effects _ "github.com/pseudomuto/protoc-gen-doc/extensions/google_api_http" // imported for side effects _ "github.com/pseudomuto/protoc-gen-doc/extensions/lyft_validate" // imported for side effects _ "github.com/pseudomuto/protoc-gen-doc/extensions/validator_field" // imported for side effects diff --git a/doc.go b/doc.go index e9bee30c..df9f6622 100644 --- a/doc.go +++ b/doc.go @@ -5,15 +5,15 @@ // // Example: generate HTML documentation // -// protoc --doc_out=. --doc_opt=html,index.html protos/*.proto +// protoc --doc_out=. --doc_opt=html,index.html protos/*.proto // // Example: exclude patterns // -// protoc --doc_out=. --doc_opt=html,index.html:google/*,somedir/* protos/*.proto +// protoc --doc_out=. --doc_opt=html,index.html:google/*,somedir/* protos/*.proto // // Example: use a custom template // -// protoc --doc_out=. --doc_opt=custom.tmpl,docs.txt protos/*.proto +// protoc --doc_out=. --doc_opt=custom.tmpl,docs.txt protos/*.proto // // For more details, check out the README at https://github.com/pseudomuto/protoc-gen-doc package gendoc diff --git a/extensions/buf_validate/buf_validate.go b/extensions/buf_validate/buf_validate.go new file mode 100644 index 00000000..dafb55f6 --- /dev/null +++ b/extensions/buf_validate/buf_validate.go @@ -0,0 +1,98 @@ +package extensions + +import ( + "encoding/json" + "reflect" + "strings" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "github.com/pseudomuto/protoc-gen-doc/extensions" +) + +// ValidateRule represents a single validator rule from the (validate.rules) method option extension. +type ValidateRule struct { + Name string `json:"name"` + Value interface{} `json:"value"` +} + +// ValidateExtension contains the rules set by the (validate.rules) method option extension. +type ValidateExtension struct { + *validate.FieldConstraints + rules []ValidateRule // memoized so that we don't have to use reflection more than we need. +} + +// MarshalJSON implements the json.Marshaler interface. +func (v ValidateExtension) MarshalJSON() ([]byte, error) { return json.Marshal(v.Rules()) } + +// Rules returns the set of rules for this extension. +func (v ValidateExtension) Rules() []ValidateRule { + if v.FieldConstraints == nil { + return nil + } + if v.rules == nil { + v.rules = flattenRules("", reflect.ValueOf(v.FieldConstraints)) + } + return v.rules +} + +func flattenRules(prefix string, vv reflect.Value) (rules []ValidateRule) { + vv = reflect.Indirect(vv) + vt := vv.Type() + switch vt.Kind() { + case reflect.Struct: + nextField: + for i := 0; i < vt.NumField(); i++ { + f := vt.Field(i) + ft := f.Type + fv := vv.Field(i) + + var wasIndirect bool + for ft.Kind() == reflect.Interface || ft.Kind() == reflect.Ptr { + if fv.IsNil() { + continue nextField + } + wasIndirect = true + fv = fv.Elem() + ft = fv.Type() + } + + if !wasIndirect && fv.IsZero() { + continue nextField + } + + name := prefix + if tag, ok := f.Tag.Lookup("protobuf"); ok { + for _, opt := range strings.Split(tag, ",") { + if strings.HasPrefix(opt, "name=") { + if name != "" && !strings.HasSuffix(name, ".") { + name += "." + } + name += strings.TrimPrefix(opt, "name=") + break + } + } + } else if _, ok := f.Tag.Lookup("protobuf_oneof"); !ok { + continue nextField + } + rules = append(rules, flattenRules(name, fv)...) + } + case reflect.Slice: + if vv.Len() == 0 { + return nil + } + fallthrough + default: + rules = append(rules, ValidateRule{Name: prefix, Value: vv.Interface()}) + } + return rules +} + +func init() { + extensions.SetTransformer("buf.validate.field", func(payload interface{}) interface{} { + rules, ok := payload.(*validate.FieldConstraints) + if !ok { + return nil + } + return ValidateExtension{FieldConstraints: rules} + }) +} diff --git a/extensions/buf_validate/buf_validate_test.go b/extensions/buf_validate/buf_validate_test.go new file mode 100644 index 00000000..7a99f628 --- /dev/null +++ b/extensions/buf_validate/buf_validate_test.go @@ -0,0 +1,32 @@ +package extensions_test + +import ( + "testing" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "github.com/golang/protobuf/proto" + "github.com/pseudomuto/protoc-gen-doc/extensions" + . "github.com/pseudomuto/protoc-gen-doc/extensions/buf_validate" + "github.com/stretchr/testify/require" +) + +func TestTransform(t *testing.T) { + fieldRules := &validate.FieldConstraints{ + Type: &validate.FieldConstraints_String_{ + String_: &validate.StringRules{ + + MinLen: proto.Uint64(1), + NotIn: []string{"invalid"}, + }, + }, + } + + transformed := extensions.Transform(map[string]interface{}{"buf.validate.field": fieldRules}) + require.NotEmpty(t, transformed) + + rules := transformed["buf.validate.field"].(ValidateExtension).Rules() + require.Equal(t, []ValidateRule{ + {Name: "string.min_len", Value: uint64(1)}, + {Name: "string.not_in", Value: []string{"invalid"}}, + }, rules) +} diff --git a/go.mod b/go.mod index 858b19e6..f0507c92 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/pseudomuto/protoc-gen-doc go 1.17 require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240508200655-46a4cf4ba109.2 github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible github.com/envoyproxy/protoc-gen-validate v1.0.2 @@ -31,7 +32,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4f99eb2d..199f4146 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240508200655-46a4cf4ba109.2 h1:cFrEG/pJch6t62+jqndcPXeTNkYcztS4tBRgNkR+drw= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.34.2-20240508200655-46a4cf4ba109.2/go.mod h1:ylS4c28ACSI59oJrOdW4pHS4n0Hw4TgSPHn8rpHl4Yw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -2447,8 +2449,8 @@ google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/renderer.go b/renderer.go index eba74c5f..8d24f017 100644 --- a/renderer.go +++ b/renderer.go @@ -90,10 +90,12 @@ type Processor interface { // supplying a non-empty string as the last parameter. // // Example: generating an HTML template (assuming you've got a Template object) -// data, err := RenderTemplate(RenderTypeHTML, &template, "") +// +// data, err := RenderTemplate(RenderTypeHTML, &template, "") // // Example: generating a custom template (assuming you've got a Template object) -// data, err := RenderTemplate(RenderTypeHTML, &template, "{{range .Files}}{{.Name}}{{end}}") +// +// data, err := RenderTemplate(RenderTypeHTML, &template, "{{range .Files}}{{.Name}}{{end}}") func RenderTemplate(kind RenderType, template *Template, inputTemplate string) ([]byte, error) { if inputTemplate != "" { processor := &textRenderer{inputTemplate}