From 74fe4ef2d3bb8c0ee59f3febb725cf8ac77286f6 Mon Sep 17 00:00:00 2001 From: Ido David Date: Thu, 20 Jan 2022 03:11:51 -0500 Subject: [PATCH] first commit --- .earthignore | 1 + .github/workflows/ci.yml | 32 +++++++ .github/workflows/release.yml | 46 +++++++++ .gitignore | 4 + Earthfile | 81 ++++++++++++++++ LICENSE | 21 +++++ README.md | 87 +++++++++++++++++ cmd/protoc-gen-fieldmask/main.go | 45 +++++++++ go.mod | 16 ++++ go.sum | 131 ++++++++++++++++++++++++++ protoc/generator.go | 9 ++ protoc/helper.go | 27 ++++++ protoc/interface_generator.go | 40 ++++++++ protoc/plugin.go | 90 ++++++++++++++++++ protoc/struct_generator.go | 86 +++++++++++++++++ protoc/vars_generator.go | 42 +++++++++ protos/cases/from_other_file.proto | 13 +++ protos/cases/pkg_a.proto | 10 ++ protos/cases/pkg_b.proto | 10 ++ protos/cases/recursive.proto | 10 ++ protos/cases/thirdpartyimport/a.proto | 13 +++ protos/cases/thirdpartyimport/b.proto | 13 +++ protos/cases/types.proto | 58 ++++++++++++ test/cases_test.go | 122 ++++++++++++++++++++++++ 24 files changed, 1007 insertions(+) create mode 100644 .earthignore create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Earthfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/protoc-gen-fieldmask/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 protoc/generator.go create mode 100644 protoc/helper.go create mode 100644 protoc/interface_generator.go create mode 100644 protoc/plugin.go create mode 100644 protoc/struct_generator.go create mode 100644 protoc/vars_generator.go create mode 100644 protos/cases/from_other_file.proto create mode 100644 protos/cases/pkg_a.proto create mode 100644 protos/cases/pkg_b.proto create mode 100644 protos/cases/recursive.proto create mode 100644 protos/cases/thirdpartyimport/a.proto create mode 100644 protos/cases/thirdpartyimport/b.proto create mode 100644 protos/cases/types.proto create mode 100644 test/cases_test.go diff --git a/.earthignore b/.earthignore new file mode 100644 index 0000000..8da2b37 --- /dev/null +++ b/.earthignore @@ -0,0 +1 @@ +cmd/tester/* \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3e3b52c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + + - uses: nelonoel/branch-name@v1.0.1 + + - uses: earthly/actions/setup-earthly@v1 + with: + version: v0.6.4 + + - name: Earthly Version + run: earthly --version + + - name: Build + env: + COMMIT_HASH: ${{ github.sha }} + FORCE_COLOR: 1 + run: earthly -P --ci +all diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..41141ec --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,46 @@ +name: Release + +on: + push: + tags: + - 'v[0-9]+\.[0-9]+\.[0-9]+' + +jobs: + release-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + + - uses: nelonoel/branch-name@v1.0.1 + + - uses: earthly/actions/setup-earthly@v1 + with: + version: v0.6.4 + + - name: Earthly Version + run: earthly --version + + - name: Create Version + id: version + if: success() + run: | + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + # Strip "v" prefix from tag name + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + # set to output var + echo ::set-output name=VERSION::${VERSION} + + - name: Build + if: success() + env: + FORCE_COLOR: 1 + VERSION: ${{ steps.version.outputs.VERSION }} + run: earthly -P --ci --output +all --VERSION=$VERSION + + - name: Upload Release Assets + if: success() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload $BRANCH_NAME ./bin/protoc-gen-fieldmask*.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5b1d46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +gen/ +.idea/ +vendor/ diff --git a/Earthfile b/Earthfile new file mode 100644 index 0000000..f090d29 --- /dev/null +++ b/Earthfile @@ -0,0 +1,81 @@ +VERSION 0.6 + +ARG ALPINE_VERSION=3.15 +ARG GO_VERSION=1.17 +ARG LINTER_VERSION=v1.43.0 +FROM golang:$GO_VERSION-alpine$ALPINE_VERSION +WORKDIR /app + +stage: + COPY --dir go.mod go.sum ./ + RUN go mod download -x + COPY --dir protoc cmd . + SAVE ARTIFACT /app + +vendor: + FROM +stage + RUN go mod vendor + +lint: + # Installs golangci-lint to ./bin + RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s $LINTER_VERSION + COPY +stage/app . + RUN ./bin/golangci-lint run --skip-dirs=vendor --skip-dirs=./gen/ --deadline=5m --tests=true -E revive \ + -E gosec -E unconvert -E goconst -E gocyclo -E goimports + +build: + FROM +vendor + # compile app binary, save as artifact + ARG VERSION="dev" + ARG GOOS + ARG GOARCH + RUN go build -ldflags="-s -w -X 'main.version=${VERSION}'" -mod=vendor -o bin/protoc-gen-fieldmask ./cmd/protoc-gen-fieldmask/... + SAVE ARTIFACT ./bin/protoc-gen-fieldmask /protoc-gen-fieldmask + +zip: + RUN apk add zip + WORKDIR /artifacts + ARG VERSION + ARG ZIP_FILE_NAME + ARG EXT + COPY (+build/protoc-gen-fieldmask) protoc-gen-fieldmask${EXT} + RUN zip -m protoc-gen-fieldmask-${VERSION}-${ZIP_FILE_NAME}.zip protoc-gen-fieldmask${EXT} + SAVE ARTIFACT /artifacts + +build-all: + WORKDIR /artifacts + COPY (+zip/artifacts/*.zip --GOOS=darwin --GOARCH=amd64 --ZIP_FILE_NAME=osx-x86_64) . + COPY (+zip/artifacts/*.zip --GOOS=linux --GOARCH=386 --ZIP_FILE_NAME=linux-x86_32) . + COPY (+zip/artifacts/*.zip --GOOS=linux --GOARCH=amd64 --ZIP_FILE_NAME=linux-x86_64) . + COPY (+zip/artifacts/*.zip --GOOS=windows --GOARCH=386 --ZIP_FILE_NAME=win32 --EXT=.exe) . + COPY (+zip/artifacts/*.zip --GOOS=windows --GOARCH=amd64 --ZIP_FILE_NAME=win64 --EXT=.exe) . + SAVE ARTIFACT /artifacts AS LOCAL bin + +test-gen: + ARG DOCKER_PROTOC_VERSION=1.42_0 + FROM namely/protoc-all:$DOCKER_PROTOC_VERSION + RUN mkdir /plugins + COPY +build/protoc-gen-fieldmask /usr/local/bin/. + COPY --dir protos . + RUN entrypoint.sh -i protos -d protos/cases -l go -o gen + RUN entrypoint.sh -i protos -d protos/cases/thirdpartyimport -l go -o gen + RUN protoc -I/opt/include -Iprotos --fieldmask_out=gen protos/cases/*.proto protos/cases/thirdpartyimport/*.proto + SAVE ARTIFACT gen /gen AS LOCAL test/gen + +test: + FROM +vendor + RUN apk add build-base + COPY --dir test . + COPY --dir +test-gen/gen test/. + RUN go mod vendor + RUN go test github.com/idodod/protoc-gen-fieldmask/test + +all: + BUILD +lint + BUILD +test + BUILD +build-all + +clean: + LOCALLY + RUN rm -rf test/gen vendor + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..27325c7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Ido David + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d05da5 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# protoc-gen-fieldmask + +[![CI](https://github.com/idodod/protoc-gen-fieldmask/actions/workflows/ci.yml/badge.svg)](https://github.com/idodod/protoc-gen-fieldmask/actions/workflows/ci.yml) +![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/idodod/protoc-gen-fieldmask) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/idodod/protoc-gen-fieldmask) +![GitHub](https://img.shields.io/github/license/idodod/protoc-gen-fieldmask) + +A protoc plugin that generates fieldmask paths as static type properties for proto messages, which elimantes the usage of error-prone strings. + +For example, given the following proto messages: + +```proto + +syntax = "proto3"; + +package example; + +option go_package = "example/;example"; + +import "google/type/date.proto"; + +message Foo { + string baz = 1; + int32 xyz = 2; + Bar my_bar = 3; + google.type.Date some_date = 4; +} + +message Bar { + string some_field = 1; + bool another_field = 2; +} +``` + +fieldmasks paths can be used as follows: + +```golang + foo := &example.Foo{} + + # Prints "baz" + fmt.Println(foo.FieldMaskPaths().Baz()) + + # Prints "xyz" + fmt.Println(foo.FieldMaskPaths().Xyz()) + + # prints "my_bar" + fmt.Println(foo.FieldMaskPaths().MyBar().String()) + + # since baz is a nested message, we can print a nested path - "my_bar.some_field" + fmt.Println(foo.FieldMaskPaths().MyBar().SomeField()) + + # thirdparty messages work the same way: + #print "some_date" + fmt.Println(foo.FieldMaskPaths().SomeDate().String()) + + #print "some_date.year" + fmt.Println(foo.FieldMaskPaths().SomeDate().Year()) +``` + +## Usage + +### Installation + +The plugin can be downloaded from the [release page](https://github.com/idodod/protoc-gen-fieldmask/releases/latest), and should be ideally installed somewhere available in your `$PATH`. + +### Executing the plugin + +```sh +protoc --fieldmask_out=gen protos/example.proto + +# if the plugin is not in your $PATH: +protoc --fieldmask_out=out_dir protos/example.proto --plugin=protoc-gen-fieldmask=/path/to/protoc-gen-fieldmask +``` + +### Parameters + +The following parameters can be set by passing `--fieldmask_opt` to the command: + +* `maxdepth`: This option is relevant for a recursive message case.\ + Specify the max depth for which the paths will be pregenerated. If the path depth gets over the max value, it will be generated at runtime. + default value is `7`. + +## Features + +* Currently the only supported language is `go`. +* All paths are pregenerated (except for recursive messages past `maxdepth`). +* Support all type of fields including repeated fields, maps, oneofs, third parties, nested messages and recursive messages. diff --git a/cmd/protoc-gen-fieldmask/main.go b/cmd/protoc-gen-fieldmask/main.go new file mode 100644 index 0000000..989ed47 --- /dev/null +++ b/cmd/protoc-gen-fieldmask/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/idodod/protoc-gen-fieldmask/protoc" + "google.golang.org/protobuf/compiler/protogen" +) + +const ( + defaultMaxDepth = 7 + defaultLang = "go" +) + +var version = "dev" + +func main() { + app := filepath.Base(os.Args[0]) + showVersion := flag.Bool("version", false, "print the version and exit") + flag.Parse() + if *showVersion { + fmt.Printf("%s %v\n", app, version) + return + } + + var flags flag.FlagSet + maxDepth := flags.Uint("maxdepth", defaultMaxDepth, "") + lang := flags.String("lang", defaultLang, "") + protogen.Options{ + ParamFunc: flags.Set, + }.Run(func(plugin *protogen.Plugin) error { + if strings.ToLower(*lang) != defaultLang { + return errors.New("go is the only supported language at the moment") + } + if *maxDepth <= 0 { + return errors.New("maxdepth must be bigger than 0") + } + return protoc.Generate(plugin, *maxDepth) + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..495abd7 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/idodod/protoc-gen-fieldmask + +go 1.17 + +require ( + github.com/iancoleman/strcase v0.2.0 + github.com/stretchr/testify v1.7.0 + google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 + google.golang.org/protobuf v1.27.1 +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3567637 --- /dev/null +++ b/go.sum @@ -0,0 +1,131 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q= +google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/protoc/generator.go b/protoc/generator.go new file mode 100644 index 0000000..39f0631 --- /dev/null +++ b/protoc/generator.go @@ -0,0 +1,9 @@ +package protoc + +import ( + "google.golang.org/protobuf/compiler/protogen" +) + +type generator interface { + Generate(file *protogen.GeneratedFile) +} diff --git a/protoc/helper.go b/protoc/helper.go new file mode 100644 index 0000000..d475440 --- /dev/null +++ b/protoc/helper.go @@ -0,0 +1,27 @@ +package protoc + +import ( + "github.com/iancoleman/strcase" + "google.golang.org/protobuf/compiler/protogen" +) + +func getInterfaceName(message *protogen.Message) string { + return strcase.ToLowerCamel(string(message.Desc.Parent().FullName())) + message.GoIdent.GoName + "Int" + structSuffix +} + +func getStructName(message *protogen.Message) string { + return strcase.ToLowerCamel(string(message.Desc.Parent().FullName())) + message.GoIdent.GoName + structSuffix +} + +func getStructNewFunction(message *protogen.Message) string { + return "new" + strcase.ToCamel(getStructName(message)) +} + +func getFilePath(f *protogen.File) string { + return f.GeneratedFilenamePrefix + generatedExtension +} + +func getFileHeaderComment(protoFile string) string { + return "// Code generated by protoc-gen-fieldmask. DO NOT EDIT.\n" + + "// source: " + protoFile +} diff --git a/protoc/interface_generator.go b/protoc/interface_generator.go new file mode 100644 index 0000000..8841659 --- /dev/null +++ b/protoc/interface_generator.go @@ -0,0 +1,40 @@ +package protoc + +import ( + "google.golang.org/protobuf/compiler/protogen" +) + +type interfaceGenerator struct { + name string + strFields []*protogen.Field + msgFields []*protogen.Field +} + +func newInterfaceGenerator(message *protogen.Message) *interfaceGenerator { + return &interfaceGenerator{ + name: getInterfaceName(message), + } +} + +// AddStringFields adds fields for which the fieldmask path is a simple string +func (x *interfaceGenerator) AddStringFields(fields ...*protogen.Field) { + x.strFields = append(x.strFields, fields...) +} + +// AddMessageFields adds fields for which the fieldmask path is a nested message with additional nested paths +func (x *interfaceGenerator) AddMessageFields(fields ...*protogen.Field) { + x.msgFields = append(x.msgFields, fields...) +} + +// Generate generates an interface with all fieldmask paths functions for the given type. +func (x *interfaceGenerator) Generate(g *protogen.GeneratedFile) { + g.P("type ", x.name, " interface {") + for _, field := range x.strFields { + g.P(field.GoName, "() string") + } + for _, field := range x.msgFields { + g.P(field.GoName, "() *", getStructName(field.Message)) + } + g.P("}") + g.P() +} diff --git a/protoc/plugin.go b/protoc/plugin.go new file mode 100644 index 0000000..36c8f3b --- /dev/null +++ b/protoc/plugin.go @@ -0,0 +1,90 @@ +package protoc + +import ( + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" +) + +const ( + generatedExtension = ".pb.fieldmask.go" + structSuffix = "FieldMaskPaths" +) + +// Generate will iterate over all given proto files and will generate fieldmask paths functions for each message +func Generate(plugin *protogen.Plugin, maxDepth uint) error { + seen := make(map[string]map[string]struct{}) + for _, f := range plugin.Files { + if !f.Generate { + continue + } + m, exists := seen[f.GoImportPath.String()] + if !exists { + m = make(map[string]struct{}) + seen[f.GoImportPath.String()] = m + } + generateFile(f, plugin, m, maxDepth) + } + return nil +} + +func generateFile(f *protogen.File, plugin *protogen.Plugin, seen map[string]struct{}, maxDepth uint) { + + if len(f.Messages) > 0 { + g := plugin.NewGeneratedFile(getFilePath(f), f.GoImportPath) + g.P(getFileHeaderComment(f.Desc.Path())) + g.P("package " + f.GoPackageName) + g.P("") + + varsGenerator := newVarsGenerator(maxDepth) + generators := []generator{varsGenerator} + + packageName := string(f.GoImportPath) + for _, message := range f.Messages { + generators = append(generators, generateFieldMaskPaths(g, packageName, message, "", seen, varsGenerator, maxDepth)...) + } + + for _, generator := range generators { + generator.Generate(g) + } + } +} + +// generateFieldMaskPaths generates a FieldMaskPath struct for each proto message which will contain the fieldmask paths +func generateFieldMaskPaths(g *protogen.GeneratedFile, generatedFileImportPath string, message *protogen.Message, currFieldPath string, seen map[string]struct{}, varsGenerator *varsGenerator, maxDepth uint) []generator { + if len(message.Fields) == 0 { + return nil + } + messageName := string(message.Desc.FullName()) + if _, exists := seen[messageName]; exists { + return nil + } + seen[messageName] = struct{}{} + + msgStructGenerator := newStructGenerator(message, maxDepth) + msgInterfaceGenerator := newInterfaceGenerator(message) + + var generators []generator + // only generate the fieldmask function if the message belongs to the current file's package + if string(message.GoIdent.GoImportPath) == generatedFileImportPath { + varsGenerator.AddMessage(message) + generators = append(generators, msgInterfaceGenerator) + } + generators = append(generators, msgStructGenerator) + + for _, field := range message.Fields { + if (field.Desc.Kind() != protoreflect.MessageKind && field.Desc.Kind() != protoreflect.GroupKind) || field.Desc.IsList() || field.Desc.IsMap() { + msgInterfaceGenerator.AddStringFields(field) + msgStructGenerator.AddStringFields(field) + } else { + msgInterfaceGenerator.AddMessageFields(field) + msgStructGenerator.AddMessageFields(field) + nextFieldPath := string(field.Desc.Name()) + if currFieldPath != "" { + nextFieldPath = currFieldPath + "." + nextFieldPath + } + generators = append(generators, generateFieldMaskPaths(g, generatedFileImportPath, field.Message, nextFieldPath, seen, varsGenerator, maxDepth)...) + } + } + g.P() + return generators +} diff --git a/protoc/struct_generator.go b/protoc/struct_generator.go new file mode 100644 index 0000000..45f4389 --- /dev/null +++ b/protoc/struct_generator.go @@ -0,0 +1,86 @@ +package protoc + +import ( + "github.com/iancoleman/strcase" + "google.golang.org/protobuf/compiler/protogen" +) + +type structGenerator struct { + name string + strFields []*protogen.Field + msgFields []*protogen.Field + maxDepth uint +} + +func newStructGenerator(message *protogen.Message, maxDepth uint) *structGenerator { + return &structGenerator{ + name: strcase.ToLowerCamel(string(message.Desc.Parent().FullName())) + message.GoIdent.GoName + structSuffix, + maxDepth: maxDepth, + } +} + +// AddStringFields adds fields for which the fieldmask path is a simple string +func (x *structGenerator) AddStringFields(fields ...*protogen.Field) { + x.strFields = append(x.strFields, fields...) +} + +// AddMessageFields adds fields for which the fieldmask path is a nested message with additional nested paths +func (x *structGenerator) AddMessageFields(fields ...*protogen.Field) { + x.msgFields = append(x.msgFields, fields...) +} + +// Generate generates a struct with all fieldmask paths functions for the given type. +func (x *structGenerator) Generate(g *protogen.GeneratedFile) { + // generate struct with all fields + g.P("type ", x.name, " struct {") + g.P("fieldPath string") + g.P("prefix string") + for _, field := range x.strFields { + g.P(strcase.ToLowerCamel(field.GoName), " string") + } + for _, field := range x.msgFields { + g.P(strcase.ToLowerCamel(field.GoName), " *", getStructName(field.Message)) + } + g.P("}") + g.P() + + // generate ctor + g.P("func ", "new"+strcase.ToCamel(x.name), "(fieldPath string, maxDepth int) *", x.name, " { ") + g.P("if maxDepth <= 0 {") + g.P("return nil") + g.P("}") + g.P("prefix := \"\"") + g.P("if fieldPath != \"\" {") + g.P("prefix = fieldPath + \".\"") + g.P("}") + g.P("return &", x.name, "{") + g.P("fieldPath: fieldPath,") + g.P("prefix: prefix,") + for _, field := range x.strFields { + g.P(strcase.ToLowerCamel(field.GoName), ": prefix + \"", field.Desc.Name(), "\",") + } + for _, field := range x.msgFields { + fieldStructNewFunction := getStructNewFunction(field.Message) + g.P(strcase.ToLowerCamel(field.GoName), ": ", fieldStructNewFunction, "(prefix + \"", field.Desc.Name(), "\", maxDepth - 1),") + } + g.P("}") + g.P("}") + g.P() + + // generate receiver methods + g.P("func (x *", x.name, ") String() string { return x.fieldPath }") + for _, field := range x.strFields { + g.P("func (x *", x.name, ") ", field.GoName, "() string { return x.", strcase.ToLowerCamel(field.GoName), "}") + } + for _, field := range x.msgFields { + varName := strcase.ToLowerCamel(field.GoName) + fieldStructNewFunction := getStructNewFunction(field.Message) + g.P("func (x *", x.name, ") ", field.GoName, "() *", getStructName(field.Message), " {") + g.P("if x.", varName, "!= nil {") + g.P("return x.", varName) + g.P("}") + g.P("return ", fieldStructNewFunction, "(x.prefix + \"", field.Desc.Name(), "\",", x.maxDepth, ")") + g.P("}") + } + g.P() +} diff --git a/protoc/vars_generator.go b/protoc/vars_generator.go new file mode 100644 index 0000000..16cc7da --- /dev/null +++ b/protoc/vars_generator.go @@ -0,0 +1,42 @@ +package protoc + +import ( + "github.com/iancoleman/strcase" + "google.golang.org/protobuf/compiler/protogen" +) + +type varsGenerator struct { + messages []*protogen.Message + maxDepth uint +} + +func newVarsGenerator(maxDepth uint) *varsGenerator { + return &varsGenerator{ + maxDepth: maxDepth, + } +} + +// AddMessage adds proto message definitions for which to create an instance of generated fieldmaskpath type +func (x *varsGenerator) AddMessage(messages ...*protogen.Message) { + x.messages = append(x.messages, messages...) +} + +// Generate generates the var definitions for all added messages +func (x *varsGenerator) Generate(g *protogen.GeneratedFile) { + for _, message := range x.messages { + messageStructName := getStructName(message) + structNewFunction := getStructNewFunction(message) + localVarName := "local" + strcase.ToCamel(messageStructName) + g.P("var ", localVarName, " = ", structNewFunction, "(\"\",", x.maxDepth, ")") + } + g.P() + for _, message := range x.messages { + messageInterfaceName := getInterfaceName(message) + messageStructName := getStructName(message) + localVarName := "local" + strcase.ToCamel(messageStructName) + g.P("func (x *", message.GoIdent.GoName, ") ", structSuffix, "() ", messageInterfaceName, " {") + g.P("return ", localVarName) + g.P("}") + } + g.P() +} diff --git a/protos/cases/from_other_file.proto b/protos/cases/from_other_file.proto new file mode 100644 index 0000000..8da0a0d --- /dev/null +++ b/protos/cases/from_other_file.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package cases; + +option go_package = "cases/;cases"; + +import "google/type/date.proto"; + +message YetAnotherTestNestedExternalMessage { + string foo = 1; + int32 bar = 2; + google.type.Date baz = 3; +} \ No newline at end of file diff --git a/protos/cases/pkg_a.proto b/protos/cases/pkg_a.proto new file mode 100644 index 0000000..836d64f --- /dev/null +++ b/protos/cases/pkg_a.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package casesa; + +option go_package = "cases/a;a"; + +message Foo { + string bar = 1; + int32 baz = 2; +} \ No newline at end of file diff --git a/protos/cases/pkg_b.proto b/protos/cases/pkg_b.proto new file mode 100644 index 0000000..176b091 --- /dev/null +++ b/protos/cases/pkg_b.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package casesb; + +option go_package = "cases/b;b"; + +message Foo { + string bar = 1; + int32 baz = 2; +} \ No newline at end of file diff --git a/protos/cases/recursive.proto b/protos/cases/recursive.proto new file mode 100644 index 0000000..83a113c --- /dev/null +++ b/protos/cases/recursive.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package example; + +option go_package = "cases/;cases"; + +message Node { + string name = 1; + Node next = 2; +} diff --git a/protos/cases/thirdpartyimport/a.proto b/protos/cases/thirdpartyimport/a.proto new file mode 100644 index 0000000..dd2b4bd --- /dev/null +++ b/protos/cases/thirdpartyimport/a.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package thirdpartyimport; + +option go_package = "cases/thirdpartyimport;thirdpartyimport"; + +import "google/type/date.proto"; + +message FooA { + string bar = 1; + int32 baz = 2; + google.type.Date some_date = 3; +} \ No newline at end of file diff --git a/protos/cases/thirdpartyimport/b.proto b/protos/cases/thirdpartyimport/b.proto new file mode 100644 index 0000000..92a0b45 --- /dev/null +++ b/protos/cases/thirdpartyimport/b.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package thirdpartyimport; + +option go_package = "cases/thirdpartyimport;thirdpartyimport"; + +import "google/type/date.proto"; + +message FooB { + string bar = 1; + int32 baz = 2; + google.type.Date some_other_date = 3; +} \ No newline at end of file diff --git a/protos/cases/types.proto b/protos/cases/types.proto new file mode 100644 index 0000000..3938b24 --- /dev/null +++ b/protos/cases/types.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package cases; + +option go_package = "cases/;cases"; + +import "cases/from_other_file.proto"; +import "google/protobuf/any.proto"; +import "google/type/date.proto"; + +enum MyEnum { + UNDEFINED = 0; + ONE = 1; + TWO = 2; + THREE = 3; +} + +message TestNestedExternalMessage { + string foo = 1; + int32 bar = 2; +} + +message Foo { + double my_double_field = 1; + float my_float_field = 2; + int32 my_int_32_field = 3; + int64 my_int_64_field = 4; + uint32 my_uint_32_field = 5; + uint64 my_uint_64_field = 6; + sint32 my_sint_32_field = 7; + sint64 my_sint_64_field = 8; + fixed32 my_fixed_32_field = 9; + fixed64 my_fixed_64_field = 10; + sfixed32 my_sfixed_32_field = 11; + sfixed64 my_sfixed_64_field = 12; + bool my_bool_field = 13; + string my_string_field = 14; + bytes my_bytes_field = 15; + MyEnum my_enum_field = 16; + google.protobuf.Any my_any_field = 17; + oneof my_oneof_field { + string option_1 = 18; + google.protobuf.Any option_2 = 19; + } + map my_map_field = 20; + repeated string my_string_list_field = 21; + repeated google.protobuf.Any my_any_list_field = 22; + google.type.Date my_date_field = 23; + TestNestedExternalMessage my_nested_ext_msg = 24; + + message TestNestedInternalMessage { + string foo = 1; + int32 bar = 2; + } + + TestNestedInternalMessage my_nested_int_msg = 25; + YetAnotherTestNestedExternalMessage my_yet_another_test_nested_external_msg = 26; +} diff --git a/test/cases_test.go b/test/cases_test.go new file mode 100644 index 0000000..0807b2e --- /dev/null +++ b/test/cases_test.go @@ -0,0 +1,122 @@ +package test + +import ( + "reflect" + "strings" + "testing" + + "github.com/iancoleman/strcase" + "github.com/idodod/protoc-gen-fieldmask/test/gen/cases" + "github.com/idodod/protoc-gen-fieldmask/test/gen/cases/a" + "github.com/idodod/protoc-gen-fieldmask/test/gen/cases/b" + "github.com/idodod/protoc-gen-fieldmask/test/gen/cases/thirdpartyimport" + "github.com/stretchr/testify/suite" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/fieldmaskpb" +) + +type TestSuite struct { + suite.Suite + maxDepth int +} + +func TestTestSuite(t *testing.T) { + suite.Run(t, new(TestSuite)) +} + +func (s *TestSuite) SetupTest() { + s.maxDepth = 100 +} + +func (s *TestSuite) TestTypes() { + testCases := map[string]struct { + msg proto.Message + }{ + "all field types get a fieldmask path": {msg: &cases.Foo{}}, + "an external nested message also gets fieldmasks when it's a parent message": {msg: &cases.TestNestedExternalMessage{}}, + "an internal nested message also gets fieldmasks when it's a parent message": {msg: &cases.Foo_TestNestedInternalMessage{}}, + "an external nested message from another file also gets fieldmasks when it's a parent message": {msg: &cases.YetAnotherTestNestedExternalMessage{}}, + "messages with the same name and a different package get fieldmasks (a.Foo, 1/2)": {msg: &a.Foo{}}, + "messages with the same name and a different package get fieldmasks (b.Foo, 2/2)": {msg: &b.Foo{}}, + "messages from different proto files, in the same package can get fieldmask for 3rd-parties (1/2)": {msg: &thirdpartyimport.FooA{}}, + "messages from different proto files, in the same package can get fieldmask for 3rd-parties (2/2)": {msg: &thirdpartyimport.FooB{}}, + "recursive message works": {msg: &cases.Node{}}, + } + + for name, testCase := range testCases { + s.Run(name, func() { + msgTypeCountMap := make(map[string]int) + msgPtr := testCase.msg + msgPtrType := reflect.TypeOf(msgPtr) + fmPaths, res := msgPtrType.MethodByName("FieldMaskPaths") + s.Run("FieldMaskPaths exists", func() { + s.Require().True(res) + }) + fmVal := fmPaths.Func.Call([]reflect.Value{reflect.ValueOf(msgPtr)})[0] + ref := fmVal.Type() + s.Run("FieldMaskPaths does not have a String method", func() { + _, res := ref.MethodByName("String") + s.Assert().False(res) + }) + + s.Run("All fields have valid paths", func() { + paths := s.collectAndAssertPaths("", msgPtrType, fmVal, msgTypeCountMap) + s.Assert().NotZero(len(paths), "number of paths cannot be zero") + fm, err := fieldmaskpb.New(msgPtr, paths...) + s.Require().NoError(err) + s.T().Log(fm) + }) + }) + } +} + +func (s *TestSuite) collectAndAssertPaths(parent string, protoMessagePtrType reflect.Type, fieldMaskValue reflect.Value, typesMap map[string]int) []string { + el := protoMessagePtrType.Elem() + name := el.PkgPath() + "." + el.Name() + if c, exists := typesMap[name]; exists && c > s.maxDepth { + return nil + } + typesMap[name]++ + + var paths []string + for i := 0; i < protoMessagePtrType.NumMethod(); i++ { + method := protoMessagePtrType.Method(i) + if !method.IsExported() || !strings.HasPrefix(method.Name, "Get") { + continue + } + funcType := method.Func.Type() + if funcType.NumOut() != 1 { + continue + } + + out := funcType.Out(0) + if out.Kind() == reflect.Interface { + // skipping oneof's + continue + } + fieldMaskMethodName := strings.TrimPrefix(method.Name, "Get") + m := fieldMaskValue.MethodByName(fieldMaskMethodName) + s.Assert().False(m.IsZero()) + s.Assert().Zero(m.Type().NumIn()) + s.Assert().Equal(1, m.Type().NumOut()) + outType := m.Type().Out(0) + expected := strcase.ToSnake(fieldMaskMethodName) + if parent != "" { + expected = parent + "." + expected + } + fv := m.Call(nil)[0] + var res string + if outType.Kind() == reflect.String { + res = fv.String() + s.Assert().Equal(expected, res) + } else if outType.Kind() == reflect.Ptr { + strMethod, exists := outType.MethodByName("String") + s.Assert().True(exists, "String method does not exists for %s", fieldMaskMethodName) + res = strMethod.Func.Call([]reflect.Value{fv})[0].String() + s.Assert().Equal(expected, res) + paths = append(paths, s.collectAndAssertPaths(res, out, fv, typesMap)...) + } + paths = append(paths, res) + } + return paths +}