Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: protoc-gen-go-xfieldmask #755

Merged
merged 15 commits into from
Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ builds:
- windows
goarch: [amd64]

- main: ./cmd/protoc-gen-go-xfieldmask
id: "protoc-gen-go-xfieldmask"
binary: protoc-gen-go-xfieldmask
goos:
- linux
- darwin
- windows
goarch: [amd64]

changelog:
sort: asc
filters:
Expand Down
7 changes: 6 additions & 1 deletion buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins:
out: proto
opt:
- paths=source_relative

- name: go-xerror
out: proto
opt:
Expand Down Expand Up @@ -40,3 +40,8 @@ plugins:
# - omit_enum_default_value=true
- generate_unbound_methods=true
- include_package_in_tags=true

# - name: go-xfieldmask
# out: proto
# opt:
# - paths=source_relative
136 changes: 136 additions & 0 deletions cmd/protoc-gen-go-xfieldmask/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# protoc-gen-xfieldMask插件

## 说明
- 在protobuf中,`google.protobuf.FieldMask` 字段用于标识消息体中需要被更新(处理)的字段。
- `protoc-gen-xfieldMask`插件基于protobuf自定义扩展字段的方式,以插件->模板的形式在xxx_fm.pb.go文件中生成对应方法,方便客户端和服务端使用fieldMask进行字段选择性处理
- `protoc-gen-xfieldMask`可以根据需要选择性mask 请求和响应中的字段。请求和响应中的mask字段不会有冲突
- `protoc-gen-xfieldMask`可以深层指定message下的嵌套字段

## 使用
### helloworld.proto(proto/helloworld/v1/helloworld.proto)
```
syntax = "proto3";

package helloworld.v1;

import "google/protobuf/field_mask.proto";
import "fieldmask/v1/option.proto";

// The greeting service definition.
service GreeterService {
// Sends a goodbye greeting
rpc SayGoodBye (SayGoodByeRequest) returns (SayGoodByeResponse);
}

// The request message containing the greetings
message SayGoodByeRequest{
// name of the user
string name = 1;
// age of the user
uint64 age = 2;
// type filter mode
Type type = 3;
// update_mask FieldMask
google.protobuf.FieldMask update_mask = 4[
// Whether to mask the request field
(fieldmask.v1.Option).in = true,
// Whether to mask the request field
(fieldmask.v1.Option).out = true
];
}

// The response message containing the greetings
message SayGoodByeResponse{
// Data 响应数据
message Data {
// name of the user
uint64 age = 1;
// age of the user
string name = 2;
// other info of the user
OtherHelloMessage other = 3;
}
// error...
uint32 error = 1;
// msg...
string msg = 2;
// data...
Data data = 3;
}

// The response OtherHelloMessage containing the greetings
message OtherHelloMessage{
// id...
uint32 id = 1;
// address...
string address = 2;
}

// Type
enum Type {
// TYPE_UNSPECIFIED ...
TYPE_UNSPECIFIED = 0;
// TYPE_Filter ... filter模式 表示mask的字段被保留
TYPE_Filter = 1;
// TYPE_Prune ... prune模式 表示mask的字段被剔除
TYPE_Prune = 2;
}
```

### 客户端调用(proto/helloworld/v1/fieldmask_test.go)
```
protoreq := &SayGoodByeRequest{
Name: "foo",
Type: Type_TYPE_Filter, // 表示采用过滤模式
}
// MaskInName:表示需要服务端处理name字段;
// MaskOutDataName:表示需要服务端返回data.name字段;
// MaskOutDataOther:表示需要服务端返回data.other下的所有字段
protoreq.MaskInName().MaskOutDataName().MaskOutDataOther()
```

### 服务端调用(proto/helloworld/v1/helloworld_impl.go)
```
func (s *FooServer) SayGoodBye(ctx context.Context, in *SayGoodByeRequest) (out *SayGoodByeResponse, err error) {
// 初始化过滤/剔除器
var fm = new(SayGoodByeRequest_FieldMask)
if in.Type == Type_TYPE_Filter {
fm = in.FieldMaskFilter()
} else {
fm = in.FieldMaskPrune()
}
out = &SayGoodByeResponse{
Error: 0,
Msg: "请求正常",
Data: &SayGoodByeResponse_Data{
Age: 1,
Name: "",
Other: &OtherHelloMessage{
Id: 1,
Address: "bar",
},
},
}
// 判断是否需要处理name字段
if fm.MaskedInName() {
out.Data.Name = in.GetName()
}
// 判断是否需要处理age字段
if fm.MaskedInAge() {
out.Data.Age = in.GetAge()
}
out1, _ := json.Marshal(out)
fmt.Println("out1:", string(out1)) // out1: {"error":0,"msg":"请求正常","data":{"age":1,"name":"foo","other":{"id":1,"address":"bar"}}}
// 过滤响应数据
_ = fm.Mask(out)
out2, _ := json.Marshal(out)
fmt.Println("out2:", string(out2)) // out2: {"error":0,"msg":"请求正常","data":{"age":1,"name":"","other":null}}
return
}
```


## 注意
- 推荐客户端服务端约定使用filter过滤模式
- message下的嵌套字段如果引入的是外部的消息体,则Mask字段不可再深层指定
- 目前在框架层面过滤了响应体中的error和msg字段,不管是否masked都会保留在响应体中
144 changes: 144 additions & 0 deletions cmd/protoc-gen-go-xfieldmask/fieldmask.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package main

import (
"fmt"
"strings"

fieldmask "github.com/douyu/jupiter/proto/fieldmask/v1"

"github.com/douyu/jupiter/pkg"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
)

var version = pkg.JupiterVersion()

const (
googlefieldmaskPkg = protogen.GoImportPath("google.golang.org/protobuf/types/known/fieldmaskpb")
pbfieldmaskPkg = protogen.GoImportPath("github.com/douyu/jupiter/pkg/util/xfieldmask")
deprecationComment = "// Deprecated: Do not use."
)

// generateFile generates a _fm.pb.go file.
func generateFile(gen *protogen.Plugin, file *protogen.File) *protogen.GeneratedFile {
if len(file.Services) == 0 {
return nil
}
filename := file.GeneratedFilenamePrefix + "_fm.pb.go"
g := gen.NewGeneratedFile(filename, file.GoImportPath)
g.P("// Code generated by github.com/douyu/jupiter/cmd/protoc-gen-go-fieldmask. DO NOT EDIT.")
g.P("// versions:")
g.P(fmt.Sprintf("// - protoc-gen-go-fieldmask %s", version))
g.P("// - protoc ", protocVersion(gen))
g.P()
g.P("package ", file.GoPackageName)
g.P()
g.P("// This is a compile-time assertion to ensure that this generated file")
g.P("// is compatible with the github.com/douyu/jupiter/cmd/protoc-gen-go-fieldmask package it is being compiled against.")
g.P("var _ = ", googlefieldmaskPkg.Ident("New"))
g.P("var _ = ", pbfieldmaskPkg.Ident("New"))
g.P()

for _, service := range file.Services {
genService(gen, file, g, service)
}
return g
}

func genService(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, s *protogen.Service) {
if s.Desc.Options().(*descriptorpb.ServiceOptions).GetDeprecated() {
g.P("//")
g.P(deprecationComment)
}

sd := &service{
Name: s.GoName,
FullName: string(s.Desc.FullName()),
FilePath: file.Desc.Path(),
}
// 判断是否存在对应扩展字段
for _, method := range s.Methods {
if genMessage(gen, method) != nil {
sd.Message = append(sd.Message, genMessage(gen, method))
}
}

g.P(sd.execute())
}

func genMessage(gen *protogen.Plugin, m *protogen.Method) *message {
var msg = &message{
UpdateInFields: make([]*field, 0),
UpdateOutFields: make([]*field, 0),
}
// 找到标识字段并判断请求响应是否需要mask
for _, field := range m.Input.Fields {
mask, ok := proto.GetExtension(field.Desc.Options(), fieldmask.E_Option).(*fieldmask.FieldMask)
if mask != nil && ok {
msg.IdentifyFieldGoName = field.GoName
msg.IdentifyField = string(field.Desc.Name())
if mask.GetIn() {
msg.RequestName = m.Input.GoIdent.GoName
msg.In = true
}
if mask.GetOut() {
msg.ResponseName = m.Output.GoIdent.GoName
msg.Out = true
}
break
}
}
// 写入所有mask的字段
visitMessage(gen, m.Input.Desc, "", msg, MsgTypeIn)
visitMessage(gen, m.Output.Desc, "", msg, MsgTypeOut)
msg.RemovePrefix()
return msg
}

// 递归遍历消息类型中的所有字段
func visitMessage(gen *protogen.Plugin, md protoreflect.MessageDescriptor, prefix string, msg *message, msgType MsgType) {
var subString string
for i := 0; i < md.Fields().Len(); i++ {
fd := md.Fields().Get(i)
if fd.Name() == protoreflect.Name(msg.IdentifyField) {
continue
}
if msgType == MsgTypeIn {
msg.UpdateInFields = append(msg.UpdateInFields, &field{
UnderLineName: fmt.Sprintf("%s_%s", prefix, fd.Name()),
DotName: fmt.Sprintf("%s.%s", prefix, fd.Name()),
})
subString = msg.RequestName
} else {
msg.UpdateOutFields = append(msg.UpdateOutFields, &field{
UnderLineName: fmt.Sprintf("%s_%s", prefix, fd.Name()),
DotName: fmt.Sprintf("%s.%s", prefix, fd.Name()),
})
subString = msg.ResponseName
}

// 判断嵌套类型
if fd.Kind() == protoreflect.MessageKind {
nestedMd := fd.Message()
if nestedMd != nil && strings.Contains(string(nestedMd.FullName()), subString) {
visitMessage(gen, nestedMd, prefix+"."+string(fd.Name()), msg, msgType)
}
}
}
}

func protocVersion(gen *protogen.Plugin) string {
v := gen.Request.GetCompilerVersion()
if v == nil {
return "(unknown)"
}

var suffix string
if s := v.GetSuffix(); s != "" {
suffix = "-" + s
}

return fmt.Sprintf("v%d.%d.%d%s", v.GetMajor(), v.GetMinor(), v.GetPatch(), suffix)
}
Loading