├── codecov.yml ├── go.mod ├── .gitignore ├── .github └── workflows │ ├── linter.yml │ ├── tests.yml │ └── coverage.yml ├── go.sum ├── Dockerfile.generate ├── LICENSE ├── testproto ├── testproto.proto └── testproto.pb.go ├── README.md ├── examples_test.go ├── fmutils.go └── fmutils_test.go /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "**/*.pb.go" -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mennanov/fmutils 2 | 3 | go 1.22 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 9 | google.golang.org/protobuf v1.36.6 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | coverage.txt 15 | .idea 16 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: [ "push", "pull_request" ] 4 | 5 | jobs: 6 | 7 | build: 8 | name: Linter 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup Go 1.22 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.22' 16 | - name: Go vet 17 | run: go vet ./... 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Go tests 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | go-test: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: [ '1.21', '1.22', '1.23', '1.24' ] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Go ${{ matrix.go-version }} 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - name: Run tests 20 | run: go test -v ./... 21 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Tests coverage 2 | 3 | on: [ "push", "pull_request" ] 4 | 5 | jobs: 6 | 7 | build: 8 | name: Coverage 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup Go 1.22 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.22' 16 | - name: Run tests with coverage 17 | run: go test -v -coverprofile=coverage.txt -covermode=atomic ./... 18 | - uses: codecov/codecov-action@v3 19 | with: 20 | token: ${{ secrets.CODECOV_TOKEN }} 21 | verbose: true -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 2 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 5 | google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 h1:vArvWooPH749rNHpBGgVl+U9B9dATjiEhJzcWGlovNs= 6 | google.golang.org/genproto v0.0.0-20230202175211-008b39050e57/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= 7 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 8 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 9 | -------------------------------------------------------------------------------- /Dockerfile.generate: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:1.25-bookworm AS builder 3 | 4 | ARG PROTOC_VERSION=31.0 5 | ARG PROTOC_GEN_GO_VERSION=1.36.6 6 | 7 | WORKDIR /src 8 | 9 | # Install protoc. 10 | RUN apt-get update \ 11 | && apt-get install -y curl unzip \ 12 | && rm -rf /var/lib/apt/lists/* \ 13 | && curl -sSL -o /tmp/protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip \ 14 | && unzip /tmp/protoc.zip -d /usr/local \ 15 | && rm /tmp/protoc.zip 16 | 17 | # Install the Go code generator. 18 | RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v${PROTOC_GEN_GO_VERSION} 19 | 20 | # Copy the proto and generate the Go output. 21 | COPY testproto/testproto.proto testproto.proto 22 | RUN protoc --proto_path=. --go_out=. --go_opt=paths=source_relative testproto.proto 23 | 24 | FROM scratch AS artifact 25 | COPY --from=builder /src/testproto.pb.go /testproto/testproto.pb.go 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Renat Mennanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /testproto/testproto.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package testproto; 4 | 5 | import "google/protobuf/any.proto"; 6 | import "google/protobuf/field_mask.proto"; 7 | 8 | option go_package = "github.com/mennanov/fmutils/testproto;testproto"; 9 | 10 | message User { 11 | int64 user_id = 1; 12 | string name = 2; 13 | } 14 | 15 | message Photo { 16 | int64 photo_id = 1; 17 | string path = 2; 18 | Dimensions dimensions = 3; 19 | } 20 | 21 | message Dimensions { 22 | int32 width = 1; 23 | int32 height = 2; 24 | } 25 | 26 | message Attribute { 27 | map tags = 1; 28 | } 29 | 30 | message Address { 31 | string number = 1; 32 | string street = 2; 33 | string postal_code = 3; 34 | } 35 | 36 | message Options { 37 | optional string optional_string = 1; 38 | optional int32 optional_int = 2; 39 | optional Photo optional_photo = 3; 40 | optional Attribute optional_attr = 4; 41 | } 42 | 43 | message Profile { 44 | User user = 1; 45 | Photo photo = 2; 46 | repeated int64 login_timestamps = 3; 47 | repeated Photo gallery = 4; 48 | map attributes = 5; 49 | map addresses_by_name = 6; 50 | } 51 | 52 | message UpdateProfileRequest { 53 | Profile profile = 1; 54 | google.protobuf.FieldMask fieldmask = 2; 55 | } 56 | 57 | enum Status { 58 | UNKNOWN = 0; 59 | OK = 1; 60 | FAILED = 2; 61 | } 62 | 63 | message Result { 64 | bytes data = 1; 65 | int64 next_token = 2; 66 | } 67 | 68 | message Event { 69 | int64 event_id = 1; 70 | oneof changed { 71 | User user = 2; 72 | Photo photo = 3; 73 | Status status = 4; 74 | google.protobuf.Any details = 5; 75 | Profile profile = 6; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang protobuf FieldMask utils 2 | 3 | [![Build Status](https://github.com/mennanov/fmutils/actions/workflows/tests.yml/badge.svg)](https://github.com/mennanov/fmutils/actions/workflows/tests.yml) 4 | [![Coverage Status](https://codecov.io/gh/mennanov/fmutils/branch/main/graph/badge.svg)](https://codecov.io/gh/mennanov/fmutils) 5 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/mennanov/fmutils)](https://pkg.go.dev/github.com/mennanov/fmutils) 6 | 7 | ### Filter a protobuf message with a FieldMask applied 8 | 9 | ```go 10 | // Keeps the fields mentioned in the paths untouched, all the other fields will be cleared. 11 | fmutils.Filter(protoMessage, []string{"a.b.c", "d"}) 12 | ``` 13 | 14 | ### Prune a protobuf message with a FieldMask applied 15 | 16 | ```go 17 | // Clears all the fields mentioned in the paths, all the other fields will be left untouched. 18 | fmutils.Prune(protoMessage, []string{"a.b.c", "d"}) 19 | ``` 20 | 21 | ### Merge protobuf messages with a FieldMask applied 22 | 23 | ```go 24 | // Overwrites the fields in the dst from src. 25 | // Only the fields listed in the field mask will be copied. 26 | fmutils.Overwrite(src, dst, []string{"a.b.c", "d"}) 27 | ``` 28 | 29 | ### Working with Golang protobuf APIv1 30 | 31 | This library uses the [new Go API for protocol buffers](https://blog.golang.org/protobuf-apiv2). 32 | If your `*.pb.go` files are generated with the old version APIv1 then you have 2 choices: 33 | 34 | - migrate to the new APIv2 `google.golang.org/protobuf` 35 | - upgrade an existing APIv1 version to `github.com/golang/protobuf@v1.4.0` that implements the new API 36 | 37 | In both cases you'll need to regenerate `*.pb.go` files. 38 | 39 | If you decide to stay with APIv1 then you need to use 40 | the [`proto.MessageV2`](https://pkg.go.dev/github.com/golang/protobuf@v1.4.3/proto#MessageV2) function like this: 41 | 42 | ```go 43 | import protov1 "github.com/golang/protobuf/proto" 44 | 45 | fmutils.Filter(protov1.MessageV2(protoMessage), []string{"a.b.c", "d"}) 46 | ``` 47 | 48 | [Read more about the Go protobuf API versions.](https://blog.golang.org/protobuf-apiv2#TOC_4.) 49 | 50 | ### Examples 51 | 52 | See the [examples_test.go](https://github.com/mennanov/fmutils/blob/main/examples_test.go) for real life examples. 53 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package fmutils_test 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "google.golang.org/genproto/protobuf/field_mask" 8 | "google.golang.org/protobuf/proto" 9 | 10 | "github.com/mennanov/fmutils" 11 | "github.com/mennanov/fmutils/testproto" 12 | ) 13 | 14 | var reSpaces = regexp.MustCompile(`\s+`) 15 | 16 | // ExampleFilter_update_request illustrates an API endpoint that updates an existing entity. 17 | // The request to that endpoint provides a field mask that should be used to update the entity. 18 | func ExampleFilter_update_request() { 19 | // Assuming the profile entity is loaded from a database. 20 | profile := &testproto.Profile{ 21 | User: &testproto.User{ 22 | UserId: 64, 23 | Name: "user name", 24 | }, 25 | Photo: &testproto.Photo{ 26 | PhotoId: 2, 27 | Path: "photo path", 28 | Dimensions: &testproto.Dimensions{ 29 | Width: 100, 30 | Height: 120, 31 | }, 32 | }, 33 | LoginTimestamps: []int64{1, 2, 3}, 34 | } 35 | // An API request from an API user. 36 | updateProfileRequest := &testproto.UpdateProfileRequest{ 37 | Profile: &testproto.Profile{ 38 | User: &testproto.User{ 39 | UserId: 65, // not listed in the field mask, so won't be updated. 40 | Name: "new user name", 41 | }, 42 | Photo: &testproto.Photo{ 43 | PhotoId: 3, // not listed in the field mask, so won't be updated. 44 | Path: "new photo path", 45 | Dimensions: &testproto.Dimensions{ 46 | Width: 50, 47 | }, 48 | }, 49 | LoginTimestamps: []int64{4, 5}}, 50 | Fieldmask: &field_mask.FieldMask{ 51 | Paths: []string{"user.name", "photo.path", "photo.dimensions.width", "login_timestamps"}}, 52 | } 53 | // Normalize and validate the field mask before using it. 54 | updateProfileRequest.Fieldmask.Normalize() 55 | if !updateProfileRequest.Fieldmask.IsValid(profile) { 56 | // Return an error. 57 | panic("invalid field mask") 58 | } 59 | // Redact the request according to the provided field mask. 60 | fmutils.Filter(updateProfileRequest.GetProfile(), updateProfileRequest.Fieldmask.GetPaths()) 61 | // Now that the request is vetted we can merge it with the profile entity. 62 | proto.Merge(profile, updateProfileRequest.GetProfile()) 63 | // The profile can now be saved in a database. 64 | fmt.Println(reSpaces.ReplaceAllString(profile.String(), " ")) 65 | // Output: user:{user_id:64 name:"new user name"} photo:{photo_id:2 path:"new photo path" dimensions:{width:50 height:120}} login_timestamps:1 login_timestamps:2 login_timestamps:3 login_timestamps:4 login_timestamps:5 66 | } 67 | 68 | // ExampleFilter_reuse_mask illustrates how a single NestedMask instance can be used to process multiple proto messages. 69 | func ExampleFilter_reuse_mask() { 70 | users := []*testproto.User{ 71 | { 72 | UserId: 1, 73 | Name: "name 1", 74 | }, 75 | { 76 | UserId: 2, 77 | Name: "name 2", 78 | }, 79 | } 80 | // Create a mask only once and reuse it. 81 | mask := fmutils.NestedMaskFromPaths([]string{"name"}) 82 | for _, user := range users { 83 | mask.Filter(user) 84 | } 85 | fmt.Println(users) 86 | // Output: [name:"name 1" name:"name 2"] 87 | } 88 | 89 | // ExamplePathsFromFieldNumbers illustrates how to convert protobuf field numbers to field paths. 90 | // This is useful when you have field numbers from the protobuf schema and need to convert them 91 | // to field paths for use with field masks. 92 | func ExamplePathsFromFieldNumbers() { 93 | user := &testproto.User{} 94 | 95 | // Convert field numbers to field paths. 96 | // Field 1 is "user_id", field 2 is "name" 97 | paths := fmutils.PathsFromFieldNumbers(user, 1, 2) 98 | fmt.Println("Field numbers:", []int{1, 2}) 99 | fmt.Println("Paths:", paths) 100 | 101 | // Output: 102 | // Field numbers: [1 2] 103 | // Paths: [user_id name] 104 | } 105 | -------------------------------------------------------------------------------- /fmutils.go: -------------------------------------------------------------------------------- 1 | package fmutils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "google.golang.org/protobuf/proto" 8 | "google.golang.org/protobuf/reflect/protoreflect" 9 | ) 10 | 11 | // Filter keeps the msg fields that are listed in the paths and clears all the rest. 12 | // 13 | // This is a handy wrapper for NestedMask.Filter method. 14 | // If the same paths are used to process multiple proto messages use NestedMask.Filter method directly. 15 | func Filter(msg proto.Message, paths []string) { 16 | NestedMaskFromPaths(paths).Filter(msg) 17 | } 18 | 19 | // Prune clears all the fields listed in paths from the given msg. 20 | // 21 | // This is a handy wrapper for NestedMask.Prune method. 22 | // If the same paths are used to process multiple proto messages use NestedMask.Filter method directly. 23 | func Prune(msg proto.Message, paths []string) { 24 | NestedMaskFromPaths(paths).Prune(msg) 25 | } 26 | 27 | // Overwrite overwrites all the fields listed in paths in the dest msg using values from src msg. 28 | // Map values are partially merged when the mask addresses nested fields inside the map value. 29 | // 30 | // This is a handy wrapper for NestedMask.Overwrite method. 31 | // If the same paths are used to process multiple proto messages use NestedMask.Overwrite method directly. 32 | func Overwrite(src, dest proto.Message, paths []string) { 33 | NestedMaskFromPaths(paths).Overwrite(src, dest) 34 | } 35 | 36 | // Validate checks if all paths are valid for specified message 37 | // 38 | // This is a handy wrapper for NestedMask.Validate method. 39 | // If the same paths are used to process multiple proto messages use NestedMask.Validate method directly. 40 | func Validate(validationModel proto.Message, paths []string) error { 41 | return NestedMaskFromPaths(paths).Validate(validationModel) 42 | } 43 | 44 | // NestedMask represents a field mask as a recursive map. 45 | type NestedMask map[string]NestedMask 46 | 47 | // NestedMaskFromPaths creates an instance of NestedMask for the given paths. 48 | // 49 | // For example ["foo.bar", "foo.baz"] becomes {"foo": {"bar": nil, "baz": nil}}. 50 | func NestedMaskFromPaths(paths []string) NestedMask { 51 | var add func(path string, fm NestedMask) 52 | add = func(path string, mask NestedMask) { 53 | if len(path) == 0 { 54 | // Invalid input. 55 | return 56 | } 57 | dotIdx := strings.IndexRune(path, '.') 58 | if dotIdx == -1 { 59 | mask[path] = nil 60 | } else { 61 | field := path[:dotIdx] 62 | if len(field) == 0 { 63 | // Invalid input. 64 | return 65 | } 66 | rest := path[dotIdx+1:] 67 | nested := mask[field] 68 | if nested == nil { 69 | nested = make(NestedMask) 70 | mask[field] = nested 71 | } 72 | add(rest, nested) 73 | } 74 | } 75 | 76 | mask := make(NestedMask) 77 | for _, p := range paths { 78 | add(p, mask) 79 | } 80 | 81 | return mask 82 | } 83 | 84 | // Filter keeps the msg fields that are listed in the paths and clears all the rest. 85 | // 86 | // If the mask is empty then all the fields are kept. 87 | // Paths are assumed to be valid and normalized otherwise the function may panic. 88 | // See google.golang.org/protobuf/types/known/fieldmaskpb for details. 89 | func (mask NestedMask) Filter(msg proto.Message) { 90 | if len(mask) == 0 { 91 | return 92 | } 93 | 94 | rft := msg.ProtoReflect() 95 | rft.Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool { 96 | m, ok := mask[string(fd.Name())] 97 | if ok { 98 | if len(m) == 0 { 99 | return true 100 | } 101 | 102 | if fd.IsMap() { 103 | xmap := rft.Get(fd).Map() 104 | xmap.Range(func(mk protoreflect.MapKey, mv protoreflect.Value) bool { 105 | if mi, ok := m[mk.String()]; ok { 106 | if i, ok := mv.Interface().(protoreflect.Message); ok && len(mi) > 0 { 107 | mi.Filter(i.Interface()) 108 | } 109 | } else { 110 | xmap.Clear(mk) 111 | } 112 | 113 | return true 114 | }) 115 | } else if fd.IsList() { 116 | list := rft.Get(fd).List() 117 | for i := 0; i < list.Len(); i++ { 118 | m.Filter(list.Get(i).Message().Interface()) 119 | } 120 | } else if fd.Kind() == protoreflect.MessageKind { 121 | m.Filter(rft.Get(fd).Message().Interface()) 122 | } 123 | } else { 124 | rft.Clear(fd) 125 | } 126 | return true 127 | }) 128 | } 129 | 130 | // Prune clears all the fields listed in paths from the given msg. 131 | // 132 | // All other fields are kept untouched. If the mask is empty no fields are cleared. 133 | // This operation is the opposite of NestedMask.Filter. 134 | // Paths are assumed to be valid and normalized otherwise the function may panic. 135 | // See google.golang.org/protobuf/types/known/fieldmaskpb for details. 136 | func (mask NestedMask) Prune(msg proto.Message) { 137 | if len(mask) == 0 { 138 | return 139 | } 140 | 141 | rft := msg.ProtoReflect() 142 | rft.Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool { 143 | m, ok := mask[string(fd.Name())] 144 | if ok { 145 | if len(m) == 0 { 146 | rft.Clear(fd) 147 | return true 148 | } 149 | 150 | if fd.IsMap() { 151 | xmap := rft.Get(fd).Map() 152 | xmap.Range(func(mk protoreflect.MapKey, mv protoreflect.Value) bool { 153 | if mi, ok := m[mk.String()]; ok { 154 | if i, ok := mv.Interface().(protoreflect.Message); ok && len(mi) > 0 { 155 | mi.Prune(i.Interface()) 156 | } else { 157 | xmap.Clear(mk) 158 | } 159 | } 160 | 161 | return true 162 | }) 163 | } else if fd.IsList() { 164 | list := rft.Get(fd).List() 165 | for i := 0; i < list.Len(); i++ { 166 | m.Prune(list.Get(i).Message().Interface()) 167 | } 168 | } else if fd.Kind() == protoreflect.MessageKind { 169 | m.Prune(rft.Get(fd).Message().Interface()) 170 | } 171 | } 172 | return true 173 | }) 174 | } 175 | 176 | // Overwrite overwrites all the fields listed in paths in the dest msg using values from src msg. 177 | // 178 | // All other fields are kept untouched. If the mask is empty, no fields are overwritten. 179 | // Supports scalars, messages, repeated fields, and maps. 180 | // If the parent of the field is nil message, the parent is initiated before overwriting the field 181 | // If the field in src is empty value, the field in dest is cleared. 182 | // Paths are assumed to be valid and normalized otherwise the function may panic. 183 | func (mask NestedMask) Overwrite(src, dest proto.Message) { 184 | mask.overwrite(src.ProtoReflect(), dest.ProtoReflect()) 185 | } 186 | 187 | // Validate checks if all paths are valid for specified message. 188 | // 189 | // Supports scalars, messages, repeated fields, and maps. 190 | func (m NestedMask) Validate(validationModel proto.Message) error { 191 | err := m.validate("", validationModel.ProtoReflect()) 192 | if err != nil { 193 | return fmt.Errorf("invalid mask: %w", err) 194 | } 195 | 196 | return nil 197 | } 198 | 199 | func (mask NestedMask) overwrite(srcRft, destRft protoreflect.Message) { 200 | for srcFDName, submask := range mask { 201 | srcFD := srcRft.Descriptor().Fields().ByName(protoreflect.Name(srcFDName)) 202 | srcVal := srcRft.Get(srcFD) 203 | if len(submask) == 0 { 204 | if isValid(srcFD, srcVal) && !srcVal.Equal(srcFD.Default()) { 205 | destRft.Set(srcFD, srcVal) 206 | } else { 207 | destRft.Clear(srcFD) 208 | } 209 | } else if srcFD.IsMap() && srcFD.Kind() == protoreflect.MessageKind { 210 | srcMap := srcRft.Get(srcFD).Map() 211 | destMap := destRft.Get(srcFD).Map() 212 | if !destMap.IsValid() { 213 | destRft.Set(srcFD, protoreflect.ValueOf(srcMap)) 214 | destMap = destRft.Get(srcFD).Map() 215 | } 216 | srcMap.Range(func(mk protoreflect.MapKey, mv protoreflect.Value) bool { 217 | if mi, ok := submask[mk.String()]; ok { 218 | if i, ok := mv.Interface().(protoreflect.Message); ok && len(mi) > 0 { 219 | // Clone existing dest entry so we don't mutate other fields. 220 | var destMsg protoreflect.Message 221 | if v := destMap.Get(mk); v.IsValid() && v.Message().IsValid() { 222 | destMsg = proto.Clone(v.Message().Interface()).ProtoReflect() 223 | } else { 224 | destMsg = i.New() 225 | } 226 | // Store the (cloned/new) message then overwrite only masked fields. 227 | destMap.Set(mk, protoreflect.ValueOfMessage(destMsg)) 228 | mi.overwrite(mv.Message(), destMsg) 229 | } else { 230 | destMap.Set(mk, mv) 231 | } 232 | } else { 233 | destMap.Clear(mk) 234 | } 235 | return true 236 | }) 237 | } else if srcFD.IsList() && srcFD.Kind() == protoreflect.MessageKind { 238 | srcList := srcRft.Get(srcFD).List() 239 | destList := destRft.Mutable(srcFD).List() 240 | // Truncate anything in dest that exceeds the length of src 241 | if srcList.Len() < destList.Len() { 242 | destList.Truncate(srcList.Len()) 243 | } 244 | for i := 0; i < srcList.Len(); i++ { 245 | srcListItem := srcList.Get(i) 246 | var destListItem protoreflect.Message 247 | if destList.Len() > i { 248 | // Overwrite existing items. 249 | destListItem = destList.Get(i).Message() 250 | } else { 251 | // Append new items to overwrite. 252 | destListItem = destList.AppendMutable().Message() 253 | } 254 | submask.overwrite(srcListItem.Message(), destListItem) 255 | } 256 | 257 | } else if srcFD.Kind() == protoreflect.MessageKind { 258 | // If the dest field is nil 259 | if !destRft.Get(srcFD).Message().IsValid() { 260 | destRft.Set(srcFD, protoreflect.ValueOf(destRft.Get(srcFD).Message().New())) 261 | } 262 | submask.overwrite(srcRft.Get(srcFD).Message(), destRft.Get(srcFD).Message()) 263 | } 264 | } 265 | } 266 | 267 | func (mask NestedMask) validate(pathPrefix string, msg protoreflect.Message) error { 268 | for fieldName, submask := range mask { 269 | fieldDesc := msg.Descriptor().Fields().ByName(protoreflect.Name(fieldName)) 270 | if fieldDesc == nil { 271 | return fmt.Errorf("unknown path: %q", fullPath(pathPrefix, fieldName)) 272 | } 273 | 274 | if len(submask) == 0 { 275 | continue 276 | } 277 | 278 | var nestedMsg protoreflect.Message 279 | 280 | if fieldDesc.IsList() { 281 | listVal := msg.Get(fieldDesc).List().NewElement() 282 | 283 | var ok bool 284 | 285 | if nestedMsg, ok = listVal.Interface().(protoreflect.Message); !ok { 286 | return fmt.Errorf("%q: list element isn't message kind", fullPath(pathPrefix, fieldName)) 287 | } 288 | } else if fieldDesc.IsMap() { 289 | mapVal := msg.Get(fieldDesc).Map().NewValue() 290 | 291 | var ok bool 292 | 293 | if nestedMsg, ok = mapVal.Interface().(protoreflect.Message); !ok { 294 | return fmt.Errorf("%q: map value isn't message kind", fullPath(pathPrefix, fieldName)) 295 | } 296 | } else if fieldDesc.Kind() == protoreflect.MessageKind { 297 | nestedMsg = msg.Get(fieldDesc).Message() 298 | } else { 299 | return fmt.Errorf("%q: can't get nested fields", fullPath(pathPrefix, fieldName)) 300 | } 301 | 302 | err := submask.validate(fullPath(pathPrefix, fieldName), nestedMsg) 303 | if err != nil { 304 | return err 305 | } 306 | } 307 | 308 | return nil 309 | } 310 | 311 | func fullPath(pathPrefix, field string) string { 312 | if pathPrefix == "" { 313 | return field 314 | } 315 | 316 | return pathPrefix + "." + field 317 | } 318 | 319 | func isValid(fd protoreflect.FieldDescriptor, val protoreflect.Value) bool { 320 | if fd.IsMap() { 321 | return val.Map().IsValid() 322 | } else if fd.IsList() { 323 | return val.List().IsValid() 324 | } else if fd.Message() != nil { 325 | return val.Message().IsValid() 326 | } 327 | return true 328 | } 329 | 330 | // PathsFromFieldNumbers converts protobuf field numbers to field paths for the given message. 331 | // 332 | // This function takes a protobuf message and a list of field numbers, and 333 | // returns a slice of field paths (field names) corresponding to those numbers. 334 | // 335 | // Field numbers that don't exist in the message descriptor are skipped. 336 | // 337 | // If no field numbers are provided, returns nil. 338 | // 339 | // Example: 340 | // 341 | // // For a message with fields: name (field 1), age (field 2), address (field 3) 342 | // paths := PathsFromFieldNumbers(msg, 1, 2) 343 | // // Returns: ["name", "age"] 344 | func PathsFromFieldNumbers(msg proto.Message, fieldNumbers ...int) []string { 345 | if len(fieldNumbers) == 0 { 346 | return nil 347 | } 348 | paths := make([]string, 0, len(fieldNumbers)) 349 | descriptor := msg.ProtoReflect().Descriptor() 350 | for _, n := range fieldNumbers { 351 | field := descriptor.Fields().ByNumber(protoreflect.FieldNumber(n)) 352 | if field != nil { 353 | paths = append(paths, field.TextName()) 354 | } 355 | } 356 | return paths 357 | } 358 | -------------------------------------------------------------------------------- /testproto/testproto.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.36.6 4 | // protoc v6.31.0 5 | // source: testproto.proto 6 | 7 | package testproto 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | anypb "google.golang.org/protobuf/types/known/anypb" 13 | fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" 14 | reflect "reflect" 15 | sync "sync" 16 | unsafe "unsafe" 17 | ) 18 | 19 | const ( 20 | // Verify that this generated code is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 22 | // Verify that runtime/protoimpl is sufficiently up-to-date. 23 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 24 | ) 25 | 26 | type Status int32 27 | 28 | const ( 29 | Status_UNKNOWN Status = 0 30 | Status_OK Status = 1 31 | Status_FAILED Status = 2 32 | ) 33 | 34 | // Enum value maps for Status. 35 | var ( 36 | Status_name = map[int32]string{ 37 | 0: "UNKNOWN", 38 | 1: "OK", 39 | 2: "FAILED", 40 | } 41 | Status_value = map[string]int32{ 42 | "UNKNOWN": 0, 43 | "OK": 1, 44 | "FAILED": 2, 45 | } 46 | ) 47 | 48 | func (x Status) Enum() *Status { 49 | p := new(Status) 50 | *p = x 51 | return p 52 | } 53 | 54 | func (x Status) String() string { 55 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 56 | } 57 | 58 | func (Status) Descriptor() protoreflect.EnumDescriptor { 59 | return file_testproto_proto_enumTypes[0].Descriptor() 60 | } 61 | 62 | func (Status) Type() protoreflect.EnumType { 63 | return &file_testproto_proto_enumTypes[0] 64 | } 65 | 66 | func (x Status) Number() protoreflect.EnumNumber { 67 | return protoreflect.EnumNumber(x) 68 | } 69 | 70 | // Deprecated: Use Status.Descriptor instead. 71 | func (Status) EnumDescriptor() ([]byte, []int) { 72 | return file_testproto_proto_rawDescGZIP(), []int{0} 73 | } 74 | 75 | type User struct { 76 | state protoimpl.MessageState `protogen:"open.v1"` 77 | UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` 78 | Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 79 | unknownFields protoimpl.UnknownFields 80 | sizeCache protoimpl.SizeCache 81 | } 82 | 83 | func (x *User) Reset() { 84 | *x = User{} 85 | mi := &file_testproto_proto_msgTypes[0] 86 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 87 | ms.StoreMessageInfo(mi) 88 | } 89 | 90 | func (x *User) String() string { 91 | return protoimpl.X.MessageStringOf(x) 92 | } 93 | 94 | func (*User) ProtoMessage() {} 95 | 96 | func (x *User) ProtoReflect() protoreflect.Message { 97 | mi := &file_testproto_proto_msgTypes[0] 98 | if x != nil { 99 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 100 | if ms.LoadMessageInfo() == nil { 101 | ms.StoreMessageInfo(mi) 102 | } 103 | return ms 104 | } 105 | return mi.MessageOf(x) 106 | } 107 | 108 | // Deprecated: Use User.ProtoReflect.Descriptor instead. 109 | func (*User) Descriptor() ([]byte, []int) { 110 | return file_testproto_proto_rawDescGZIP(), []int{0} 111 | } 112 | 113 | func (x *User) GetUserId() int64 { 114 | if x != nil { 115 | return x.UserId 116 | } 117 | return 0 118 | } 119 | 120 | func (x *User) GetName() string { 121 | if x != nil { 122 | return x.Name 123 | } 124 | return "" 125 | } 126 | 127 | type Photo struct { 128 | state protoimpl.MessageState `protogen:"open.v1"` 129 | PhotoId int64 `protobuf:"varint,1,opt,name=photo_id,json=photoId,proto3" json:"photo_id,omitempty"` 130 | Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` 131 | Dimensions *Dimensions `protobuf:"bytes,3,opt,name=dimensions,proto3" json:"dimensions,omitempty"` 132 | unknownFields protoimpl.UnknownFields 133 | sizeCache protoimpl.SizeCache 134 | } 135 | 136 | func (x *Photo) Reset() { 137 | *x = Photo{} 138 | mi := &file_testproto_proto_msgTypes[1] 139 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 140 | ms.StoreMessageInfo(mi) 141 | } 142 | 143 | func (x *Photo) String() string { 144 | return protoimpl.X.MessageStringOf(x) 145 | } 146 | 147 | func (*Photo) ProtoMessage() {} 148 | 149 | func (x *Photo) ProtoReflect() protoreflect.Message { 150 | mi := &file_testproto_proto_msgTypes[1] 151 | if x != nil { 152 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 153 | if ms.LoadMessageInfo() == nil { 154 | ms.StoreMessageInfo(mi) 155 | } 156 | return ms 157 | } 158 | return mi.MessageOf(x) 159 | } 160 | 161 | // Deprecated: Use Photo.ProtoReflect.Descriptor instead. 162 | func (*Photo) Descriptor() ([]byte, []int) { 163 | return file_testproto_proto_rawDescGZIP(), []int{1} 164 | } 165 | 166 | func (x *Photo) GetPhotoId() int64 { 167 | if x != nil { 168 | return x.PhotoId 169 | } 170 | return 0 171 | } 172 | 173 | func (x *Photo) GetPath() string { 174 | if x != nil { 175 | return x.Path 176 | } 177 | return "" 178 | } 179 | 180 | func (x *Photo) GetDimensions() *Dimensions { 181 | if x != nil { 182 | return x.Dimensions 183 | } 184 | return nil 185 | } 186 | 187 | type Dimensions struct { 188 | state protoimpl.MessageState `protogen:"open.v1"` 189 | Width int32 `protobuf:"varint,1,opt,name=width,proto3" json:"width,omitempty"` 190 | Height int32 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"` 191 | unknownFields protoimpl.UnknownFields 192 | sizeCache protoimpl.SizeCache 193 | } 194 | 195 | func (x *Dimensions) Reset() { 196 | *x = Dimensions{} 197 | mi := &file_testproto_proto_msgTypes[2] 198 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 199 | ms.StoreMessageInfo(mi) 200 | } 201 | 202 | func (x *Dimensions) String() string { 203 | return protoimpl.X.MessageStringOf(x) 204 | } 205 | 206 | func (*Dimensions) ProtoMessage() {} 207 | 208 | func (x *Dimensions) ProtoReflect() protoreflect.Message { 209 | mi := &file_testproto_proto_msgTypes[2] 210 | if x != nil { 211 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 212 | if ms.LoadMessageInfo() == nil { 213 | ms.StoreMessageInfo(mi) 214 | } 215 | return ms 216 | } 217 | return mi.MessageOf(x) 218 | } 219 | 220 | // Deprecated: Use Dimensions.ProtoReflect.Descriptor instead. 221 | func (*Dimensions) Descriptor() ([]byte, []int) { 222 | return file_testproto_proto_rawDescGZIP(), []int{2} 223 | } 224 | 225 | func (x *Dimensions) GetWidth() int32 { 226 | if x != nil { 227 | return x.Width 228 | } 229 | return 0 230 | } 231 | 232 | func (x *Dimensions) GetHeight() int32 { 233 | if x != nil { 234 | return x.Height 235 | } 236 | return 0 237 | } 238 | 239 | type Attribute struct { 240 | state protoimpl.MessageState `protogen:"open.v1"` 241 | Tags map[string]string `protobuf:"bytes,1,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` 242 | unknownFields protoimpl.UnknownFields 243 | sizeCache protoimpl.SizeCache 244 | } 245 | 246 | func (x *Attribute) Reset() { 247 | *x = Attribute{} 248 | mi := &file_testproto_proto_msgTypes[3] 249 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 250 | ms.StoreMessageInfo(mi) 251 | } 252 | 253 | func (x *Attribute) String() string { 254 | return protoimpl.X.MessageStringOf(x) 255 | } 256 | 257 | func (*Attribute) ProtoMessage() {} 258 | 259 | func (x *Attribute) ProtoReflect() protoreflect.Message { 260 | mi := &file_testproto_proto_msgTypes[3] 261 | if x != nil { 262 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 263 | if ms.LoadMessageInfo() == nil { 264 | ms.StoreMessageInfo(mi) 265 | } 266 | return ms 267 | } 268 | return mi.MessageOf(x) 269 | } 270 | 271 | // Deprecated: Use Attribute.ProtoReflect.Descriptor instead. 272 | func (*Attribute) Descriptor() ([]byte, []int) { 273 | return file_testproto_proto_rawDescGZIP(), []int{3} 274 | } 275 | 276 | func (x *Attribute) GetTags() map[string]string { 277 | if x != nil { 278 | return x.Tags 279 | } 280 | return nil 281 | } 282 | 283 | type Address struct { 284 | state protoimpl.MessageState `protogen:"open.v1"` 285 | Number string `protobuf:"bytes,1,opt,name=number,proto3" json:"number,omitempty"` 286 | Street string `protobuf:"bytes,2,opt,name=street,proto3" json:"street,omitempty"` 287 | PostalCode string `protobuf:"bytes,3,opt,name=postal_code,json=postalCode,proto3" json:"postal_code,omitempty"` 288 | unknownFields protoimpl.UnknownFields 289 | sizeCache protoimpl.SizeCache 290 | } 291 | 292 | func (x *Address) Reset() { 293 | *x = Address{} 294 | mi := &file_testproto_proto_msgTypes[4] 295 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 296 | ms.StoreMessageInfo(mi) 297 | } 298 | 299 | func (x *Address) String() string { 300 | return protoimpl.X.MessageStringOf(x) 301 | } 302 | 303 | func (*Address) ProtoMessage() {} 304 | 305 | func (x *Address) ProtoReflect() protoreflect.Message { 306 | mi := &file_testproto_proto_msgTypes[4] 307 | if x != nil { 308 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 309 | if ms.LoadMessageInfo() == nil { 310 | ms.StoreMessageInfo(mi) 311 | } 312 | return ms 313 | } 314 | return mi.MessageOf(x) 315 | } 316 | 317 | // Deprecated: Use Address.ProtoReflect.Descriptor instead. 318 | func (*Address) Descriptor() ([]byte, []int) { 319 | return file_testproto_proto_rawDescGZIP(), []int{4} 320 | } 321 | 322 | func (x *Address) GetNumber() string { 323 | if x != nil { 324 | return x.Number 325 | } 326 | return "" 327 | } 328 | 329 | func (x *Address) GetStreet() string { 330 | if x != nil { 331 | return x.Street 332 | } 333 | return "" 334 | } 335 | 336 | func (x *Address) GetPostalCode() string { 337 | if x != nil { 338 | return x.PostalCode 339 | } 340 | return "" 341 | } 342 | 343 | type Options struct { 344 | state protoimpl.MessageState `protogen:"open.v1"` 345 | OptionalString *string `protobuf:"bytes,1,opt,name=optional_string,json=optionalString,proto3,oneof" json:"optional_string,omitempty"` 346 | OptionalInt *int32 `protobuf:"varint,2,opt,name=optional_int,json=optionalInt,proto3,oneof" json:"optional_int,omitempty"` 347 | OptionalPhoto *Photo `protobuf:"bytes,3,opt,name=optional_photo,json=optionalPhoto,proto3,oneof" json:"optional_photo,omitempty"` 348 | OptionalAttr *Attribute `protobuf:"bytes,4,opt,name=optional_attr,json=optionalAttr,proto3,oneof" json:"optional_attr,omitempty"` 349 | unknownFields protoimpl.UnknownFields 350 | sizeCache protoimpl.SizeCache 351 | } 352 | 353 | func (x *Options) Reset() { 354 | *x = Options{} 355 | mi := &file_testproto_proto_msgTypes[5] 356 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 357 | ms.StoreMessageInfo(mi) 358 | } 359 | 360 | func (x *Options) String() string { 361 | return protoimpl.X.MessageStringOf(x) 362 | } 363 | 364 | func (*Options) ProtoMessage() {} 365 | 366 | func (x *Options) ProtoReflect() protoreflect.Message { 367 | mi := &file_testproto_proto_msgTypes[5] 368 | if x != nil { 369 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 370 | if ms.LoadMessageInfo() == nil { 371 | ms.StoreMessageInfo(mi) 372 | } 373 | return ms 374 | } 375 | return mi.MessageOf(x) 376 | } 377 | 378 | // Deprecated: Use Options.ProtoReflect.Descriptor instead. 379 | func (*Options) Descriptor() ([]byte, []int) { 380 | return file_testproto_proto_rawDescGZIP(), []int{5} 381 | } 382 | 383 | func (x *Options) GetOptionalString() string { 384 | if x != nil && x.OptionalString != nil { 385 | return *x.OptionalString 386 | } 387 | return "" 388 | } 389 | 390 | func (x *Options) GetOptionalInt() int32 { 391 | if x != nil && x.OptionalInt != nil { 392 | return *x.OptionalInt 393 | } 394 | return 0 395 | } 396 | 397 | func (x *Options) GetOptionalPhoto() *Photo { 398 | if x != nil { 399 | return x.OptionalPhoto 400 | } 401 | return nil 402 | } 403 | 404 | func (x *Options) GetOptionalAttr() *Attribute { 405 | if x != nil { 406 | return x.OptionalAttr 407 | } 408 | return nil 409 | } 410 | 411 | type Profile struct { 412 | state protoimpl.MessageState `protogen:"open.v1"` 413 | User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` 414 | Photo *Photo `protobuf:"bytes,2,opt,name=photo,proto3" json:"photo,omitempty"` 415 | LoginTimestamps []int64 `protobuf:"varint,3,rep,packed,name=login_timestamps,json=loginTimestamps,proto3" json:"login_timestamps,omitempty"` 416 | Gallery []*Photo `protobuf:"bytes,4,rep,name=gallery,proto3" json:"gallery,omitempty"` 417 | Attributes map[string]*Attribute `protobuf:"bytes,5,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` 418 | AddressesByName map[string]*Address `protobuf:"bytes,6,rep,name=addresses_by_name,json=addressesByName,proto3" json:"addresses_by_name,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` 419 | unknownFields protoimpl.UnknownFields 420 | sizeCache protoimpl.SizeCache 421 | } 422 | 423 | func (x *Profile) Reset() { 424 | *x = Profile{} 425 | mi := &file_testproto_proto_msgTypes[6] 426 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 427 | ms.StoreMessageInfo(mi) 428 | } 429 | 430 | func (x *Profile) String() string { 431 | return protoimpl.X.MessageStringOf(x) 432 | } 433 | 434 | func (*Profile) ProtoMessage() {} 435 | 436 | func (x *Profile) ProtoReflect() protoreflect.Message { 437 | mi := &file_testproto_proto_msgTypes[6] 438 | if x != nil { 439 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 440 | if ms.LoadMessageInfo() == nil { 441 | ms.StoreMessageInfo(mi) 442 | } 443 | return ms 444 | } 445 | return mi.MessageOf(x) 446 | } 447 | 448 | // Deprecated: Use Profile.ProtoReflect.Descriptor instead. 449 | func (*Profile) Descriptor() ([]byte, []int) { 450 | return file_testproto_proto_rawDescGZIP(), []int{6} 451 | } 452 | 453 | func (x *Profile) GetUser() *User { 454 | if x != nil { 455 | return x.User 456 | } 457 | return nil 458 | } 459 | 460 | func (x *Profile) GetPhoto() *Photo { 461 | if x != nil { 462 | return x.Photo 463 | } 464 | return nil 465 | } 466 | 467 | func (x *Profile) GetLoginTimestamps() []int64 { 468 | if x != nil { 469 | return x.LoginTimestamps 470 | } 471 | return nil 472 | } 473 | 474 | func (x *Profile) GetGallery() []*Photo { 475 | if x != nil { 476 | return x.Gallery 477 | } 478 | return nil 479 | } 480 | 481 | func (x *Profile) GetAttributes() map[string]*Attribute { 482 | if x != nil { 483 | return x.Attributes 484 | } 485 | return nil 486 | } 487 | 488 | func (x *Profile) GetAddressesByName() map[string]*Address { 489 | if x != nil { 490 | return x.AddressesByName 491 | } 492 | return nil 493 | } 494 | 495 | type UpdateProfileRequest struct { 496 | state protoimpl.MessageState `protogen:"open.v1"` 497 | Profile *Profile `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` 498 | Fieldmask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=fieldmask,proto3" json:"fieldmask,omitempty"` 499 | unknownFields protoimpl.UnknownFields 500 | sizeCache protoimpl.SizeCache 501 | } 502 | 503 | func (x *UpdateProfileRequest) Reset() { 504 | *x = UpdateProfileRequest{} 505 | mi := &file_testproto_proto_msgTypes[7] 506 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 507 | ms.StoreMessageInfo(mi) 508 | } 509 | 510 | func (x *UpdateProfileRequest) String() string { 511 | return protoimpl.X.MessageStringOf(x) 512 | } 513 | 514 | func (*UpdateProfileRequest) ProtoMessage() {} 515 | 516 | func (x *UpdateProfileRequest) ProtoReflect() protoreflect.Message { 517 | mi := &file_testproto_proto_msgTypes[7] 518 | if x != nil { 519 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 520 | if ms.LoadMessageInfo() == nil { 521 | ms.StoreMessageInfo(mi) 522 | } 523 | return ms 524 | } 525 | return mi.MessageOf(x) 526 | } 527 | 528 | // Deprecated: Use UpdateProfileRequest.ProtoReflect.Descriptor instead. 529 | func (*UpdateProfileRequest) Descriptor() ([]byte, []int) { 530 | return file_testproto_proto_rawDescGZIP(), []int{7} 531 | } 532 | 533 | func (x *UpdateProfileRequest) GetProfile() *Profile { 534 | if x != nil { 535 | return x.Profile 536 | } 537 | return nil 538 | } 539 | 540 | func (x *UpdateProfileRequest) GetFieldmask() *fieldmaskpb.FieldMask { 541 | if x != nil { 542 | return x.Fieldmask 543 | } 544 | return nil 545 | } 546 | 547 | type Result struct { 548 | state protoimpl.MessageState `protogen:"open.v1"` 549 | Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` 550 | NextToken int64 `protobuf:"varint,2,opt,name=next_token,json=nextToken,proto3" json:"next_token,omitempty"` 551 | unknownFields protoimpl.UnknownFields 552 | sizeCache protoimpl.SizeCache 553 | } 554 | 555 | func (x *Result) Reset() { 556 | *x = Result{} 557 | mi := &file_testproto_proto_msgTypes[8] 558 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 559 | ms.StoreMessageInfo(mi) 560 | } 561 | 562 | func (x *Result) String() string { 563 | return protoimpl.X.MessageStringOf(x) 564 | } 565 | 566 | func (*Result) ProtoMessage() {} 567 | 568 | func (x *Result) ProtoReflect() protoreflect.Message { 569 | mi := &file_testproto_proto_msgTypes[8] 570 | if x != nil { 571 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 572 | if ms.LoadMessageInfo() == nil { 573 | ms.StoreMessageInfo(mi) 574 | } 575 | return ms 576 | } 577 | return mi.MessageOf(x) 578 | } 579 | 580 | // Deprecated: Use Result.ProtoReflect.Descriptor instead. 581 | func (*Result) Descriptor() ([]byte, []int) { 582 | return file_testproto_proto_rawDescGZIP(), []int{8} 583 | } 584 | 585 | func (x *Result) GetData() []byte { 586 | if x != nil { 587 | return x.Data 588 | } 589 | return nil 590 | } 591 | 592 | func (x *Result) GetNextToken() int64 { 593 | if x != nil { 594 | return x.NextToken 595 | } 596 | return 0 597 | } 598 | 599 | type Event struct { 600 | state protoimpl.MessageState `protogen:"open.v1"` 601 | EventId int64 `protobuf:"varint,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` 602 | // Types that are valid to be assigned to Changed: 603 | // 604 | // *Event_User 605 | // *Event_Photo 606 | // *Event_Status 607 | // *Event_Details 608 | // *Event_Profile 609 | Changed isEvent_Changed `protobuf_oneof:"changed"` 610 | unknownFields protoimpl.UnknownFields 611 | sizeCache protoimpl.SizeCache 612 | } 613 | 614 | func (x *Event) Reset() { 615 | *x = Event{} 616 | mi := &file_testproto_proto_msgTypes[9] 617 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 618 | ms.StoreMessageInfo(mi) 619 | } 620 | 621 | func (x *Event) String() string { 622 | return protoimpl.X.MessageStringOf(x) 623 | } 624 | 625 | func (*Event) ProtoMessage() {} 626 | 627 | func (x *Event) ProtoReflect() protoreflect.Message { 628 | mi := &file_testproto_proto_msgTypes[9] 629 | if x != nil { 630 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 631 | if ms.LoadMessageInfo() == nil { 632 | ms.StoreMessageInfo(mi) 633 | } 634 | return ms 635 | } 636 | return mi.MessageOf(x) 637 | } 638 | 639 | // Deprecated: Use Event.ProtoReflect.Descriptor instead. 640 | func (*Event) Descriptor() ([]byte, []int) { 641 | return file_testproto_proto_rawDescGZIP(), []int{9} 642 | } 643 | 644 | func (x *Event) GetEventId() int64 { 645 | if x != nil { 646 | return x.EventId 647 | } 648 | return 0 649 | } 650 | 651 | func (x *Event) GetChanged() isEvent_Changed { 652 | if x != nil { 653 | return x.Changed 654 | } 655 | return nil 656 | } 657 | 658 | func (x *Event) GetUser() *User { 659 | if x != nil { 660 | if x, ok := x.Changed.(*Event_User); ok { 661 | return x.User 662 | } 663 | } 664 | return nil 665 | } 666 | 667 | func (x *Event) GetPhoto() *Photo { 668 | if x != nil { 669 | if x, ok := x.Changed.(*Event_Photo); ok { 670 | return x.Photo 671 | } 672 | } 673 | return nil 674 | } 675 | 676 | func (x *Event) GetStatus() Status { 677 | if x != nil { 678 | if x, ok := x.Changed.(*Event_Status); ok { 679 | return x.Status 680 | } 681 | } 682 | return Status_UNKNOWN 683 | } 684 | 685 | func (x *Event) GetDetails() *anypb.Any { 686 | if x != nil { 687 | if x, ok := x.Changed.(*Event_Details); ok { 688 | return x.Details 689 | } 690 | } 691 | return nil 692 | } 693 | 694 | func (x *Event) GetProfile() *Profile { 695 | if x != nil { 696 | if x, ok := x.Changed.(*Event_Profile); ok { 697 | return x.Profile 698 | } 699 | } 700 | return nil 701 | } 702 | 703 | type isEvent_Changed interface { 704 | isEvent_Changed() 705 | } 706 | 707 | type Event_User struct { 708 | User *User `protobuf:"bytes,2,opt,name=user,proto3,oneof"` 709 | } 710 | 711 | type Event_Photo struct { 712 | Photo *Photo `protobuf:"bytes,3,opt,name=photo,proto3,oneof"` 713 | } 714 | 715 | type Event_Status struct { 716 | Status Status `protobuf:"varint,4,opt,name=status,proto3,enum=testproto.Status,oneof"` 717 | } 718 | 719 | type Event_Details struct { 720 | Details *anypb.Any `protobuf:"bytes,5,opt,name=details,proto3,oneof"` 721 | } 722 | 723 | type Event_Profile struct { 724 | Profile *Profile `protobuf:"bytes,6,opt,name=profile,proto3,oneof"` 725 | } 726 | 727 | func (*Event_User) isEvent_Changed() {} 728 | 729 | func (*Event_Photo) isEvent_Changed() {} 730 | 731 | func (*Event_Status) isEvent_Changed() {} 732 | 733 | func (*Event_Details) isEvent_Changed() {} 734 | 735 | func (*Event_Profile) isEvent_Changed() {} 736 | 737 | var File_testproto_proto protoreflect.FileDescriptor 738 | 739 | const file_testproto_proto_rawDesc = "" + 740 | "\n" + 741 | "\x0ftestproto.proto\x12\ttestproto\x1a\x19google/protobuf/any.proto\x1a google/protobuf/field_mask.proto\"3\n" + 742 | "\x04User\x12\x17\n" + 743 | "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x12\n" + 744 | "\x04name\x18\x02 \x01(\tR\x04name\"m\n" + 745 | "\x05Photo\x12\x19\n" + 746 | "\bphoto_id\x18\x01 \x01(\x03R\aphotoId\x12\x12\n" + 747 | "\x04path\x18\x02 \x01(\tR\x04path\x125\n" + 748 | "\n" + 749 | "dimensions\x18\x03 \x01(\v2\x15.testproto.DimensionsR\n" + 750 | "dimensions\":\n" + 751 | "\n" + 752 | "Dimensions\x12\x14\n" + 753 | "\x05width\x18\x01 \x01(\x05R\x05width\x12\x16\n" + 754 | "\x06height\x18\x02 \x01(\x05R\x06height\"x\n" + 755 | "\tAttribute\x122\n" + 756 | "\x04tags\x18\x01 \x03(\v2\x1e.testproto.Attribute.TagsEntryR\x04tags\x1a7\n" + 757 | "\tTagsEntry\x12\x10\n" + 758 | "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + 759 | "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"Z\n" + 760 | "\aAddress\x12\x16\n" + 761 | "\x06number\x18\x01 \x01(\tR\x06number\x12\x16\n" + 762 | "\x06street\x18\x02 \x01(\tR\x06street\x12\x1f\n" + 763 | "\vpostal_code\x18\x03 \x01(\tR\n" + 764 | "postalCode\"\xa7\x02\n" + 765 | "\aOptions\x12,\n" + 766 | "\x0foptional_string\x18\x01 \x01(\tH\x00R\x0eoptionalString\x88\x01\x01\x12&\n" + 767 | "\foptional_int\x18\x02 \x01(\x05H\x01R\voptionalInt\x88\x01\x01\x12<\n" + 768 | "\x0eoptional_photo\x18\x03 \x01(\v2\x10.testproto.PhotoH\x02R\roptionalPhoto\x88\x01\x01\x12>\n" + 769 | "\roptional_attr\x18\x04 \x01(\v2\x14.testproto.AttributeH\x03R\foptionalAttr\x88\x01\x01B\x12\n" + 770 | "\x10_optional_stringB\x0f\n" + 771 | "\r_optional_intB\x11\n" + 772 | "\x0f_optional_photoB\x10\n" + 773 | "\x0e_optional_attr\"\xf3\x03\n" + 774 | "\aProfile\x12#\n" + 775 | "\x04user\x18\x01 \x01(\v2\x0f.testproto.UserR\x04user\x12&\n" + 776 | "\x05photo\x18\x02 \x01(\v2\x10.testproto.PhotoR\x05photo\x12)\n" + 777 | "\x10login_timestamps\x18\x03 \x03(\x03R\x0floginTimestamps\x12*\n" + 778 | "\agallery\x18\x04 \x03(\v2\x10.testproto.PhotoR\agallery\x12B\n" + 779 | "\n" + 780 | "attributes\x18\x05 \x03(\v2\".testproto.Profile.AttributesEntryR\n" + 781 | "attributes\x12S\n" + 782 | "\x11addresses_by_name\x18\x06 \x03(\v2'.testproto.Profile.AddressesByNameEntryR\x0faddressesByName\x1aS\n" + 783 | "\x0fAttributesEntry\x12\x10\n" + 784 | "\x03key\x18\x01 \x01(\tR\x03key\x12*\n" + 785 | "\x05value\x18\x02 \x01(\v2\x14.testproto.AttributeR\x05value:\x028\x01\x1aV\n" + 786 | "\x14AddressesByNameEntry\x12\x10\n" + 787 | "\x03key\x18\x01 \x01(\tR\x03key\x12(\n" + 788 | "\x05value\x18\x02 \x01(\v2\x12.testproto.AddressR\x05value:\x028\x01\"~\n" + 789 | "\x14UpdateProfileRequest\x12,\n" + 790 | "\aprofile\x18\x01 \x01(\v2\x12.testproto.ProfileR\aprofile\x128\n" + 791 | "\tfieldmask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskR\tfieldmask\";\n" + 792 | "\x06Result\x12\x12\n" + 793 | "\x04data\x18\x01 \x01(\fR\x04data\x12\x1d\n" + 794 | "\n" + 795 | "next_token\x18\x02 \x01(\x03R\tnextToken\"\x8d\x02\n" + 796 | "\x05Event\x12\x19\n" + 797 | "\bevent_id\x18\x01 \x01(\x03R\aeventId\x12%\n" + 798 | "\x04user\x18\x02 \x01(\v2\x0f.testproto.UserH\x00R\x04user\x12(\n" + 799 | "\x05photo\x18\x03 \x01(\v2\x10.testproto.PhotoH\x00R\x05photo\x12+\n" + 800 | "\x06status\x18\x04 \x01(\x0e2\x11.testproto.StatusH\x00R\x06status\x120\n" + 801 | "\adetails\x18\x05 \x01(\v2\x14.google.protobuf.AnyH\x00R\adetails\x12.\n" + 802 | "\aprofile\x18\x06 \x01(\v2\x12.testproto.ProfileH\x00R\aprofileB\t\n" + 803 | "\achanged*)\n" + 804 | "\x06Status\x12\v\n" + 805 | "\aUNKNOWN\x10\x00\x12\x06\n" + 806 | "\x02OK\x10\x01\x12\n" + 807 | "\n" + 808 | "\x06FAILED\x10\x02B1Z/github.com/mennanov/fmutils/testproto;testprotob\x06proto3" 809 | 810 | var ( 811 | file_testproto_proto_rawDescOnce sync.Once 812 | file_testproto_proto_rawDescData []byte 813 | ) 814 | 815 | func file_testproto_proto_rawDescGZIP() []byte { 816 | file_testproto_proto_rawDescOnce.Do(func() { 817 | file_testproto_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_testproto_proto_rawDesc), len(file_testproto_proto_rawDesc))) 818 | }) 819 | return file_testproto_proto_rawDescData 820 | } 821 | 822 | var file_testproto_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 823 | var file_testproto_proto_msgTypes = make([]protoimpl.MessageInfo, 13) 824 | var file_testproto_proto_goTypes = []any{ 825 | (Status)(0), // 0: testproto.Status 826 | (*User)(nil), // 1: testproto.User 827 | (*Photo)(nil), // 2: testproto.Photo 828 | (*Dimensions)(nil), // 3: testproto.Dimensions 829 | (*Attribute)(nil), // 4: testproto.Attribute 830 | (*Address)(nil), // 5: testproto.Address 831 | (*Options)(nil), // 6: testproto.Options 832 | (*Profile)(nil), // 7: testproto.Profile 833 | (*UpdateProfileRequest)(nil), // 8: testproto.UpdateProfileRequest 834 | (*Result)(nil), // 9: testproto.Result 835 | (*Event)(nil), // 10: testproto.Event 836 | nil, // 11: testproto.Attribute.TagsEntry 837 | nil, // 12: testproto.Profile.AttributesEntry 838 | nil, // 13: testproto.Profile.AddressesByNameEntry 839 | (*fieldmaskpb.FieldMask)(nil), // 14: google.protobuf.FieldMask 840 | (*anypb.Any)(nil), // 15: google.protobuf.Any 841 | } 842 | var file_testproto_proto_depIdxs = []int32{ 843 | 3, // 0: testproto.Photo.dimensions:type_name -> testproto.Dimensions 844 | 11, // 1: testproto.Attribute.tags:type_name -> testproto.Attribute.TagsEntry 845 | 2, // 2: testproto.Options.optional_photo:type_name -> testproto.Photo 846 | 4, // 3: testproto.Options.optional_attr:type_name -> testproto.Attribute 847 | 1, // 4: testproto.Profile.user:type_name -> testproto.User 848 | 2, // 5: testproto.Profile.photo:type_name -> testproto.Photo 849 | 2, // 6: testproto.Profile.gallery:type_name -> testproto.Photo 850 | 12, // 7: testproto.Profile.attributes:type_name -> testproto.Profile.AttributesEntry 851 | 13, // 8: testproto.Profile.addresses_by_name:type_name -> testproto.Profile.AddressesByNameEntry 852 | 7, // 9: testproto.UpdateProfileRequest.profile:type_name -> testproto.Profile 853 | 14, // 10: testproto.UpdateProfileRequest.fieldmask:type_name -> google.protobuf.FieldMask 854 | 1, // 11: testproto.Event.user:type_name -> testproto.User 855 | 2, // 12: testproto.Event.photo:type_name -> testproto.Photo 856 | 0, // 13: testproto.Event.status:type_name -> testproto.Status 857 | 15, // 14: testproto.Event.details:type_name -> google.protobuf.Any 858 | 7, // 15: testproto.Event.profile:type_name -> testproto.Profile 859 | 4, // 16: testproto.Profile.AttributesEntry.value:type_name -> testproto.Attribute 860 | 5, // 17: testproto.Profile.AddressesByNameEntry.value:type_name -> testproto.Address 861 | 18, // [18:18] is the sub-list for method output_type 862 | 18, // [18:18] is the sub-list for method input_type 863 | 18, // [18:18] is the sub-list for extension type_name 864 | 18, // [18:18] is the sub-list for extension extendee 865 | 0, // [0:18] is the sub-list for field type_name 866 | } 867 | 868 | func init() { file_testproto_proto_init() } 869 | func file_testproto_proto_init() { 870 | if File_testproto_proto != nil { 871 | return 872 | } 873 | file_testproto_proto_msgTypes[5].OneofWrappers = []any{} 874 | file_testproto_proto_msgTypes[9].OneofWrappers = []any{ 875 | (*Event_User)(nil), 876 | (*Event_Photo)(nil), 877 | (*Event_Status)(nil), 878 | (*Event_Details)(nil), 879 | (*Event_Profile)(nil), 880 | } 881 | type x struct{} 882 | out := protoimpl.TypeBuilder{ 883 | File: protoimpl.DescBuilder{ 884 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 885 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_testproto_proto_rawDesc), len(file_testproto_proto_rawDesc)), 886 | NumEnums: 1, 887 | NumMessages: 13, 888 | NumExtensions: 0, 889 | NumServices: 0, 890 | }, 891 | GoTypes: file_testproto_proto_goTypes, 892 | DependencyIndexes: file_testproto_proto_depIdxs, 893 | EnumInfos: file_testproto_proto_enumTypes, 894 | MessageInfos: file_testproto_proto_msgTypes, 895 | }.Build() 896 | File_testproto_proto = out.File 897 | file_testproto_proto_goTypes = nil 898 | file_testproto_proto_depIdxs = nil 899 | } 900 | -------------------------------------------------------------------------------- /fmutils_test.go: -------------------------------------------------------------------------------- 1 | package fmutils 2 | 3 | import ( 4 | "reflect" 5 | "slices" 6 | "testing" 7 | 8 | "google.golang.org/protobuf/proto" 9 | "google.golang.org/protobuf/types/known/anypb" 10 | 11 | "github.com/mennanov/fmutils/testproto" 12 | ) 13 | 14 | func Test_NestedMaskFromPaths(t *testing.T) { 15 | type args struct { 16 | paths []string 17 | } 18 | tests := []struct { 19 | name string 20 | args args 21 | want NestedMask 22 | }{ 23 | { 24 | name: "no nested fields", 25 | args: args{paths: []string{"a", "b", "c"}}, 26 | want: NestedMask{"a": nil, "b": nil, "c": nil}, 27 | }, 28 | { 29 | name: "with nested fields", 30 | args: args{paths: []string{"aaa.bb.c", "dd.e", "f"}}, 31 | want: NestedMask{ 32 | "aaa": NestedMask{"bb": NestedMask{"c": nil}}, 33 | "dd": NestedMask{"e": nil}, 34 | "f": nil}, 35 | }, 36 | { 37 | name: "single field", 38 | args: args{paths: []string{"a"}}, 39 | want: NestedMask{"a": nil}, 40 | }, 41 | { 42 | name: "empty fields", 43 | args: args{paths: []string{}}, 44 | want: NestedMask{}, 45 | }, 46 | { 47 | name: "invalid input", 48 | args: args{paths: []string{".", "..", "...", ".a.", ""}}, 49 | want: NestedMask{}, 50 | }, 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | if got := NestedMaskFromPaths(tt.args.paths); !reflect.DeepEqual(got, tt.want) { 55 | t.Errorf("NestedMaskFromPaths() = %v, want %v", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func createAny(m proto.Message) *anypb.Any { 62 | any, err := anypb.New(m) 63 | if err != nil { 64 | panic(err) 65 | } 66 | return any 67 | } 68 | 69 | func TestFilter(t *testing.T) { 70 | tests := []struct { 71 | name string 72 | paths []string 73 | msg proto.Message 74 | want proto.Message 75 | }{ 76 | { 77 | name: "empty mask keeps all the fields", 78 | paths: []string{}, 79 | msg: &testproto.Profile{ 80 | User: &testproto.User{ 81 | UserId: 1, 82 | Name: "user name", 83 | }, 84 | Photo: &testproto.Photo{ 85 | PhotoId: 2, 86 | Path: "photo path", 87 | Dimensions: &testproto.Dimensions{ 88 | Width: 100, 89 | Height: 120, 90 | }, 91 | }, 92 | LoginTimestamps: []int64{1, 2}, 93 | }, 94 | want: &testproto.Profile{ 95 | User: &testproto.User{ 96 | UserId: 1, 97 | Name: "user name", 98 | }, 99 | Photo: &testproto.Photo{ 100 | PhotoId: 2, 101 | Path: "photo path", 102 | Dimensions: &testproto.Dimensions{ 103 | Width: 100, 104 | Height: 120, 105 | }, 106 | }, 107 | LoginTimestamps: []int64{1, 2}, 108 | }, 109 | }, 110 | { 111 | name: "path with empty string is ignored", 112 | paths: []string{""}, 113 | msg: &testproto.Profile{ 114 | User: &testproto.User{ 115 | UserId: 1, 116 | Name: "user name", 117 | }, 118 | }, 119 | want: &testproto.Profile{ 120 | User: &testproto.User{ 121 | UserId: 1, 122 | Name: "user name", 123 | }, 124 | }, 125 | }, 126 | { 127 | name: "mask with all root fields keeps all root fields", 128 | paths: []string{"user", "photo"}, 129 | msg: &testproto.Profile{ 130 | User: &testproto.User{ 131 | UserId: 1, 132 | Name: "user name", 133 | }, 134 | Photo: &testproto.Photo{ 135 | PhotoId: 2, 136 | Path: "photo path", 137 | Dimensions: &testproto.Dimensions{ 138 | Width: 100, 139 | Height: 120, 140 | }, 141 | }, 142 | }, 143 | want: &testproto.Profile{ 144 | User: &testproto.User{ 145 | UserId: 1, 146 | Name: "user name", 147 | }, 148 | Photo: &testproto.Photo{ 149 | PhotoId: 2, 150 | Path: "photo path", 151 | Dimensions: &testproto.Dimensions{ 152 | Width: 100, 153 | Height: 120, 154 | }, 155 | }, 156 | }, 157 | }, 158 | { 159 | name: "mask with single root field keeps that field only", 160 | paths: []string{"user"}, 161 | msg: &testproto.Profile{ 162 | User: &testproto.User{ 163 | UserId: 1, 164 | Name: "user name", 165 | }, 166 | Photo: &testproto.Photo{ 167 | PhotoId: 2, 168 | Path: "photo path", 169 | Dimensions: &testproto.Dimensions{ 170 | Width: 100, 171 | Height: 120, 172 | }, 173 | }, 174 | }, 175 | want: &testproto.Profile{ 176 | User: &testproto.User{ 177 | UserId: 1, 178 | Name: "user name", 179 | }, 180 | }, 181 | }, 182 | { 183 | name: "mask with nested fields keeps the listed fields only", 184 | paths: []string{"user.name", "photo.path", "photo.dimensions.width"}, 185 | msg: &testproto.Profile{ 186 | User: &testproto.User{ 187 | UserId: 1, 188 | Name: "user name", 189 | }, 190 | Photo: &testproto.Photo{ 191 | PhotoId: 2, 192 | Path: "photo path", 193 | Dimensions: &testproto.Dimensions{ 194 | Width: 100, 195 | Height: 120, 196 | }, 197 | }, 198 | }, 199 | want: &testproto.Profile{ 200 | User: &testproto.User{ 201 | Name: "user name", 202 | }, 203 | Photo: &testproto.Photo{ 204 | Path: "photo path", 205 | Dimensions: &testproto.Dimensions{ 206 | Width: 100, 207 | }, 208 | }, 209 | }, 210 | }, 211 | { 212 | name: "mask with oneof field keeps the entire field", 213 | paths: []string{"user"}, 214 | msg: &testproto.Event{ 215 | EventId: 1, 216 | Changed: &testproto.Event_User{User: &testproto.User{ 217 | UserId: 1, 218 | Name: "user name", 219 | }}, 220 | }, 221 | want: &testproto.Event{ 222 | Changed: &testproto.Event_User{User: &testproto.User{ 223 | UserId: 1, 224 | Name: "user name", 225 | }}, 226 | }, 227 | }, 228 | { 229 | name: "mask with nested oneof fields keeps listed fields only", 230 | paths: []string{"profile.photo.dimensions", "profile.user.user_id", "profile.login_timestamps"}, 231 | msg: &testproto.Event{ 232 | EventId: 1, 233 | Changed: &testproto.Event_Profile{Profile: &testproto.Profile{ 234 | User: &testproto.User{ 235 | UserId: 1, 236 | Name: "user name", 237 | }, 238 | Photo: &testproto.Photo{ 239 | PhotoId: 1, 240 | Path: "photo path", 241 | Dimensions: &testproto.Dimensions{ 242 | Width: 100, 243 | Height: 120, 244 | }, 245 | }, 246 | LoginTimestamps: []int64{1, 2, 3}, 247 | }}, 248 | }, 249 | want: &testproto.Event{ 250 | Changed: &testproto.Event_Profile{Profile: &testproto.Profile{ 251 | User: &testproto.User{ 252 | UserId: 1, 253 | }, 254 | Photo: &testproto.Photo{ 255 | Dimensions: &testproto.Dimensions{ 256 | Width: 100, 257 | Height: 120, 258 | }, 259 | }, 260 | LoginTimestamps: []int64{1, 2, 3}, 261 | }}, 262 | }, 263 | }, 264 | { 265 | name: "mask with Any field in oneof field keeps the entire Any field", 266 | paths: []string{"details"}, 267 | msg: &testproto.Event{ 268 | EventId: 1, 269 | Changed: &testproto.Event_Details{Details: createAny(&testproto.Result{ 270 | Data: []byte("bytes"), 271 | NextToken: 1, 272 | })}, 273 | }, 274 | want: &testproto.Event{ 275 | Changed: &testproto.Event_Details{Details: createAny(&testproto.Result{ 276 | Data: []byte("bytes"), 277 | NextToken: 1, 278 | })}, 279 | }, 280 | }, 281 | { 282 | name: "mask with repeated nested fields keeps the listed fields", 283 | paths: []string{"profile.gallery.photo_id", "profile.gallery.dimensions.height"}, 284 | msg: &testproto.Event{ 285 | EventId: 1, 286 | Changed: &testproto.Event_Profile{ 287 | Profile: &testproto.Profile{ 288 | Photo: &testproto.Photo{ 289 | PhotoId: 4, 290 | Path: "photo path", 291 | }, 292 | Gallery: []*testproto.Photo{ 293 | { 294 | PhotoId: 1, 295 | Path: "path 1", 296 | Dimensions: &testproto.Dimensions{ 297 | Width: 100, 298 | Height: 200, 299 | }, 300 | }, 301 | { 302 | PhotoId: 2, 303 | Path: "path 2", 304 | Dimensions: &testproto.Dimensions{ 305 | Width: 300, 306 | Height: 400, 307 | }, 308 | }, 309 | }, 310 | }, 311 | }, 312 | }, 313 | want: &testproto.Event{ 314 | Changed: &testproto.Event_Profile{ 315 | Profile: &testproto.Profile{ 316 | Gallery: []*testproto.Photo{ 317 | { 318 | PhotoId: 1, 319 | Dimensions: &testproto.Dimensions{ 320 | Height: 200, 321 | }, 322 | }, 323 | { 324 | PhotoId: 2, 325 | Dimensions: &testproto.Dimensions{ 326 | Height: 400, 327 | }, 328 | }, 329 | }, 330 | }, 331 | }, 332 | }, 333 | }, 334 | { 335 | name: "mask with repeated field keeps the listed field only", 336 | paths: []string{"profile.gallery"}, 337 | msg: &testproto.Event{ 338 | EventId: 1, 339 | Changed: &testproto.Event_Profile{ 340 | Profile: &testproto.Profile{ 341 | Photo: &testproto.Photo{ 342 | PhotoId: 4, 343 | Path: "photo path", 344 | }, 345 | Gallery: []*testproto.Photo{ 346 | { 347 | PhotoId: 1, 348 | Path: "path 1", 349 | Dimensions: &testproto.Dimensions{ 350 | Width: 100, 351 | Height: 200, 352 | }, 353 | }, 354 | { 355 | PhotoId: 2, 356 | Path: "path 2", 357 | Dimensions: &testproto.Dimensions{ 358 | Width: 300, 359 | Height: 400, 360 | }, 361 | }, 362 | }, 363 | }, 364 | }, 365 | }, 366 | want: &testproto.Event{ 367 | Changed: &testproto.Event_Profile{ 368 | Profile: &testproto.Profile{ 369 | Gallery: []*testproto.Photo{ 370 | { 371 | PhotoId: 1, 372 | Path: "path 1", 373 | Dimensions: &testproto.Dimensions{ 374 | Width: 100, 375 | Height: 200, 376 | }, 377 | }, 378 | { 379 | PhotoId: 2, 380 | Path: "path 2", 381 | Dimensions: &testproto.Dimensions{ 382 | Width: 300, 383 | Height: 400, 384 | }, 385 | }, 386 | }, 387 | }, 388 | }, 389 | }, 390 | }, 391 | { 392 | name: "mask with map field keeps the listed field only", 393 | paths: []string{"profile.attributes.a1", "profile.attributes.a2.tags.t2", "profile.attributes.aNonExistant"}, 394 | msg: &testproto.Event{ 395 | EventId: 1, 396 | Changed: &testproto.Event_Profile{ 397 | Profile: &testproto.Profile{ 398 | Attributes: map[string]*testproto.Attribute{ 399 | "a1": { 400 | Tags: map[string]string{ 401 | "t1": "1", 402 | "t2": "2", 403 | "t3": "3", 404 | }, 405 | }, 406 | "a2": { 407 | Tags: map[string]string{ 408 | "t1": "1", 409 | "t2": "2", 410 | "t3": "3", 411 | }, 412 | }, 413 | "a3": { 414 | Tags: map[string]string{ 415 | "t1": "1", 416 | "t2": "2", 417 | "t3": "3", 418 | }, 419 | }, 420 | }, 421 | }, 422 | }, 423 | }, 424 | want: &testproto.Event{ 425 | Changed: &testproto.Event_Profile{ 426 | Profile: &testproto.Profile{ 427 | Attributes: map[string]*testproto.Attribute{ 428 | "a1": { 429 | Tags: map[string]string{ 430 | "t1": "1", 431 | "t2": "2", 432 | "t3": "3", 433 | }, 434 | }, 435 | "a2": { 436 | Tags: map[string]string{ 437 | "t2": "2", 438 | }, 439 | }, 440 | }, 441 | }, 442 | }, 443 | }, 444 | }, 445 | } 446 | for _, tt := range tests { 447 | t.Run(tt.name, func(t *testing.T) { 448 | Filter(tt.msg, tt.paths) 449 | if !proto.Equal(tt.msg, tt.want) { 450 | t.Errorf("msg %v, want %v", tt.msg, tt.want) 451 | } 452 | }) 453 | } 454 | } 455 | 456 | func TestPrune(t *testing.T) { 457 | tests := []struct { 458 | name string 459 | paths []string 460 | msg proto.Message 461 | want proto.Message 462 | }{ 463 | { 464 | name: "empty mask keeps all the fields", 465 | paths: []string{}, 466 | msg: &testproto.Profile{ 467 | User: &testproto.User{ 468 | UserId: 1, 469 | Name: "user name", 470 | }, 471 | Photo: &testproto.Photo{ 472 | PhotoId: 2, 473 | Path: "photo path", 474 | Dimensions: &testproto.Dimensions{ 475 | Width: 100, 476 | Height: 120, 477 | }, 478 | }, 479 | }, 480 | want: &testproto.Profile{ 481 | User: &testproto.User{ 482 | UserId: 1, 483 | Name: "user name", 484 | }, 485 | Photo: &testproto.Photo{ 486 | PhotoId: 2, 487 | Path: "photo path", 488 | Dimensions: &testproto.Dimensions{ 489 | Width: 100, 490 | Height: 120, 491 | }, 492 | }, 493 | }, 494 | }, 495 | { 496 | name: "mask all root fields clears all fields", 497 | paths: []string{"user", "photo"}, 498 | msg: &testproto.Profile{ 499 | User: &testproto.User{ 500 | UserId: 1, 501 | Name: "user name", 502 | }, 503 | Photo: &testproto.Photo{ 504 | PhotoId: 2, 505 | Path: "photo path", 506 | Dimensions: &testproto.Dimensions{ 507 | Width: 100, 508 | Height: 120, 509 | }, 510 | }, 511 | }, 512 | want: &testproto.Profile{}, 513 | }, 514 | { 515 | name: "mask with single root field clears that field only", 516 | paths: []string{"user"}, 517 | msg: &testproto.Profile{ 518 | User: &testproto.User{ 519 | UserId: 1, 520 | Name: "user name", 521 | }, 522 | Photo: &testproto.Photo{ 523 | PhotoId: 2, 524 | Path: "photo path", 525 | Dimensions: &testproto.Dimensions{ 526 | Width: 100, 527 | Height: 120, 528 | }, 529 | }, 530 | }, 531 | want: &testproto.Profile{ 532 | Photo: &testproto.Photo{ 533 | PhotoId: 2, 534 | Path: "photo path", 535 | Dimensions: &testproto.Dimensions{ 536 | Width: 100, 537 | Height: 120, 538 | }, 539 | }, 540 | }, 541 | }, 542 | { 543 | name: "mask with nested fields clears that fields only", 544 | paths: []string{"user.name", "photo.path", "photo.dimensions.width"}, 545 | msg: &testproto.Profile{ 546 | User: &testproto.User{ 547 | UserId: 1, 548 | Name: "user name", 549 | }, 550 | Photo: &testproto.Photo{ 551 | PhotoId: 2, 552 | Path: "photo path", 553 | Dimensions: &testproto.Dimensions{ 554 | Width: 100, 555 | Height: 120, 556 | }, 557 | }, 558 | }, 559 | want: &testproto.Profile{ 560 | User: &testproto.User{ 561 | UserId: 1, 562 | }, 563 | Photo: &testproto.Photo{ 564 | PhotoId: 2, 565 | Dimensions: &testproto.Dimensions{ 566 | Height: 120, 567 | }, 568 | }, 569 | }, 570 | }, 571 | { 572 | name: "mask with oneof field clears that entire field only", 573 | paths: []string{"user"}, 574 | msg: &testproto.Event{ 575 | EventId: 1, 576 | Changed: &testproto.Event_User{User: &testproto.User{ 577 | UserId: 1, 578 | Name: "user name", 579 | }}, 580 | }, 581 | want: &testproto.Event{ 582 | EventId: 1, 583 | }, 584 | }, 585 | { 586 | name: "mask with nested oneof fields clears listed fields only", 587 | paths: []string{"profile.photo.dimensions", "profile.user.user_id", "profile.login_timestamps"}, 588 | msg: &testproto.Event{ 589 | EventId: 1, 590 | Changed: &testproto.Event_Profile{Profile: &testproto.Profile{ 591 | User: &testproto.User{ 592 | UserId: 1, 593 | Name: "user name", 594 | }, 595 | Photo: &testproto.Photo{ 596 | PhotoId: 1, 597 | Path: "photo path", 598 | Dimensions: &testproto.Dimensions{ 599 | Width: 100, 600 | Height: 120, 601 | }, 602 | }, 603 | LoginTimestamps: []int64{1, 2, 3}, 604 | }}, 605 | }, 606 | want: &testproto.Event{ 607 | EventId: 1, 608 | Changed: &testproto.Event_Profile{Profile: &testproto.Profile{ 609 | User: &testproto.User{ 610 | Name: "user name", 611 | }, 612 | Photo: &testproto.Photo{ 613 | PhotoId: 1, 614 | Path: "photo path", 615 | }, 616 | }}, 617 | }, 618 | }, 619 | { 620 | name: "mask with repeated nested fields clears the listed fields", 621 | paths: []string{"profile.gallery.photo_id", "profile.gallery.dimensions.height"}, 622 | msg: &testproto.Event{ 623 | EventId: 1, 624 | Changed: &testproto.Event_Profile{ 625 | Profile: &testproto.Profile{ 626 | Photo: &testproto.Photo{ 627 | PhotoId: 4, 628 | Path: "photo path", 629 | }, 630 | Gallery: []*testproto.Photo{ 631 | { 632 | PhotoId: 1, 633 | Path: "path 1", 634 | Dimensions: &testproto.Dimensions{ 635 | Width: 100, 636 | Height: 200, 637 | }, 638 | }, 639 | { 640 | PhotoId: 2, 641 | Path: "path 2", 642 | Dimensions: &testproto.Dimensions{ 643 | Width: 300, 644 | Height: 400, 645 | }, 646 | }, 647 | }, 648 | }, 649 | }, 650 | }, 651 | want: &testproto.Event{ 652 | EventId: 1, 653 | Changed: &testproto.Event_Profile{ 654 | Profile: &testproto.Profile{ 655 | Photo: &testproto.Photo{ 656 | PhotoId: 4, 657 | Path: "photo path", 658 | }, 659 | Gallery: []*testproto.Photo{ 660 | { 661 | Path: "path 1", 662 | Dimensions: &testproto.Dimensions{ 663 | Width: 100, 664 | }, 665 | }, 666 | { 667 | Path: "path 2", 668 | Dimensions: &testproto.Dimensions{ 669 | Width: 300, 670 | }, 671 | }, 672 | }, 673 | }, 674 | }, 675 | }, 676 | }, 677 | { 678 | name: "mask with repeated field clears the listed field only", 679 | paths: []string{"profile.gallery"}, 680 | msg: &testproto.Event{ 681 | EventId: 1, 682 | Changed: &testproto.Event_Profile{ 683 | Profile: &testproto.Profile{ 684 | Photo: &testproto.Photo{ 685 | PhotoId: 4, 686 | Path: "photo path", 687 | }, 688 | Gallery: []*testproto.Photo{ 689 | { 690 | PhotoId: 1, 691 | Path: "path 1", 692 | Dimensions: &testproto.Dimensions{ 693 | Width: 100, 694 | Height: 200, 695 | }, 696 | }, 697 | { 698 | PhotoId: 2, 699 | Path: "path 2", 700 | Dimensions: &testproto.Dimensions{ 701 | Width: 300, 702 | Height: 400, 703 | }, 704 | }, 705 | }, 706 | }, 707 | }, 708 | }, 709 | want: &testproto.Event{ 710 | EventId: 1, 711 | Changed: &testproto.Event_Profile{ 712 | Profile: &testproto.Profile{ 713 | Photo: &testproto.Photo{ 714 | PhotoId: 4, 715 | Path: "photo path", 716 | }, 717 | }, 718 | }, 719 | }, 720 | }, 721 | { 722 | name: "mask with map field prunes the listed field", 723 | paths: []string{"profile.attributes.a1", "profile.attributes.a2.tags.t2", "profile.attributes.aNonExistant"}, 724 | msg: &testproto.Event{ 725 | EventId: 1, 726 | Changed: &testproto.Event_Profile{ 727 | Profile: &testproto.Profile{ 728 | Attributes: map[string]*testproto.Attribute{ 729 | "a1": { 730 | Tags: map[string]string{ 731 | "t1": "1", 732 | "t2": "2", 733 | "t3": "3", 734 | }, 735 | }, 736 | "a2": { 737 | Tags: map[string]string{ 738 | "t1": "1", 739 | "t2": "2", 740 | "t3": "3", 741 | }, 742 | }, 743 | "a3": { 744 | Tags: map[string]string{ 745 | "t1": "1", 746 | "t2": "2", 747 | "t3": "3", 748 | }, 749 | }, 750 | }, 751 | }, 752 | }, 753 | }, 754 | want: &testproto.Event{ 755 | EventId: 1, 756 | Changed: &testproto.Event_Profile{ 757 | Profile: &testproto.Profile{ 758 | Attributes: map[string]*testproto.Attribute{ 759 | "a2": { 760 | Tags: map[string]string{ 761 | "t1": "1", 762 | "t3": "3", 763 | }, 764 | }, 765 | "a3": { 766 | Tags: map[string]string{ 767 | "t1": "1", 768 | "t2": "2", 769 | "t3": "3", 770 | }, 771 | }, 772 | }, 773 | }, 774 | }, 775 | }, 776 | }, 777 | } 778 | for _, tt := range tests { 779 | t.Run(tt.name, func(t *testing.T) { 780 | Prune(tt.msg, tt.paths) 781 | if !proto.Equal(tt.msg, tt.want) { 782 | t.Errorf("msg %v, want %v", tt.msg, tt.want) 783 | } 784 | }) 785 | } 786 | } 787 | 788 | func TestOverwrite(t *testing.T) { 789 | tests := []struct { 790 | name string 791 | paths []string 792 | src proto.Message 793 | dest proto.Message 794 | want proto.Message 795 | }{ 796 | { 797 | name: "overwrite scalar/message/map/list", 798 | paths: []string{ 799 | "user.user_id", "photo", "login_timestamps", "attributes", 800 | }, 801 | src: &testproto.Profile{ 802 | User: &testproto.User{ 803 | UserId: 567, 804 | Name: "different-name", 805 | }, 806 | Photo: &testproto.Photo{ 807 | Path: "photo-path", 808 | }, 809 | LoginTimestamps: []int64{1, 2, 3}, 810 | Attributes: map[string]*testproto.Attribute{ 811 | "src": {}, 812 | }, 813 | }, 814 | dest: &testproto.Profile{ 815 | User: &testproto.User{ 816 | Name: "name", 817 | }, 818 | LoginTimestamps: []int64{4}, 819 | Attributes: map[string]*testproto.Attribute{ 820 | "dest": {}, 821 | }, 822 | }, 823 | want: &testproto.Profile{ 824 | User: &testproto.User{ 825 | UserId: 567, 826 | Name: "name", 827 | }, 828 | Photo: &testproto.Photo{ 829 | Path: "photo-path", 830 | }, 831 | LoginTimestamps: []int64{1, 2, 3}, 832 | Attributes: map[string]*testproto.Attribute{ 833 | "src": {}, 834 | }, 835 | }, 836 | }, 837 | { 838 | name: "field inside nil message", 839 | paths: []string{"photo.path"}, 840 | src: &testproto.Profile{ 841 | Photo: &testproto.Photo{ 842 | Path: "photo-path", 843 | }, 844 | }, 845 | dest: &testproto.Profile{ 846 | Photo: nil, 847 | }, 848 | want: &testproto.Profile{ 849 | Photo: &testproto.Photo{ 850 | Path: "photo-path", 851 | }, 852 | }, 853 | }, 854 | { 855 | name: "empty message/map/list fields", 856 | paths: []string{"user", "photo.photo_id", "attributes", "login_timestamps"}, 857 | 858 | src: &testproto.Profile{ 859 | User: nil, // Empty message 860 | Photo: &testproto.Photo{ 861 | PhotoId: 0, // Empty scalar 862 | }, 863 | Attributes: make(map[string]*testproto.Attribute), // Empty map 864 | LoginTimestamps: make([]int64, 0), // Empty list 865 | }, 866 | dest: &testproto.Profile{ 867 | User: &testproto.User{ 868 | Name: "name", 869 | }, 870 | Photo: &testproto.Photo{ 871 | PhotoId: 1234, 872 | }, 873 | Attributes: map[string]*testproto.Attribute{ 874 | "attribute": { 875 | Tags: map[string]string{ 876 | "tag": "val", 877 | }, 878 | }, 879 | }, 880 | LoginTimestamps: []int64{1, 2, 3}, 881 | Gallery: []*testproto.Photo{ 882 | { 883 | PhotoId: 567, 884 | Path: "path", 885 | }, 886 | }, 887 | }, 888 | want: &testproto.Profile{ 889 | User: nil, // Empty message 890 | Photo: &testproto.Photo{ 891 | PhotoId: 0, // Empty scalar 892 | }, 893 | Attributes: make(map[string]*testproto.Attribute), // Empty map 894 | LoginTimestamps: make([]int64, 0), // Empty list 895 | Gallery: []*testproto.Photo{ 896 | { 897 | PhotoId: 567, 898 | Path: "path", 899 | }, 900 | }, 901 | }, 902 | }, 903 | { 904 | name: "partial overwrite of message fields inside map", 905 | paths: []string{ 906 | "addresses_by_name.home.number", 907 | "addresses_by_name.home.postal_code", 908 | "addresses_by_name.work.postal_code", 909 | "addresses_by_name.friend", 910 | }, 911 | src: &testproto.Profile{ 912 | AddressesByName: map[string]*testproto.Address{ 913 | "home": { 914 | Number: "18", 915 | PostalCode: "", // Empty value. 916 | }, 917 | "work": { 918 | PostalCode: "69009", 919 | }, 920 | "friend": { 921 | Number: "3", 922 | Street: "Rue de Brest", 923 | PostalCode: "29200", 924 | }, 925 | "skip": { 926 | Number: "1", 927 | Street: "Street", 928 | PostalCode: "1234", 929 | }, 930 | }, 931 | }, 932 | dest: &testproto.Profile{ 933 | AddressesByName: map[string]*testproto.Address{ 934 | "home": { 935 | Number: "1", 936 | Street: "Rue de la République", 937 | PostalCode: "75007", 938 | }, 939 | "work": { 940 | Number: "2", 941 | Street: "Rue de la Charité", 942 | PostalCode: "69002", 943 | }, 944 | }, 945 | }, 946 | want: &testproto.Profile{ 947 | AddressesByName: map[string]*testproto.Address{ 948 | "home": { 949 | Number: "18", 950 | Street: "Rue de la République", 951 | PostalCode: "", 952 | }, 953 | "work": { 954 | Number: "2", 955 | Street: "Rue de la Charité", 956 | PostalCode: "69009", 957 | }, 958 | "friend": { 959 | Number: "3", 960 | Street: "Rue de Brest", 961 | PostalCode: "29200", 962 | }, 963 | }, 964 | }, 965 | }, 966 | { 967 | name: "overwrite map with message values", 968 | paths: []string{"attributes.src1.tags.key1", "attributes.src2"}, 969 | src: &testproto.Profile{ 970 | User: nil, 971 | Attributes: map[string]*testproto.Attribute{ 972 | "src1": { 973 | Tags: map[string]string{"key1": "value1", "key2": "value2"}, 974 | }, 975 | "src2": { 976 | Tags: map[string]string{"key3": "value3"}, 977 | }, 978 | }, 979 | }, 980 | dest: &testproto.Profile{ 981 | User: &testproto.User{ 982 | Name: "name", 983 | }, 984 | Attributes: map[string]*testproto.Attribute{ 985 | "dest1": { 986 | Tags: map[string]string{"key4": "value4"}, 987 | }, 988 | }, 989 | }, 990 | want: &testproto.Profile{ 991 | User: &testproto.User{ 992 | Name: "name", 993 | }, 994 | Attributes: map[string]*testproto.Attribute{ 995 | "src1": { 996 | Tags: map[string]string{"key1": "value1"}, 997 | }, 998 | "src2": { 999 | Tags: map[string]string{"key3": "value3"}, 1000 | }, 1001 | "dest1": { 1002 | Tags: map[string]string{"key4": "value4"}, 1003 | }, 1004 | }, 1005 | }, 1006 | }, 1007 | { 1008 | name: "overwrite repeated message fields", 1009 | paths: []string{"gallery.path"}, 1010 | src: &testproto.Profile{ 1011 | User: &testproto.User{ 1012 | UserId: 567, 1013 | Name: "different-name", 1014 | }, 1015 | Photo: &testproto.Photo{ 1016 | Path: "photo-path", 1017 | }, 1018 | LoginTimestamps: []int64{1, 2, 3}, 1019 | Attributes: map[string]*testproto.Attribute{ 1020 | "src": {}, 1021 | }, 1022 | Gallery: []*testproto.Photo{ 1023 | { 1024 | PhotoId: 123, 1025 | Path: "test-path-1", 1026 | Dimensions: &testproto.Dimensions{ 1027 | Width: 345, 1028 | Height: 456, 1029 | }, 1030 | }, 1031 | { 1032 | PhotoId: 234, 1033 | Path: "test-path-2", 1034 | Dimensions: &testproto.Dimensions{ 1035 | Width: 3456, 1036 | Height: 4567, 1037 | }, 1038 | }, 1039 | { 1040 | PhotoId: 345, 1041 | Path: "test-path-3", 1042 | Dimensions: &testproto.Dimensions{ 1043 | Width: 34567, 1044 | Height: 45678, 1045 | }, 1046 | }, 1047 | }, 1048 | }, 1049 | dest: &testproto.Profile{ 1050 | User: &testproto.User{ 1051 | Name: "name", 1052 | }, 1053 | Gallery: []*testproto.Photo{ 1054 | { 1055 | PhotoId: 123, 1056 | Path: "test-path-7", 1057 | Dimensions: &testproto.Dimensions{ 1058 | Width: 345, 1059 | Height: 456, 1060 | }, 1061 | }, 1062 | { 1063 | PhotoId: 234, 1064 | Path: "test-path-6", 1065 | Dimensions: &testproto.Dimensions{ 1066 | Width: 3456, 1067 | Height: 4567, 1068 | }, 1069 | }, 1070 | { 1071 | PhotoId: 345, 1072 | Path: "test-path-5", 1073 | Dimensions: &testproto.Dimensions{ 1074 | Width: 34567, 1075 | Height: 45678, 1076 | }, 1077 | }, 1078 | { 1079 | PhotoId: 345, 1080 | Path: "test-path-4", 1081 | Dimensions: &testproto.Dimensions{ 1082 | Width: 34567, 1083 | Height: 45678, 1084 | }, 1085 | }, 1086 | }, 1087 | }, 1088 | want: &testproto.Profile{ 1089 | User: &testproto.User{ 1090 | Name: "name", 1091 | }, 1092 | Gallery: []*testproto.Photo{ 1093 | { 1094 | PhotoId: 123, 1095 | Path: "test-path-1", 1096 | Dimensions: &testproto.Dimensions{ 1097 | Width: 345, 1098 | Height: 456, 1099 | }, 1100 | }, 1101 | { 1102 | PhotoId: 234, 1103 | Path: "test-path-2", 1104 | Dimensions: &testproto.Dimensions{ 1105 | Width: 3456, 1106 | Height: 4567, 1107 | }, 1108 | }, 1109 | { 1110 | PhotoId: 345, 1111 | Path: "test-path-3", 1112 | Dimensions: &testproto.Dimensions{ 1113 | Width: 34567, 1114 | Height: 45678, 1115 | }, 1116 | }, 1117 | }, 1118 | }, 1119 | }, 1120 | { 1121 | name: "overwrite repeated message fields to empty list", 1122 | paths: []string{"gallery.path"}, 1123 | src: &testproto.Profile{ 1124 | User: &testproto.User{ 1125 | UserId: 567, 1126 | Name: "different-name", 1127 | }, 1128 | Photo: &testproto.Photo{ 1129 | Path: "photo-path", 1130 | }, 1131 | LoginTimestamps: []int64{1, 2, 3}, 1132 | Attributes: map[string]*testproto.Attribute{ 1133 | "src": {}, 1134 | }, 1135 | Gallery: []*testproto.Photo{ 1136 | { 1137 | PhotoId: 123, 1138 | Path: "test-path-1", 1139 | Dimensions: &testproto.Dimensions{ 1140 | Width: 345, 1141 | Height: 456, 1142 | }, 1143 | }, 1144 | { 1145 | PhotoId: 234, 1146 | Path: "test-path-2", 1147 | Dimensions: &testproto.Dimensions{ 1148 | Width: 3456, 1149 | Height: 4567, 1150 | }, 1151 | }, 1152 | { 1153 | PhotoId: 345, 1154 | Path: "test-path-3", 1155 | Dimensions: &testproto.Dimensions{ 1156 | Width: 34567, 1157 | Height: 45678, 1158 | }, 1159 | }, 1160 | }, 1161 | }, 1162 | dest: &testproto.Profile{}, 1163 | want: &testproto.Profile{ 1164 | Gallery: []*testproto.Photo{ 1165 | { 1166 | Path: "test-path-1", 1167 | }, 1168 | { 1169 | Path: "test-path-2", 1170 | }, 1171 | { 1172 | Path: "test-path-3", 1173 | }, 1174 | }, 1175 | }, 1176 | }, 1177 | { 1178 | name: "overwrite optional fields with nil", 1179 | paths: []string{"optional_string", "optional_int", "optional_photo", "optional_attr"}, 1180 | src: &testproto.Options{ 1181 | OptionalString: nil, 1182 | OptionalInt: nil, 1183 | OptionalPhoto: nil, 1184 | OptionalAttr: nil, 1185 | }, 1186 | dest: &testproto.Options{ 1187 | OptionalString: proto.String("optional string"), 1188 | OptionalInt: proto.Int32(10), 1189 | OptionalPhoto: &testproto.Photo{ 1190 | PhotoId: 123, 1191 | Path: "test-path", 1192 | Dimensions: &testproto.Dimensions{ 1193 | Width: 100, 1194 | Height: 200, 1195 | }, 1196 | }, 1197 | OptionalAttr: &testproto.Attribute{ 1198 | Tags: map[string]string{ 1199 | "a": "b", 1200 | "c": "d", 1201 | }, 1202 | }, 1203 | }, 1204 | want: &testproto.Options{ 1205 | OptionalString: nil, 1206 | OptionalInt: nil, 1207 | OptionalPhoto: nil, 1208 | OptionalAttr: nil, 1209 | }, 1210 | }, 1211 | { 1212 | name: "overwrite empty optional field with a value", 1213 | paths: []string{"optional_string", "optional_int", "optional_photo", "optional_attr"}, 1214 | src: &testproto.Options{ 1215 | OptionalString: proto.String("optional string"), 1216 | OptionalInt: proto.Int32(10), 1217 | OptionalPhoto: &testproto.Photo{ 1218 | PhotoId: 123, 1219 | Path: "test-path", 1220 | Dimensions: &testproto.Dimensions{ 1221 | Width: 100, 1222 | Height: 200, 1223 | }, 1224 | }, 1225 | OptionalAttr: &testproto.Attribute{ 1226 | Tags: map[string]string{ 1227 | "key1": "value1", 1228 | "key2": "value2", 1229 | }, 1230 | }, 1231 | }, 1232 | dest: &testproto.Options{}, 1233 | want: &testproto.Options{ 1234 | OptionalString: proto.String("optional string"), 1235 | OptionalInt: proto.Int32(10), 1236 | OptionalPhoto: &testproto.Photo{ 1237 | PhotoId: 123, 1238 | Path: "test-path", 1239 | Dimensions: &testproto.Dimensions{ 1240 | Width: 100, 1241 | Height: 200, 1242 | }, 1243 | }, 1244 | OptionalAttr: &testproto.Attribute{ 1245 | Tags: map[string]string{ 1246 | "key1": "value1", 1247 | "key2": "value2", 1248 | }, 1249 | }, 1250 | }, 1251 | }, 1252 | } 1253 | for _, tt := range tests { 1254 | t.Run(tt.name, func(t *testing.T) { 1255 | Overwrite(tt.src, tt.dest, tt.paths) 1256 | if !proto.Equal(tt.dest, tt.want) { 1257 | t.Errorf("dest %v, want %v", tt.dest, tt.want) 1258 | } 1259 | }) 1260 | } 1261 | } 1262 | 1263 | func TestFromFieldNumbers(t *testing.T) { 1264 | tests := []struct { 1265 | name string 1266 | msg proto.Message 1267 | fieldNumbers []int 1268 | want []string 1269 | }{ 1270 | { 1271 | name: "empty field numbers", 1272 | msg: &testproto.User{}, 1273 | fieldNumbers: []int{}, 1274 | want: nil, 1275 | }, 1276 | { 1277 | name: "single field number", 1278 | msg: &testproto.User{}, 1279 | fieldNumbers: []int{1}, 1280 | want: []string{"user_id"}, 1281 | }, 1282 | { 1283 | name: "multiple field numbers", 1284 | msg: &testproto.User{}, 1285 | fieldNumbers: []int{1, 2}, 1286 | want: []string{"user_id", "name"}, 1287 | }, 1288 | { 1289 | name: "field numbers with Profile", 1290 | msg: &testproto.Profile{}, 1291 | fieldNumbers: []int{1, 2, 3}, 1292 | want: []string{"user", "photo", "login_timestamps"}, 1293 | }, 1294 | { 1295 | name: "non-existent field number", 1296 | msg: &testproto.User{}, 1297 | fieldNumbers: []int{999}, 1298 | want: []string{}, 1299 | }, 1300 | { 1301 | name: "mixed valid and invalid field numbers", 1302 | msg: &testproto.User{}, 1303 | fieldNumbers: []int{1, 999, 2}, 1304 | want: []string{"user_id", "name"}, 1305 | }, 1306 | { 1307 | name: "duplicate field numbers", 1308 | msg: &testproto.User{}, 1309 | fieldNumbers: []int{1, 1, 2}, 1310 | want: []string{"user_id", "user_id", "name"}, 1311 | }, 1312 | } 1313 | for _, tt := range tests { 1314 | t.Run(tt.name, func(t *testing.T) { 1315 | got := PathsFromFieldNumbers(tt.msg, tt.fieldNumbers...) 1316 | slices.Sort(got) 1317 | slices.Sort(tt.want) 1318 | if !slices.Equal(got, tt.want) { 1319 | t.Errorf("PathsFromFieldNumbers() = %v, want %v", got, tt.want) 1320 | } 1321 | }) 1322 | } 1323 | } 1324 | 1325 | func TestValidate(t *testing.T) { 1326 | tests := []struct { 1327 | name string 1328 | msg proto.Message 1329 | paths []string 1330 | wantErr bool 1331 | }{ 1332 | { 1333 | name: "empty mask", 1334 | msg: &testproto.Profile{}, 1335 | }, 1336 | { 1337 | name: "happy path", 1338 | msg: &testproto.Profile{}, 1339 | paths: []string{ 1340 | "user", 1341 | "photo.path", 1342 | "login_timestamps", 1343 | "gallery.photo_id", 1344 | "gallery.dimensions.width", 1345 | "attributes.tags", 1346 | }, 1347 | }, 1348 | { 1349 | name: "happy path with oneof", 1350 | msg: &testproto.Event{}, 1351 | paths: []string{ 1352 | "user", 1353 | "photo.photo_id", 1354 | "photo.dimensions.height", 1355 | "details", 1356 | "profile.login_timestamps", 1357 | "profile.attributes.tags", 1358 | }, 1359 | }, 1360 | { 1361 | name: "happy path with optional fields", 1362 | msg: &testproto.Options{}, 1363 | paths: []string{ 1364 | "optional_string", 1365 | "optional_photo.photo_id", 1366 | "optional_photo.dimensions.height", 1367 | "optional_attr.tags", 1368 | }, 1369 | }, 1370 | { 1371 | name: "incorrect root field", 1372 | msg: &testproto.Profile{}, 1373 | paths: []string{"invalid"}, 1374 | wantErr: true, 1375 | }, 1376 | { 1377 | name: "incorrect nested field", 1378 | msg: &testproto.Profile{}, 1379 | paths: []string{"user.invalid"}, 1380 | wantErr: true, 1381 | }, 1382 | { 1383 | name: "incorrect repeated field", 1384 | msg: &testproto.Profile{}, 1385 | paths: []string{"gallery.invalid"}, 1386 | wantErr: true, 1387 | }, 1388 | { 1389 | name: "incorrect nested repeated field", 1390 | msg: &testproto.Profile{}, 1391 | paths: []string{"gallery.dimensions.invalid"}, 1392 | wantErr: true, 1393 | }, 1394 | { 1395 | name: "incorrect map field", 1396 | msg: &testproto.Profile{}, 1397 | paths: []string{"attributes.invalid"}, 1398 | wantErr: true, 1399 | }, 1400 | { 1401 | name: "incorrect map field", 1402 | msg: &testproto.Profile{}, 1403 | paths: []string{"attributes.invalid"}, 1404 | wantErr: true, 1405 | }, 1406 | { 1407 | name: "incorrect field inside oneof", 1408 | msg: &testproto.Event{}, 1409 | paths: []string{"user.gallery.invalid"}, 1410 | wantErr: true, 1411 | }, 1412 | { 1413 | name: "incorrect repeated field inside oneof", 1414 | msg: &testproto.Event{}, 1415 | paths: []string{"profile.gallery.invalid"}, 1416 | wantErr: true, 1417 | }, 1418 | { 1419 | name: "incorrect map field inside oneof", 1420 | msg: &testproto.Event{}, 1421 | paths: []string{"profile.attributes.invalid"}, 1422 | wantErr: true, 1423 | }, 1424 | { 1425 | name: "incorrect optional field", 1426 | msg: &testproto.Options{}, 1427 | paths: []string{"optional_photo.invalid"}, 1428 | wantErr: true, 1429 | }, 1430 | { 1431 | name: "incorrect path for repeated field with scalar value", 1432 | msg: &testproto.Profile{}, 1433 | paths: []string{"login_timestamps.invalid"}, 1434 | wantErr: true, 1435 | }, 1436 | { 1437 | name: "incorrect path for map field with scalar value", 1438 | msg: &testproto.Attribute{}, 1439 | paths: []string{"tags.invalid"}, 1440 | wantErr: true, 1441 | }, 1442 | { 1443 | name: "incorrect nested path for scalar field", 1444 | msg: &testproto.Dimensions{}, 1445 | paths: []string{"height.invalid"}, 1446 | wantErr: true, 1447 | }, 1448 | } 1449 | for _, tt := range tests { 1450 | t.Run(tt.name, func(t *testing.T) { 1451 | err := Validate(tt.msg, tt.paths) 1452 | if (err != nil) != tt.wantErr { 1453 | t.Errorf("want error: %v, got: %v", tt.wantErr, err) 1454 | } 1455 | }) 1456 | } 1457 | } 1458 | 1459 | func BenchmarkNestedMaskFromPaths(b *testing.B) { 1460 | for i := 0; i < b.N; i++ { 1461 | NestedMaskFromPaths([]string{"aaa.bbb.c.d.e.f", "aa.b.cc.ddddddd", "e", "f", "g.h.i.j.k"}) 1462 | } 1463 | } 1464 | --------------------------------------------------------------------------------