├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── ci.yml │ └── tagpr.yml ├── .gitignore ├── .octocov.yml ├── .tagpr ├── CHANGELOG.md ├── CREDITS ├── LICENSE ├── Makefile ├── README.md ├── bidistreaming_test.go ├── clientstreaming_test.go ├── dynamic.go ├── dynamic_test.go ├── go.mod ├── go.sum ├── grpcstub.go ├── grpcstub_test.go ├── option.go ├── serverstreaming_test.go ├── testdata ├── bsr │ └── protobuf │ │ ├── buf.gen.yaml │ │ ├── buf.lock │ │ ├── buf.yaml │ │ ├── gen │ │ └── go │ │ │ └── pinger │ │ │ ├── pinger.pb.go │ │ │ ├── pinger_grpc.pb.go │ │ │ └── pingerconnect │ │ │ └── pinger.connect.go │ │ └── pinger │ │ └── pinger.proto ├── hello.proto ├── hello │ ├── hello.pb.go │ └── hello_grpc.pb.go ├── openssl.cnf ├── request_stringer_0.golden ├── request_stringer_1.golden ├── route_guide.proto └── routeguide │ ├── route_guide.pb.go │ └── route_guide_grpc.pb.go └── unary_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | groups: 7 | dependencies: 8 | patterns: 9 | - "*" 10 | schedule: 11 | interval: "weekly" 12 | time: "08:00" 13 | timezone: "Asia/Tokyo" 14 | commit-message: 15 | prefix: "chore" 16 | include: "scope" 17 | open-pull-requests-limit: 10 18 | assignees: 19 | - "k1LoW" 20 | 21 | - package-ecosystem: "gomod" 22 | directory: "/" 23 | groups: 24 | dependencies: 25 | patterns: 26 | - "*" 27 | schedule: 28 | interval: "weekly" 29 | time: "08:00" 30 | timezone: "Asia/Tokyo" 31 | commit-message: 32 | prefix: "chore" 33 | include: "scope" 34 | open-pull-requests-limit: 10 35 | assignees: 36 | - "k1LoW" 37 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - tagpr 5 | categories: 6 | - title: Breaking Changes 🛠 7 | labels: 8 | - breaking-change 9 | - title: New Features 🎉 10 | labels: 11 | - enhancement 12 | - title: Fix bug 🐛 13 | labels: 14 | - bug 15 | - title: Other Changes 16 | labels: 17 | - "*" 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | job-test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | steps: 16 | - name: Check out source code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version-file: go.mod 23 | 24 | - name: Check oldstable 25 | uses: k1LoW/oldstable@v1 26 | 27 | - name: Install Protoc 28 | uses: arduino/setup-protoc@v3 29 | 30 | - name: Setup 31 | run: make testclient 32 | 33 | - name: Run lint 34 | uses: reviewdog/action-golangci-lint@v2 35 | with: 36 | fail_on_error: true 37 | go_version_file: go.mod 38 | golangci_lint_flags: --timeout=5m 39 | 40 | - name: Run tests 41 | run: make ci 42 | 43 | - name: Run octocov 44 | uses: k1LoW/octocov-action@v1 45 | -------------------------------------------------------------------------------- /.github/workflows/tagpr.yml: -------------------------------------------------------------------------------- 1 | name: tagpr 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | tagpr: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | tagpr-tag: ${{ steps.run-tagpr.outputs.tag }} 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | steps: 15 | - name: Check out source code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: go.mod 22 | 23 | - id: run-tagpr 24 | name: Run tagpr 25 | uses: Songmu/tagpr@v1 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | testdata/*.pem 2 | testdata/*.srl 3 | coverage.out 4 | -------------------------------------------------------------------------------- /.octocov.yml: -------------------------------------------------------------------------------- 1 | # generated by octocov init 2 | coverage: 3 | if: true 4 | codeToTestRatio: 5 | code: 6 | - '**/*.go' 7 | - '!**/*_test.go' 8 | test: 9 | - '**/*_test.go' 10 | testExecutionTime: 11 | if: true 12 | diff: 13 | datastores: 14 | - artifact://${GITHUB_REPOSITORY} 15 | comment: 16 | if: is_pull_request 17 | report: 18 | if: is_default_branch 19 | datastores: 20 | - artifact://${GITHUB_REPOSITORY} 21 | -------------------------------------------------------------------------------- /.tagpr: -------------------------------------------------------------------------------- 1 | # config file for the tagpr in git config format 2 | # The tagpr generates the initial configuration, which you can rewrite to suit your environment. 3 | # CONFIGURATIONS: 4 | # tagpr.releaseBranch 5 | # Generally, it is "main." It is the branch for releases. The pcpr tracks this branch, 6 | # creates or updates a pull request as a release candidate, or tags when they are merged. 7 | # 8 | # tagpr.versionFile 9 | # Versioning file containing the semantic version needed to be updated at release. 10 | # It will be synchronized with the "git tag". 11 | # Often this is a meta-information file such as gemspec, setup.cfg, package.json, etc. 12 | # Sometimes the source code file, such as version.go or Bar.pm, is used. 13 | # If you do not want to use versioning files but only git tags, specify the "-" string here. 14 | # You can specify multiple version files by comma separated strings. 15 | # 16 | # tagpr.vPrefix 17 | # Flag whether or not v-prefix is added to semver when git tagging. (e.g. v1.2.3 if true) 18 | # This is only a tagging convention, not how it is described in the version file. 19 | # 20 | # tagpr.changelog (Optional) 21 | # Flag whether or not changelog is added or changed during the release. 22 | # 23 | # tagpr.command (Optional) 24 | # Command to change files just before release. 25 | # 26 | # tagpr.tmplate (Optional) 27 | # Pull request template in go template format 28 | # 29 | # tagpr.release (Optional) 30 | # GitHub Release creation behavior after tagging [true, draft, false] 31 | # If this value is not set, the release is to be created. 32 | [tagpr] 33 | vPrefix = true 34 | releaseBranch = main 35 | versionFile = - 36 | command = "make prerelease_for_tagpr" 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.25.9](https://github.com/k1LoW/grpcstub/compare/v0.25.8...v0.25.9) - 2025-04-28 4 | ### Other Changes 5 | - chore(deps): bump the dependencies group with 3 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/152 6 | - chore(deps): bump the dependencies group with 2 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/154 7 | - fix: lint warn by @k1LoW in https://github.com/k1LoW/grpcstub/pull/157 8 | - chore(deps): bump the dependencies group across 1 directory with 3 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/156 9 | 10 | ## [v0.25.8](https://github.com/k1LoW/grpcstub/compare/v0.25.7...v0.25.8) - 2025-03-10 11 | ### Other Changes 12 | - chore(deps): bump the dependencies group across 1 directory with 4 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/150 13 | 14 | ## [v0.25.7](https://github.com/k1LoW/grpcstub/compare/v0.25.6...v0.25.7) - 2025-02-17 15 | ### Other Changes 16 | - chore(deps): bump the dependencies group across 1 directory with 3 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/146 17 | - chore(deps): bump golang.org/x/net from 0.34.0 to 0.35.0 in the dependencies group by @dependabot in https://github.com/k1LoW/grpcstub/pull/148 18 | 19 | ## [v0.25.6](https://github.com/k1LoW/grpcstub/compare/v0.25.5...v0.25.6) - 2025-01-29 20 | ### Other Changes 21 | - chore(deps): bump the dependencies group with 6 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/143 22 | 23 | ## [v0.25.5](https://github.com/k1LoW/grpcstub/compare/v0.25.4...v0.25.5) - 2025-01-20 24 | ### Other Changes 25 | - chore(deps): bump the dependencies group with 2 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/140 26 | - chore(deps): bump the dependencies group with 4 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/142 27 | 28 | ## [v0.25.4](https://github.com/k1LoW/grpcstub/compare/v0.25.3...v0.25.4) - 2025-01-01 29 | ### Other Changes 30 | - chore(deps): bump the dependencies group with 4 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/137 31 | 32 | ## [v0.25.3](https://github.com/k1LoW/grpcstub/compare/v0.25.2...v0.25.3) - 2024-12-23 33 | ### Other Changes 34 | - chore(deps): bump the dependencies group with 4 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/135 35 | 36 | ## [v0.25.2](https://github.com/k1LoW/grpcstub/compare/v0.25.1...v0.25.2) - 2024-12-16 37 | ### Other Changes 38 | - chore(deps): bump google.golang.org/grpc from 1.68.1 to 1.69.0 in the dependencies group by @dependabot in https://github.com/k1LoW/grpcstub/pull/133 39 | 40 | ## [v0.25.1](https://github.com/k1LoW/grpcstub/compare/v0.25.0...v0.25.1) - 2024-12-14 41 | ### Other Changes 42 | - chore(deps): bump the dependencies group across 1 directory with 5 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/131 43 | 44 | ## [v0.25.0](https://github.com/k1LoW/grpcstub/compare/v0.24.1...v0.25.0) - 2024-11-24 45 | ### New Features 🎉 46 | - Add Prepend() for prepending mathcher by @k1LoW in https://github.com/k1LoW/grpcstub/pull/128 47 | ### Other Changes 48 | - chore(deps): bump the dependencies group across 1 directory with 5 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/127 49 | 50 | ## [v0.24.1](https://github.com/k1LoW/grpcstub/compare/v0.24.0...v0.24.1) - 2024-10-07 51 | ### Other Changes 52 | - chore(deps): bump the dependencies group with 2 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/123 53 | 54 | ## [v0.24.0](https://github.com/k1LoW/grpcstub/compare/v0.23.1...v0.24.0) - 2024-09-22 55 | ### Other Changes 56 | - chore(deps): bump the dependencies group with 2 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/117 57 | - Update pkgs by @k1LoW in https://github.com/k1LoW/grpcstub/pull/121 58 | - chore(deps): bump the dependencies group across 1 directory with 3 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/122 59 | 60 | ## [v0.23.1](https://github.com/k1LoW/grpcstub/compare/v0.23.0...v0.23.1) - 2024-08-22 61 | ### Other Changes 62 | - Set oldstable by @k1LoW in https://github.com/k1LoW/grpcstub/pull/116 63 | - chore(deps): bump golang.org/x/net from 0.27.0 to 0.28.0 in the dependencies group by @dependabot in https://github.com/k1LoW/grpcstub/pull/115 64 | 65 | ## [v0.23.0](https://github.com/k1LoW/grpcstub/compare/v0.22.7...v0.23.0) - 2024-07-24 66 | ### New Features 🎉 67 | - Add MarshalProtoMessage / UnmarshalProtoMessage to grpcstub by @k1LoW in https://github.com/k1LoW/grpcstub/pull/112 68 | ### Other Changes 69 | - Update go and mod by @k1LoW in https://github.com/k1LoW/grpcstub/pull/113 70 | 71 | ## [v0.22.7](https://github.com/k1LoW/grpcstub/compare/v0.22.6...v0.22.7) - 2024-07-08 72 | ### Other Changes 73 | - chore(deps): bump the dependencies group with 2 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/108 74 | 75 | ## [v0.22.6](https://github.com/k1LoW/grpcstub/compare/v0.22.5...v0.22.6) - 2024-06-20 76 | ### Fix bug 🐛 77 | - Update bufresolv by @k1LoW in https://github.com/k1LoW/grpcstub/pull/106 78 | 79 | ## [v0.22.5](https://github.com/k1LoW/grpcstub/compare/v0.22.4...v0.22.5) - 2024-06-17 80 | ### Fix bug 🐛 81 | - Update bufresolv by @k1LoW in https://github.com/k1LoW/grpcstub/pull/104 82 | 83 | ## [v0.22.4](https://github.com/k1LoW/grpcstub/compare/v0.22.3...v0.22.4) - 2024-06-17 84 | ### New Features 🎉 85 | - Support buf v2 configration files. by @k1LoW in https://github.com/k1LoW/grpcstub/pull/102 86 | 87 | ## [v0.22.3](https://github.com/k1LoW/grpcstub/compare/v0.22.2...v0.22.3) - 2024-06-17 88 | ### Other Changes 89 | - chore(deps): bump golang.org/x/net from 0.24.0 to 0.25.0 in the dependencies group by @dependabot in https://github.com/k1LoW/grpcstub/pull/96 90 | - chore(deps): bump the dependencies group with 3 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/98 91 | - chore(deps): bump the dependencies group across 1 directory with 3 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/101 92 | 93 | ## [v0.22.2](https://github.com/k1LoW/grpcstub/compare/v0.22.1...v0.22.2) - 2024-05-20 94 | ### Other Changes 95 | - chore(deps): bump google.golang.org/protobuf from 1.34.0 to 1.34.1 in the dependencies group by @dependabot in https://github.com/k1LoW/grpcstub/pull/93 96 | - chore(deps): bump google.golang.org/grpc from 1.63.2 to 1.64.0 in the dependencies group by @dependabot in https://github.com/k1LoW/grpcstub/pull/94 97 | - Add test for connectrpc by @k1LoW in https://github.com/k1LoW/grpcstub/pull/95 98 | 99 | ## [v0.22.1](https://github.com/k1LoW/grpcstub/compare/v0.22.0...v0.22.1) - 2024-05-06 100 | ### Other Changes 101 | - chore(deps): bump github.com/k1LoW/bufresolv from 0.6.1 to 0.6.3 in the dependencies group by @dependabot in https://github.com/k1LoW/grpcstub/pull/90 102 | 103 | ## [v0.22.0](https://github.com/k1LoW/grpcstub/compare/v0.21.0...v0.22.0) - 2024-05-04 104 | ### Breaking Changes 🛠 105 | - Organize options by @k1LoW in https://github.com/k1LoW/grpcstub/pull/89 106 | 107 | ## [v0.21.0](https://github.com/k1LoW/grpcstub/compare/v0.20.0...v0.21.0) - 2024-05-04 108 | ### Breaking Changes 🛠 109 | - Use protoresolv by @k1LoW in https://github.com/k1LoW/grpcstub/pull/85 110 | - Second argument of the NewServer function to allow intelligent FileDescripter resolution. by @k1LoW in https://github.com/k1LoW/grpcstub/pull/87 111 | 112 | ## [v0.20.0](https://github.com/k1LoW/grpcstub/compare/v0.19.0...v0.20.0) - 2024-05-04 113 | ### Breaking Changes 🛠 114 | - Use bufresolv by @k1LoW in https://github.com/k1LoW/grpcstub/pull/83 115 | 116 | ## [v0.19.0](https://github.com/k1LoW/grpcstub/compare/v0.18.0...v0.19.0) - 2024-04-29 117 | ### Other Changes 118 | - Update pkgs by @k1LoW in https://github.com/k1LoW/grpcstub/pull/79 119 | - chore(deps): bump the dependencies group with 2 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/80 120 | - chore(deps): bump the dependencies group with 2 updates by @dependabot in https://github.com/k1LoW/grpcstub/pull/81 121 | 122 | ## [v0.18.0](https://github.com/k1LoW/grpcstub/compare/v0.17.4...v0.18.0) - 2024-04-28 123 | ### New Features 🎉 124 | - Support Buf Schema Registry by @k1LoW in https://github.com/k1LoW/grpcstub/pull/77 125 | 126 | ## [v0.17.4](https://github.com/k1LoW/grpcstub/compare/v0.17.3...v0.17.4) - 2024-04-19 127 | ### Other Changes 128 | - Bump golang.org/x/net from 0.21.0 to 0.23.0 by @dependabot in https://github.com/k1LoW/grpcstub/pull/75 129 | 130 | ## [v0.17.2](https://github.com/k1LoW/grpcstub/compare/v0.17.1...v0.17.2) - 2024-04-15 131 | ### Fix bug 🐛 132 | - Fix race condition by @k1LoW in https://github.com/k1LoW/grpcstub/pull/74 133 | ### Other Changes 134 | - Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 by @dependabot in https://github.com/k1LoW/grpcstub/pull/72 135 | 136 | ## [v0.17.1](https://github.com/k1LoW/grpcstub/compare/v0.17.0...v0.17.1) - 2024-02-16 137 | ### Other Changes 138 | - Use v0.4.0 until https://github.com/tenntenn/golden/pull/16 is fixed. by @k1LoW in https://github.com/k1LoW/grpcstub/pull/70 139 | 140 | ## [v0.17.0](https://github.com/k1LoW/grpcstub/compare/v0.16.0...v0.17.0) - 2024-02-15 141 | ### Other Changes 142 | - Update pkgs by @k1LoW in https://github.com/k1LoW/grpcstub/pull/68 143 | 144 | ## [v0.16.0](https://github.com/k1LoW/grpcstub/compare/v0.15.3...v0.16.0) - 2024-01-12 145 | ### Breaking Changes 🛠 146 | - Fix to accept *testing.B by @k1LoW in https://github.com/k1LoW/grpcstub/pull/67 147 | 148 | ## [v0.15.3](https://github.com/k1LoW/grpcstub/compare/v0.15.2...v0.15.3) - 2023-10-30 149 | ### New Features 🎉 150 | - Add methods for clearing by @k1LoW in https://github.com/k1LoW/grpcstub/pull/64 151 | 152 | ## [v0.15.2](https://github.com/k1LoW/grpcstub/compare/v0.15.1...v0.15.2) - 2023-10-26 153 | ### Other Changes 154 | - Bump google.golang.org/grpc from 1.57.0 to 1.57.1 by @dependabot in https://github.com/k1LoW/grpcstub/pull/62 155 | 156 | ## [v0.15.1](https://github.com/k1LoW/grpcstub/compare/v0.15.0...v0.15.1) - 2023-10-12 157 | ### Other Changes 158 | - Bump golang.org/x/net from 0.9.0 to 0.17.0 by @dependabot in https://github.com/k1LoW/grpcstub/pull/61 159 | 160 | ## [v0.15.0](https://github.com/k1LoW/grpcstub/compare/v0.14.0...v0.15.0) - 2023-09-25 161 | ### New Features 🎉 162 | - Response() supports accepting any value by @k1LoW in https://github.com/k1LoW/grpcstub/pull/58 163 | 164 | ## [v0.14.0](https://github.com/k1LoW/grpcstub/compare/v0.13.0...v0.14.0) - 2023-09-22 165 | ### Breaking Changes 🛠 166 | - Requests() returns matched requests only and UnmatchedRequests() returns unmatched requests by @k1LoW in https://github.com/k1LoW/grpcstub/pull/56 167 | ### New Features 🎉 168 | - Add Stringer for Request type by @k1LoW in https://github.com/k1LoW/grpcstub/pull/57 169 | 170 | ## [v0.13.0](https://github.com/k1LoW/grpcstub/compare/v0.12.0...v0.13.0) - 2023-09-21 171 | ### Breaking Changes 🛠 172 | - Fix resolvePaths() to handle relative paths correctly by @k1LoW in https://github.com/k1LoW/grpcstub/pull/53 173 | 174 | ## [v0.12.0](https://github.com/k1LoW/grpcstub/compare/v0.11.1...v0.12.0) - 2023-09-15 175 | ### Breaking Changes 🛠 176 | - Use github.com/bufbuild/protocompile instead of github.com/jhump/protoreflect by @k1LoW in https://github.com/k1LoW/grpcstub/pull/51 177 | 178 | ## [v0.11.1](https://github.com/k1LoW/grpcstub/compare/v0.11.0...v0.11.1) - 2023-06-08 179 | ### Other Changes 180 | - Add DisableReflection option by @k1LoW in https://github.com/k1LoW/grpcstub/pull/49 181 | 182 | ## [v0.11.0](https://github.com/k1LoW/grpcstub/compare/v0.10.2...v0.11.0) - 2023-06-07 183 | ### Breaking Changes 🛠 184 | - Use google.golang.org/protobuf/reflect/protoreflect instead of github.com/jhump/protoreflect by @k1LoW in https://github.com/k1LoW/grpcstub/pull/48 185 | 186 | ## [v0.10.2](https://github.com/k1LoW/grpcstub/compare/v0.10.1...v0.10.2) - 2023-05-17 187 | - Change health check service name by @k1LoW in https://github.com/k1LoW/grpcstub/pull/45 188 | 189 | ## [v0.10.1](https://github.com/k1LoW/grpcstub/compare/v0.10.0...v0.10.1) - 2023-05-17 190 | - Add `flipflop` service for grpc.health.v1.Health.Watch stubbing by @k1LoW in https://github.com/k1LoW/grpcstub/pull/43 191 | 192 | ## [v0.10.0](https://github.com/k1LoW/grpcstub/compare/v0.9.0...v0.10.0) - 2023-05-17 193 | - Revert https://github.com/k1LoW/grpcstub/pull/32 by @k1LoW in https://github.com/k1LoW/grpcstub/pull/42 194 | 195 | ## [v0.9.0](https://github.com/k1LoW/grpcstub/compare/v0.8.1...v0.9.0) - 2023-05-17 196 | - Add EnableHealthCheck option by @k1LoW in https://github.com/k1LoW/grpcstub/pull/39 197 | 198 | ## [v0.8.1](https://github.com/k1LoW/grpcstub/compare/v0.8.0...v0.8.1) - 2023-03-19 199 | - Allow direct use of ResponseDynamic by @k1LoW in https://github.com/k1LoW/grpcstub/pull/37 200 | 201 | ## [v0.8.0](https://github.com/k1LoW/grpcstub/compare/v0.7.0...v0.8.0) - 2023-03-19 202 | - Add utility methods that embedds fmt.Sprintf by @k1LoW in https://github.com/k1LoW/grpcstub/pull/28 203 | - Add ImportPath option by @k1LoW in https://github.com/k1LoW/grpcstub/pull/30 204 | - Support dynamic response by @k1LoW in https://github.com/k1LoW/grpcstub/pull/31 205 | - Cast time.Time to timestamppb.Timestamp by @k1LoW in https://github.com/k1LoW/grpcstub/pull/32 206 | - Support ResponseDynamic to `repeated` by @k1LoW in https://github.com/k1LoW/grpcstub/pull/33 207 | - Support ResponseDynamic to `optional` by @k1LoW in https://github.com/k1LoW/grpcstub/pull/34 208 | - Remove methods of Message by @k1LoW in https://github.com/k1LoW/grpcstub/pull/35 209 | - Support custom generator for ResponseDynamic by @k1LoW in https://github.com/k1LoW/grpcstub/pull/36 210 | 211 | ## [v0.7.0](https://github.com/k1LoW/grpcstub/compare/v0.6.2...v0.7.0) - 2023-03-17 212 | - [BREAKING] Support dir path for `Proto` by @k1LoW in https://github.com/k1LoW/grpcstub/pull/25 213 | - [BREAKING] Skip registration of conflicted descriptors by @k1LoW in https://github.com/k1LoW/grpcstub/pull/27 214 | 215 | ## [v0.6.2](https://github.com/k1LoW/grpcstub/compare/v0.6.1...v0.6.2) - 2023-03-07 216 | - Bump golang.org/x/text from 0.3.3 to 0.3.8 by @dependabot in https://github.com/k1LoW/grpcstub/pull/22 217 | - Bump golang.org/x/net from 0.0.0-20201021035429-f5854403a974 to 0.7.0 by @dependabot in https://github.com/k1LoW/grpcstub/pull/24 218 | 219 | ## [v0.6.1](https://github.com/k1LoW/grpcstub/compare/v0.6.0...v0.6.1) - 2022-10-09 220 | - Always keep file paths unique by @k1LoW in https://github.com/k1LoW/grpcstub/pull/20 221 | 222 | ## [v0.6.0](https://github.com/k1LoW/grpcstub/compare/v0.5.1...v0.6.0) - 2022-10-09 223 | - [BREAKING] Add Option and Change function signature of NewServer() by @k1LoW in https://github.com/k1LoW/grpcstub/pull/17 224 | 225 | ## [v0.5.1](https://github.com/k1LoW/grpcstub/compare/v0.5.0...v0.5.1) - 2022-10-09 226 | - Use tagpr by @k1LoW in https://github.com/k1LoW/grpcstub/pull/15 227 | 228 | ## [v0.5.0](https://github.com/k1LoW/grpcstub/compare/v0.4.0...v0.5.0) (2022-07-15) 229 | 230 | * Support TLS [#14](https://github.com/k1LoW/grpcstub/pull/14) ([k1LoW](https://github.com/k1LoW)) 231 | * Add Server.ClientConn as alias [#13](https://github.com/k1LoW/grpcstub/pull/13) ([k1LoW](https://github.com/k1LoW)) 232 | 233 | ## [v0.4.0](https://github.com/k1LoW/grpcstub/compare/v0.3.0...v0.4.0) (2022-07-10) 234 | 235 | * gRPC conn close before server close [#12](https://github.com/k1LoW/grpcstub/pull/12) ([k1LoW](https://github.com/k1LoW)) 236 | * Fix grpc.Dial option [#11](https://github.com/k1LoW/grpcstub/pull/11) ([k1LoW](https://github.com/k1LoW)) 237 | 238 | ## [v0.3.0](https://github.com/k1LoW/grpcstub/compare/v0.2.4...v0.3.0) (2022-07-06) 239 | 240 | * Only the first response sends Header in bidirectional streaming [#10](https://github.com/k1LoW/grpcstub/pull/10) ([k1LoW](https://github.com/k1LoW)) 241 | 242 | ## [v0.2.4](https://github.com/k1LoW/grpcstub/compare/v0.2.3...v0.2.4) (2022-07-05) 243 | 244 | * Fix handle client streaming [#9](https://github.com/k1LoW/grpcstub/pull/9) ([k1LoW](https://github.com/k1LoW)) 245 | 246 | ## [v0.2.3](https://github.com/k1LoW/grpcstub/compare/v0.2.2...v0.2.3) (2022-07-04) 247 | 248 | * Fix keys convert: use OrigName option [#8](https://github.com/k1LoW/grpcstub/pull/8) ([k1LoW](https://github.com/k1LoW)) 249 | * The keys of the parameters of the recorded request message should be the same as in the proto file. [#7](https://github.com/k1LoW/grpcstub/pull/7) ([k1LoW](https://github.com/k1LoW)) 250 | * Use encoding/json [#6](https://github.com/k1LoW/grpcstub/pull/6) ([k1LoW](https://github.com/k1LoW)) 251 | 252 | ## [v0.2.2](https://github.com/k1LoW/grpcstub/compare/v0.2.1...v0.2.2) (2022-07-03) 253 | 254 | * Fix register desc [#5](https://github.com/k1LoW/grpcstub/pull/5) ([k1LoW](https://github.com/k1LoW)) 255 | 256 | ## [v0.2.1](https://github.com/k1LoW/grpcstub/compare/v0.2.0...v0.2.1) (2022-07-03) 257 | 258 | * Resolve relative proto paths for reflection [#4](https://github.com/k1LoW/grpcstub/pull/4) ([k1LoW](https://github.com/k1LoW)) 259 | 260 | ## [v0.2.0](https://github.com/k1LoW/grpcstub/compare/v0.1.1...v0.2.0) (2022-07-03) 261 | 262 | * Add Server.Addr() [#3](https://github.com/k1LoW/grpcstub/pull/3) ([k1LoW](https://github.com/k1LoW)) 263 | 264 | ## [v0.1.1](https://github.com/k1LoW/grpcstub/compare/v0.1.0...v0.1.1) (2022-07-02) 265 | 266 | * Add LICENSE [#2](https://github.com/k1LoW/grpcstub/pull/2) ([k1LoW](https://github.com/k1LoW)) 267 | 268 | ## [v0.1.0](https://github.com/k1LoW/grpcstub/compare/3408f46825de...v0.1.0) (2022-07-02) 269 | 270 | * Add response status handling [#1](https://github.com/k1LoW/grpcstub/pull/1) ([k1LoW](https://github.com/k1LoW)) 271 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2022 Ken'ichiro Oyama 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | ci: depsdev test 4 | 5 | test: cert 6 | go test ./... -coverprofile=coverage.out -covermode=count 7 | 8 | lint: 9 | golangci-lint run ./... 10 | 11 | depsdev: 12 | go install github.com/Songmu/ghch/cmd/ghch@latest 13 | go install github.com/Songmu/gocredits/cmd/gocredits@latest 14 | go install github.com/securego/gosec/v2/cmd/gosec@latest 15 | go install github.com/bufbuild/buf/cmd/buf@latest 16 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 17 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 18 | go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest 19 | 20 | testclient: depsdev 21 | mkdir -p testdata/routeguide 22 | mkdir -p testdata/hello 23 | cd testdata/ && protoc --go_out=routeguide --go_opt=paths=source_relative --go-grpc_out=routeguide --go-grpc_opt=paths=source_relative route_guide.proto 24 | cd testdata/ && protoc --go_out=hello --go_opt=paths=source_relative --go-grpc_out=hello --go-grpc_opt=paths=source_relative hello.proto 25 | cd testdata/bsr/protobuf && buf mod update && buf generate 26 | 27 | cert: 28 | rm -f testdata/*.pem testdata/*.srl 29 | openssl req -x509 -newkey rsa:4096 -days 365 -nodes -sha256 -keyout testdata/cakey.pem -out testdata/cacert.pem -subj "/C=UK/ST=Test State/L=Test Location/O=Test Org/OU=Test Unit/CN=*.example.com/emailAddress=k1low@pepabo.com" 30 | openssl req -newkey rsa:4096 -nodes -keyout testdata/key.pem -out testdata/csr.pem -subj "/C=JP/ST=Test State/L=Test Location/O=Test Org/OU=Test Unit/CN=*.example.com/emailAddress=k1low@pepabo.com" 31 | openssl x509 -req -sha256 -in testdata/csr.pem -days 60 -CA testdata/cacert.pem -CAkey testdata/cakey.pem -CAcreateserial -out testdata/cert.pem -extfile testdata/openssl.cnf 32 | openssl verify -CAfile testdata/cacert.pem testdata/cert.pem 33 | 34 | prerelease: 35 | git pull origin main --tag 36 | go mod tidy 37 | ghch -w -N ${VER} 38 | gocredits -w . 39 | git add CHANGELOG.md CREDITS go.mod go.sum 40 | git commit -m'Bump up version number' 41 | git tag ${VER} 42 | 43 | prerelease_for_tagpr: depsdev 44 | go mod tidy 45 | gocredits -w . 46 | git add CHANGELOG.md CREDITS go.mod go.sum 47 | 48 | release: 49 | git push origin main --tag 50 | 51 | .PHONY: default test 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grpcstub [![Go Reference](https://pkg.go.dev/badge/github.com/k1LoW/grpcstub.svg)](https://pkg.go.dev/github.com/k1LoW/grpcstub) ![Coverage](https://raw.githubusercontent.com/k1LoW/octocovs/main/badges/k1LoW/grpcstub/coverage.svg) ![Code to Test Ratio](https://raw.githubusercontent.com/k1LoW/octocovs/main/badges/k1LoW/grpcstub/ratio.svg) ![Test Execution Time](https://raw.githubusercontent.com/k1LoW/octocovs/main/badges/k1LoW/grpcstub/time.svg) 2 | 3 | grpcstub provides gRPC server and client conn ( `*grpc.ClientConn` ) for stubbing, for testing in Go. 4 | 5 | There is an HTTP version stubbing tool with the same design concept, [httpstub](https://github.com/k1LoW/httpstub). 6 | 7 | ## Usage 8 | 9 | ``` go 10 | package myapp 11 | 12 | import ( 13 | "io" 14 | "net/http" 15 | "testing" 16 | 17 | "github.com/k1LoW/grpcstub" 18 | "github.com/k1LoW/myapp/protobuf/gen/go/routeguide" 19 | ) 20 | 21 | func TestClient(t *testing.T) { 22 | ctx := context.Background() 23 | ts := grpcstub.NewServer(t, "path/to/protobuf") 24 | t.Cleanup(func() { 25 | ts.Close() 26 | }) 27 | ts.Method("GetFeature").Response(&routeguite.Feature{ 28 | Name: "hello", 29 | Location: &routeguide.Point{ 30 | Latitude: 10, 31 | Longitude: 13, 32 | }, 33 | }) 34 | // OR 35 | // ts.Method("GetFeature").Response(map[string]any{"name": "hello", "location": map[string]any{"latitude": 10, "longitude": 13}}) 36 | 37 | client := routeguide.NewRouteGuideClient(ts.Conn()) 38 | if _, err := client.GetFeature(ctx, &routeguide.Point{ 39 | Latitude: 10, 40 | Longitude: 13, 41 | }); err != nil { 42 | t.Fatal(err) 43 | } 44 | { 45 | got := len(ts.Requests()) 46 | if want := 1; got != want { 47 | t.Errorf("got %v\nwant %v", got, want) 48 | } 49 | } 50 | req := ts.Requests()[0] 51 | { 52 | got := int32(req.Message["longitude"].(float64)) 53 | if want := int32(13); got != want { 54 | t.Errorf("got %v\nwant %v", got, want) 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ## Dynamic Response 61 | 62 | grpcstub can return responses dynamically using the protocol buffer schema. 63 | 64 | ### Dynamic response to all requests 65 | 66 | ``` go 67 | ts := grpcstub.NewServer(t, "path/to/protobuf") 68 | t.Cleanup(func() { 69 | ts.Close() 70 | }) 71 | ts.ResponseDynamic() 72 | ``` 73 | 74 | ### Dynamic response to a request to a specific method (rpc) 75 | 76 | ``` go 77 | ts := grpcstub.NewServer(t, "path/to/protobuf") 78 | t.Cleanup(func() { 79 | ts.Close() 80 | }) 81 | ts.Service("routeguide.RouteGuide").Method("GetFeature").ResponseDynamic() 82 | ``` 83 | 84 | ### Dynamic response with your own generators 85 | 86 | ``` go 87 | ts := grpcstub.NewServer(t, "path/to/protobuf") 88 | t.Cleanup(func() { 89 | ts.Close() 90 | }) 91 | fk := faker.New() 92 | want := time.Now() 93 | opts := []GeneratorOption{ 94 | Generator("*_id", func(req *grpcstub.Request) any { 95 | return fk.UUID().V4() 96 | }), 97 | Generator("*_time", func(req *grpcstub.Request) any { 98 | return want 99 | }), 100 | } 101 | ts.ResponseDynamic(opts...) 102 | ``` 103 | 104 | ## Test data 105 | 106 | - https://github.com/grpc/grpc-go/blob/master/examples/route_guide/routeguide/route_guide.proto 107 | 108 | ## References 109 | 110 | - [monlabs/grpc-mock](https://github.com/monlabs/grpc-mock): Run a gRPC mock server by using protobuf reflection. 111 | -------------------------------------------------------------------------------- /bidistreaming_test.go: -------------------------------------------------------------------------------- 1 | package grpcstub 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/k1LoW/grpcstub/testdata/routeguide" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/status" 14 | ) 15 | 16 | func TestBidiStreaming(t *testing.T) { 17 | ctx := context.Background() 18 | ts := NewServer(t, "testdata/route_guide.proto") 19 | t.Cleanup(func() { 20 | ts.Close() 21 | }) 22 | ts.Method("RouteChat").Match(func(req *Request) bool { 23 | m, ok := req.Message["message"] 24 | if !ok { 25 | return false 26 | } 27 | return strings.Contains(m.(string), "hello from client[0]") 28 | }).Header("hello", "header"). 29 | Response(map[string]any{"location": nil, "message": "hello from server[0]"}) 30 | ts.Method("RouteChat"). 31 | Header("hello", "header"). 32 | Handler(func(req *Request) *Response { 33 | res := NewResponse() 34 | m, ok := req.Message["message"] 35 | if !ok { 36 | res.Status = status.New(codes.Unknown, codes.Unknown.String()) 37 | return res 38 | } 39 | mes := Message{} 40 | mes["message"] = strings.Replace(m.(string), "client", "server", 1) 41 | res.Messages = []Message{mes} 42 | return res 43 | }) 44 | 45 | client := routeguide.NewRouteGuideClient(ts.Conn()) 46 | stream, err := client.RouteChat(ctx) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | max := 5 51 | c := 0 52 | recvCount := 0 53 | var sendEnd, recvEnd bool 54 | for !sendEnd || !recvEnd { 55 | if !sendEnd { 56 | if err := stream.SendMsg(&routeguide.RouteNote{ 57 | Message: fmt.Sprintf("hello from client[%d]", c), 58 | }); err != nil { 59 | t.Error(err) 60 | sendEnd = true 61 | } 62 | c++ 63 | if c == max { 64 | sendEnd = true 65 | if err := stream.CloseSend(); err != nil { 66 | t.Error(err) 67 | } 68 | } 69 | } 70 | 71 | if !recvEnd { 72 | if res, err := stream.Recv(); err != nil { 73 | if !errors.Is(err, io.EOF) { 74 | t.Error(err) 75 | } 76 | recvEnd = true 77 | } else { 78 | recvCount++ 79 | got := res.Message 80 | if want := fmt.Sprintf("hello from server[%d]", recvCount-1); got != want { 81 | t.Errorf("got %v\nwant %v", got, want) 82 | } 83 | } 84 | } 85 | } 86 | if recvCount != max { 87 | t.Errorf("got %v\nwant %v", recvCount, max) 88 | } 89 | 90 | { 91 | got := len(ts.Requests()) 92 | if want := max; got != want { 93 | t.Errorf("got %v\nwant %v", got, want) 94 | } 95 | } 96 | } 97 | 98 | func TestBidiStreamingUnmatched(t *testing.T) { 99 | ctx := context.Background() 100 | ts := NewServer(t, "testdata/route_guide.proto") 101 | t.Cleanup(func() { 102 | ts.Close() 103 | }) 104 | ts.Method("RouteChat").Match(func(req *Request) bool { 105 | return false 106 | }).Header("hello", "header"). 107 | Response(map[string]any{"location": nil, "message": "hello from server[0]"}) 108 | 109 | client := routeguide.NewRouteGuideClient(ts.Conn()) 110 | stream, err := client.RouteChat(ctx) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | if err := stream.SendMsg(&routeguide.RouteNote{ 115 | Message: fmt.Sprintf("hello from client[%d]", 0), 116 | }); err != nil { 117 | t.Error("want error") 118 | } 119 | if _, err := stream.Recv(); err == nil { 120 | t.Error("want error") 121 | } 122 | 123 | { 124 | got := len(ts.Requests()) 125 | if want := 0; got != want { 126 | t.Errorf("got %v\nwant %v", got, want) 127 | } 128 | } 129 | 130 | { 131 | got := len(ts.UnmatchedRequests()) 132 | if want := 1; got != want { 133 | t.Errorf("got %v\nwant %v", got, want) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /clientstreaming_test.go: -------------------------------------------------------------------------------- 1 | package grpcstub 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/k1LoW/grpcstub/testdata/routeguide" 8 | ) 9 | 10 | func TestClientStreaming(t *testing.T) { 11 | ctx := context.Background() 12 | ts := NewServer(t, "testdata/route_guide.proto") 13 | t.Cleanup(func() { 14 | ts.Close() 15 | }) 16 | ts.Method("RecordRoute").Response(map[string]any{"point_count": 2, "feature_count": 2, "distance": 10, "elapsed_time": 345}) 17 | 18 | client := routeguide.NewRouteGuideClient(ts.Conn()) 19 | stream, err := client.RecordRoute(ctx) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | c := 2 24 | for i := 0; i < c; i++ { 25 | if err := stream.Send(&routeguide.Point{ 26 | Latitude: int32(i + 10), 27 | Longitude: int32(i * i * 2), 28 | }); err != nil { 29 | t.Fatal(err) 30 | } 31 | } 32 | res, err := stream.CloseAndRecv() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | { 38 | got := res.PointCount 39 | if want := int32(2); got != want { 40 | t.Errorf("got %v\nwant %v", got, want) 41 | } 42 | } 43 | 44 | { 45 | got := len(ts.Requests()) 46 | if want := 2; got != want { 47 | t.Errorf("got %v\nwant %v", got, want) 48 | } 49 | } 50 | } 51 | 52 | func TestClientStreamingUnmatched(t *testing.T) { 53 | ctx := context.Background() 54 | ts := NewServer(t, "testdata/route_guide.proto") 55 | t.Cleanup(func() { 56 | ts.Close() 57 | }) 58 | ts.Method("RecordRoute").Match(func(req *Request) bool { 59 | return false 60 | }).Response(map[string]any{"point_count": 2, "feature_count": 2, "distance": 10, "elapsed_time": 345}) 61 | 62 | client := routeguide.NewRouteGuideClient(ts.Conn()) 63 | stream, err := client.RecordRoute(ctx) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | c := 2 68 | for i := 0; i < c; i++ { 69 | if err := stream.Send(&routeguide.Point{ 70 | Latitude: int32(i + 10), 71 | Longitude: int32(i * i * 2), 72 | }); err != nil { 73 | t.Fatal(err) 74 | } 75 | } 76 | if _, err := stream.CloseAndRecv(); err == nil { 77 | t.Error("want error") 78 | } 79 | 80 | { 81 | got := len(ts.Requests()) 82 | if want := 0; got != want { 83 | t.Errorf("got %v\nwant %v", got, want) 84 | } 85 | } 86 | 87 | { 88 | got := len(ts.UnmatchedRequests()) 89 | if want := 2; got != want { 90 | t.Errorf("got %v\nwant %v", got, want) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /dynamic.go: -------------------------------------------------------------------------------- 1 | package grpcstub 2 | 3 | import ( 4 | "math/rand" 5 | "strings" 6 | "time" 7 | 8 | "github.com/jaswdr/faker" 9 | "github.com/minio/pkg/wildcard" 10 | "google.golang.org/protobuf/reflect/protoreflect" 11 | ) 12 | 13 | var fk = faker.New() 14 | 15 | type generator struct { 16 | pattern string 17 | fn GenerateFunc 18 | } 19 | 20 | type generators []*generator 21 | 22 | func (gs generators) matchFunc(name string) (GenerateFunc, bool) { 23 | for _, g := range gs { 24 | if wildcard.MatchSimple(g.pattern, name) { 25 | return g.fn, true 26 | } 27 | } 28 | return nil, false 29 | } 30 | 31 | type GeneratorOption func(generators) generators 32 | 33 | type GenerateFunc func(req *Request) any 34 | 35 | func Generator(pattern string, fn GenerateFunc) GeneratorOption { 36 | return func(gs generators) generators { 37 | return append(gs, &generator{ 38 | pattern: pattern, 39 | fn: fn, 40 | }) 41 | } 42 | } 43 | 44 | // ResponseDynamic set handler which return dynamic response. 45 | func (m *matcher) ResponseDynamic(opts ...GeneratorOption) *matcher { 46 | const messageMax = 5 47 | gs := generators{} 48 | for _, opt := range opts { 49 | gs = opt(gs) 50 | } 51 | prev := m.handler 52 | m.handler = func(req *Request, md protoreflect.MethodDescriptor) *Response { 53 | var res *Response 54 | if prev == nil { 55 | res = NewResponse() 56 | } else { 57 | res = prev(req, md) 58 | } 59 | if !md.IsStreamingClient() && !md.IsStreamingServer() { 60 | res.Messages = append(res.Messages, generateDynamicMessage(gs, req, md.Output(), nil)) 61 | } else { 62 | for i := 0; i > rand.Intn(messageMax)+1; i++ { 63 | res.Messages = append(res.Messages, generateDynamicMessage(gs, req, md.Output(), nil)) 64 | } 65 | } 66 | return res 67 | } 68 | return m 69 | } 70 | 71 | func generateDynamicMessage(gs generators, req *Request, m protoreflect.MessageDescriptor, parents []string) map[string]any { 72 | const ( 73 | floatMin = 0 74 | floatMax = 10000 75 | wMin = 1 76 | wMax = 25 77 | repeatMax = 5 78 | fieldSep = "." 79 | ) 80 | message := map[string]any{} 81 | 82 | for i := 0; i < m.Fields().Len(); i++ { 83 | f := m.Fields().Get(i) 84 | values := []any{} 85 | l := 1 86 | if f.HasOptionalKeyword() { 87 | l = rand.Intn(2) 88 | } 89 | if f.IsList() { 90 | l = rand.Intn(repeatMax) + l 91 | } 92 | n := string(f.Name()) 93 | names := append(parents, string(n)) 94 | for i := 0; i < l; i++ { 95 | fn, ok := gs.matchFunc(strings.Join(names, fieldSep)) 96 | if ok { 97 | values = append(values, fn(req)) 98 | continue 99 | } 100 | switch f.Kind() { 101 | case protoreflect.DoubleKind, protoreflect.FloatKind: 102 | values = append(values, fk.Float64(1, floatMin, floatMax)) 103 | case protoreflect.Int64Kind, protoreflect.Fixed64Kind, protoreflect.Sfixed64Kind, protoreflect.Sint64Kind: 104 | values = append(values, fk.Int64()) 105 | case protoreflect.Int32Kind, protoreflect.Fixed32Kind, protoreflect.Sfixed32Kind, protoreflect.Sint32Kind: 106 | values = append(values, fk.Int32()) 107 | case protoreflect.Uint64Kind: 108 | values = append(values, fk.UInt64()) 109 | case protoreflect.Uint32Kind: 110 | values = append(values, fk.UInt32()) 111 | case protoreflect.BoolKind: 112 | values = append(values, fk.Bool()) 113 | case protoreflect.StringKind: 114 | values = append(values, fk.Lorem().Sentence(rand.Intn(wMax-wMin+1)+wMin)) 115 | case protoreflect.GroupKind: 116 | // Group type is deprecated and not supported in proto3. 117 | case protoreflect.MessageKind: 118 | if f.Message().FullName() == "google.protobuf.Timestamp" { 119 | // Timestamp is not encoded as a message with seconds and nanos in JSON, instead it is encoded with RFC 3339: 120 | // ref: https://protobuf.dev/programming-guides/proto3/#json 121 | values = append(values, fk.Time().Time(time.Now()).Format(time.RFC3339Nano)) 122 | continue 123 | } 124 | values = append(values, generateDynamicMessage(gs, req, f.Message(), names)) 125 | case protoreflect.BytesKind: 126 | values = append(values, fk.Lorem().Bytes(rand.Intn(wMax-wMin+1)+wMin)) 127 | case protoreflect.EnumKind: 128 | values = append(values, int(f.Enum().Values().Get(0).Number())) 129 | } 130 | } 131 | if f.IsList() { 132 | message[n] = values 133 | } else { 134 | if len(values) > 0 { 135 | message[n] = values[0] 136 | } 137 | } 138 | } 139 | 140 | return message 141 | } 142 | 143 | // ResponseDynamic set handler which return dynamic response. 144 | func (s *Server) ResponseDynamic(opts ...GeneratorOption) *matcher { 145 | m := &matcher{ 146 | matchFuncs: []matchFunc{func(_ *Request) bool { return true }}, 147 | } 148 | s.mu.Lock() 149 | defer s.mu.Unlock() 150 | s.matchers = append(s.matchers, m) 151 | return m.ResponseDynamic(opts...) 152 | } 153 | -------------------------------------------------------------------------------- /dynamic_test.go: -------------------------------------------------------------------------------- 1 | package grpcstub 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/google/go-cmp/cmp/cmpopts" 10 | "github.com/k1LoW/grpcstub/testdata/hello" 11 | "github.com/k1LoW/grpcstub/testdata/routeguide" 12 | ) 13 | 14 | func TestResponseDynamic(t *testing.T) { 15 | ctx := context.Background() 16 | ts := NewServer(t, "testdata/route_guide.proto") 17 | t.Cleanup(func() { 18 | ts.Close() 19 | }) 20 | ts.Method("GetFeature").ResponseDynamic() 21 | want := 5 22 | responses := []*routeguide.Feature{} 23 | for i := 0; i < want; i++ { 24 | client := routeguide.NewRouteGuideClient(ts.Conn()) 25 | res, err := client.GetFeature(ctx, &routeguide.Point{ 26 | Latitude: 10, 27 | Longitude: 13, 28 | }) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | responses = append(responses, res) 33 | } 34 | { 35 | got := len(ts.Requests()) 36 | if got != want { 37 | t.Errorf("got %v\nwant %v", got, want) 38 | } 39 | } 40 | rc := len(responses) 41 | for i := 0; i < rc; i++ { 42 | a := responses[i%rc] 43 | b := responses[(i+1)%rc] 44 | opts := []cmp.Option{ 45 | cmpopts.IgnoreFields(routeguide.Feature{}, "state", "sizeCache", "unknownFields"), 46 | cmpopts.IgnoreFields(routeguide.Point{}, "state", "sizeCache", "unknownFields"), 47 | } 48 | if diff := cmp.Diff(a, b, opts...); diff == "" { 49 | t.Errorf("got same responses: %#v", a) 50 | } 51 | } 52 | } 53 | 54 | func TestResponseDynamicRepeated(t *testing.T) { 55 | ctx := context.Background() 56 | ts := NewServer(t, "testdata/hello.proto") 57 | t.Cleanup(func() { 58 | ts.Close() 59 | }) 60 | ts.Method("Hello").ResponseDynamic() 61 | client := hello.NewGrpcTestServiceClient(ts.Conn()) 62 | res, err := client.Hello(ctx, &hello.HelloRequest{}) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | if len(res.Hellos) == 0 { 67 | t.Error("invalid repeated field value") 68 | } 69 | } 70 | 71 | func TestResponseDynamicGenerated(t *testing.T) { 72 | ctx := context.Background() 73 | ts := NewServer(t, "testdata/hello.proto") 74 | t.Cleanup(func() { 75 | ts.Close() 76 | }) 77 | want := time.Now() 78 | opts := []GeneratorOption{ 79 | Generator("*_time", func(req *Request) any { 80 | return want 81 | }), 82 | } 83 | ts.Method("Hello").ResponseDynamic(opts...) 84 | client := hello.NewGrpcTestServiceClient(ts.Conn()) 85 | res, err := client.Hello(ctx, &hello.HelloRequest{}) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | if res.CreateTime.AsTime().UnixNano() != want.UnixNano() { 90 | t.Errorf("got %v\nwant %v", res.CreateTime.AsTime().UnixNano(), want.UnixNano()) 91 | } 92 | } 93 | 94 | func TestResponseDynamicServer(t *testing.T) { 95 | ctx := context.Background() 96 | ts := NewServer(t, "testdata/hello.proto") 97 | t.Cleanup(func() { 98 | ts.Close() 99 | }) 100 | want := time.Now() 101 | opts := []GeneratorOption{ 102 | Generator("*_time", func(req *Request) any { 103 | return want 104 | }), 105 | } 106 | ts.ResponseDynamic(opts...) 107 | client := hello.NewGrpcTestServiceClient(ts.Conn()) 108 | res, err := client.Hello(ctx, &hello.HelloRequest{}) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | if res.CreateTime.AsTime().UnixNano() != want.UnixNano() { 113 | t.Errorf("got %v\nwant %v", res.CreateTime.AsTime().UnixNano(), want.UnixNano()) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/k1LoW/grpcstub 2 | 3 | go 1.23.8 4 | 5 | require ( 6 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 7 | connectrpc.com/connect v1.18.1 8 | github.com/bmatcuk/doublestar/v4 v4.8.1 9 | github.com/bufbuild/protocompile v0.14.1 10 | github.com/google/go-cmp v0.7.0 11 | github.com/jaswdr/faker v1.19.1 12 | github.com/jhump/protoreflect/v2 v2.0.0-beta.2 13 | github.com/k1LoW/bufresolv v0.7.8 14 | github.com/k1LoW/protoresolv v0.1.7 15 | github.com/minio/pkg v1.7.5 16 | github.com/tenntenn/golden v0.5.4 17 | golang.org/x/net v0.40.0 18 | google.golang.org/grpc v1.72.1 19 | google.golang.org/protobuf v1.36.6 20 | ) 21 | 22 | require ( 23 | github.com/josharian/mapfs v0.0.0-20210615234106-095c008854e6 // indirect 24 | github.com/josharian/txtarfs v0.0.0-20210615234325-77aca6df5bca // indirect 25 | golang.org/x/sync v0.14.0 // indirect 26 | golang.org/x/sys v0.33.0 // indirect 27 | golang.org/x/text v0.25.0 // indirect 28 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 29 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M= 2 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= 3 | connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= 4 | connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= 5 | github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= 6 | github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= 7 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= 8 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 12 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 13 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 14 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 15 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 16 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 17 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 18 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 19 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 20 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 21 | github.com/jaswdr/faker v1.19.1 h1:xBoz8/O6r0QAR8eEvKJZMdofxiRH+F0M/7MU9eNKhsM= 22 | github.com/jaswdr/faker v1.19.1/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w= 23 | github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= 24 | github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= 25 | github.com/josharian/mapfs v0.0.0-20210615234106-095c008854e6 h1:c+ctPFdISggaSNCfU1IueNBAsqetJSvMcpQlT+0OVdY= 26 | github.com/josharian/mapfs v0.0.0-20210615234106-095c008854e6/go.mod h1:Rv/momJI8DgrWnBZip+SgagpcgORIZQE5SERlxNb8LY= 27 | github.com/josharian/txtarfs v0.0.0-20210615234325-77aca6df5bca h1:a8xeK4GsWLE4LYo5VI4u1Cn7ZvT1NtXouXR3DdKLB8Q= 28 | github.com/josharian/txtarfs v0.0.0-20210615234325-77aca6df5bca/go.mod h1:UbC32ft9G/jG+sZI8wLbIBNIrYr7vp/yqMDa9SxVBNA= 29 | github.com/k1LoW/bufresolv v0.7.8 h1:jolkXzCwVxPkWZrzkyZ5AIYKCMUuOrrysIDbQlsC3Z4= 30 | github.com/k1LoW/bufresolv v0.7.8/go.mod h1:47boRXO6C3JLsC04xDKISy9qUVsl8IpUe7NayLE3CWA= 31 | github.com/k1LoW/protoresolv v0.1.7 h1:jlKBERyIM20s/S9pk2DbOnqrSU5hpGSTA7qG5jV0Oqc= 32 | github.com/k1LoW/protoresolv v0.1.7/go.mod h1:QOhK9xINLO15vlemgzJJ0HKfhWx1U+dci6d1LuR7+nw= 33 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 34 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 35 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 36 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 37 | github.com/minio/pkg v1.7.5 h1:UOUJjewE5zoaDPlCMJtNx/swc1jT1ZR+IajT7hrLd44= 38 | github.com/minio/pkg v1.7.5/go.mod h1:mEfGMTm5Z0b5EGxKNuPwyb5A2d+CC/VlUyRj6RJtIwo= 39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 42 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 43 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 44 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 45 | github.com/tenntenn/golden v0.5.4 h1:laddoKuzbzGYVinsSZyEPavPh4muyKd2SMhJTKH3F3s= 46 | github.com/tenntenn/golden v0.5.4/go.mod h1:0xI/4lpoHR65AUTmd1RKR9S1Uv0JR3yR2Q1Ob2bKqQA= 47 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 48 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 49 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 50 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 51 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 52 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 53 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 54 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 55 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 56 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 57 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 58 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 59 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 60 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 61 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 62 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 63 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 64 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 66 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 67 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 68 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 69 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 72 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 73 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 78 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 79 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 80 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 81 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 82 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 83 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 84 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 85 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 86 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 87 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 88 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 89 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 90 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= 92 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= 93 | google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= 94 | google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 95 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 96 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 97 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 98 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 99 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 100 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 101 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | -------------------------------------------------------------------------------- /grpcstub.go: -------------------------------------------------------------------------------- 1 | package grpcstub 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net" 12 | "os" 13 | "path/filepath" 14 | "slices" 15 | "sort" 16 | "strings" 17 | "sync" 18 | "testing" 19 | "time" 20 | 21 | "github.com/bufbuild/protocompile" 22 | "github.com/bufbuild/protocompile/linker" 23 | "github.com/k1LoW/bufresolv" 24 | "github.com/k1LoW/protoresolv" 25 | "google.golang.org/grpc" 26 | "google.golang.org/grpc/codes" 27 | "google.golang.org/grpc/credentials" 28 | "google.golang.org/grpc/credentials/insecure" 29 | "google.golang.org/grpc/health" 30 | healthpb "google.golang.org/grpc/health/grpc_health_v1" 31 | "google.golang.org/grpc/metadata" 32 | "google.golang.org/grpc/reflection" 33 | "google.golang.org/grpc/status" 34 | "google.golang.org/protobuf/encoding/protojson" 35 | "google.golang.org/protobuf/reflect/protoreflect" 36 | "google.golang.org/protobuf/reflect/protoregistry" 37 | "google.golang.org/protobuf/types/dynamicpb" 38 | ) 39 | 40 | type serverStatus int 41 | 42 | const ( 43 | status_unknown serverStatus = iota 44 | status_start 45 | status_starting 46 | status_closing 47 | status_closed 48 | ) 49 | 50 | const ( 51 | HealthCheckService_DEFAULT = "default" 52 | HealthCheckService_FLAPPING = "flapping" 53 | ) 54 | 55 | var _ TB = (testing.TB)(nil) 56 | 57 | type TB interface { 58 | Error(args ...any) 59 | Errorf(format string, args ...any) 60 | Fatal(args ...any) 61 | Fatalf(format string, args ...any) 62 | Helper() 63 | } 64 | 65 | type Message map[string]any 66 | 67 | type Request struct { 68 | Service string 69 | Method string 70 | Headers metadata.MD 71 | Message Message 72 | } 73 | 74 | func (req *Request) String() string { 75 | var s []string 76 | s = append(s, fmt.Sprintf("%s/%s", req.Service, req.Method)) 77 | if len(req.Headers) > 0 { 78 | var keys []string 79 | for k := range req.Headers { 80 | keys = append(keys, k) 81 | } 82 | sort.SliceStable(keys, func(i, j int) bool { 83 | return keys[i] < keys[j] 84 | }) 85 | for _, k := range keys { 86 | s = append(s, fmt.Sprintf(`%s: %s`, k, strings.Join(req.Headers.Get(k), ", "))) 87 | } 88 | } 89 | s = append(s, "") 90 | if req.Message != nil { 91 | b, _ := json.MarshalIndent(req.Message, "", " ") 92 | s = append(s, string(b)) 93 | } 94 | return strings.Join(s, "\n") + "\n" 95 | } 96 | 97 | func newRequest(md protoreflect.MethodDescriptor, message Message) *Request { 98 | service, method := splitMethodFullName(md.FullName()) 99 | return &Request{ 100 | Service: service, 101 | Method: method, 102 | Headers: metadata.MD{}, 103 | Message: message, 104 | } 105 | } 106 | 107 | type Response struct { 108 | Headers metadata.MD 109 | Messages []Message 110 | Trailers metadata.MD 111 | Status *status.Status 112 | } 113 | 114 | // NewResponse returns a new empty response 115 | func NewResponse() *Response { 116 | return &Response{ 117 | Headers: metadata.MD{}, 118 | Messages: []Message{}, 119 | Trailers: metadata.MD{}, 120 | Status: nil, 121 | } 122 | } 123 | 124 | type Server struct { 125 | matchers []*matcher 126 | fds linker.Files 127 | listener net.Listener 128 | server *grpc.Server 129 | tlsc *tls.Config 130 | cacert []byte 131 | cc *grpc.ClientConn 132 | requests []*Request 133 | unmatchedRequests []*Request 134 | healthCheck bool 135 | disableReflection bool 136 | status serverStatus 137 | prependOnce bool 138 | t TB 139 | mu sync.RWMutex 140 | } 141 | 142 | type matcher struct { 143 | matchFuncs []matchFunc 144 | handler handlerFunc 145 | requests []*Request 146 | t TB 147 | mu sync.RWMutex 148 | } 149 | 150 | type matchFunc func(req *Request) bool 151 | type handlerFunc func(req *Request, md protoreflect.MethodDescriptor) *Response 152 | 153 | // NewServer returns a new server with registered *grpc.Server 154 | // protopath is a path of .proto files, import path directory or buf directory. 155 | func NewServer(t TB, protopath string, opts ...Option) *Server { 156 | t.Helper() 157 | ctx := context.Background() 158 | c := &config{} 159 | if protopath != "" { 160 | if fi, err := os.Stat(protopath); err == nil && fi.IsDir() { 161 | if _, err := os.Stat(filepath.Join(protopath, "buf.yaml")); err == nil { 162 | opts = append(opts, BufDir(protopath)) 163 | } else if _, err := os.Stat(filepath.Join(protopath, "buf.lock")); err == nil { 164 | opts = append(opts, BufDir(protopath)) 165 | } else if _, err := os.Stat(filepath.Join(protopath, "buf.work.yaml")); err == nil { 166 | opts = append(opts, BufDir(protopath)) 167 | } else { 168 | opts = append(opts, ImportPath(protopath)) 169 | } 170 | } else { 171 | opts = append(opts, Proto(protopath)) 172 | } 173 | } 174 | for _, opt := range opts { 175 | if err := opt(c); err != nil { 176 | t.Fatal(err) 177 | } 178 | } 179 | s := &Server{ 180 | t: t, 181 | healthCheck: c.healthCheck, 182 | disableReflection: c.disableReflection, 183 | } 184 | if err := s.resolveProtos(ctx, c); err != nil { 185 | t.Fatal(err) 186 | } 187 | if c.useTLS { 188 | certificate, err := tls.X509KeyPair(c.cert, c.key) 189 | if err != nil { 190 | t.Fatal(err) 191 | } 192 | tlsc := &tls.Config{ 193 | Certificates: []tls.Certificate{certificate}, 194 | } 195 | creds := credentials.NewTLS(tlsc) 196 | s.tlsc = tlsc 197 | s.cacert = c.cacert 198 | s.server = grpc.NewServer(grpc.Creds(creds)) 199 | } else { 200 | s.server = grpc.NewServer() 201 | } 202 | s.startServer() 203 | return s 204 | } 205 | 206 | // NewTLSServer returns a new server with registered secure *grpc.Server 207 | func NewTLSServer(t TB, protopath string, cacert, cert, key []byte, opts ...Option) *Server { 208 | t.Helper() 209 | opts = append(opts, UseTLS(cacert, cert, key)) 210 | return NewServer(t, protopath, opts...) 211 | } 212 | 213 | // Close shuts down *grpc.Server 214 | func (s *Server) Close() { 215 | s.mu.Lock() 216 | s.status = status_closing 217 | s.mu.Unlock() 218 | defer func() { 219 | s.mu.Lock() 220 | s.status = status_closed 221 | s.mu.Unlock() 222 | }() 223 | s.t.Helper() 224 | if s.listener == nil { 225 | s.t.Error("server is not started yet") 226 | return 227 | } 228 | if s.cc != nil { 229 | _ = s.cc.Close() 230 | s.cc = nil 231 | } 232 | done := make(chan struct{}) 233 | go func() { 234 | s.server.GracefulStop() 235 | close(done) 236 | }() 237 | t := time.NewTimer(5 * time.Second) 238 | select { 239 | case <-done: 240 | if !t.Stop() { 241 | <-t.C 242 | } 243 | case <-t.C: 244 | s.server.Stop() 245 | } 246 | } 247 | 248 | // Addr returns server listener address 249 | func (s *Server) Addr() string { 250 | s.t.Helper() 251 | if s.listener == nil { 252 | s.t.Error("server is not started yet") 253 | return "" 254 | } 255 | return s.listener.Addr().String() 256 | } 257 | 258 | // Conn returns *grpc.ClientConn which connects *grpc.Server. 259 | func (s *Server) Conn() *grpc.ClientConn { 260 | s.t.Helper() 261 | if s.listener == nil { 262 | s.t.Error("server is not started yet") 263 | return nil 264 | } 265 | var creds credentials.TransportCredentials 266 | if s.tlsc == nil { 267 | creds = insecure.NewCredentials() 268 | } else { 269 | if s.cacert == nil { 270 | s.tlsc.InsecureSkipVerify = true 271 | } else { 272 | pool := x509.NewCertPool() 273 | if ok := pool.AppendCertsFromPEM(s.cacert); !ok { 274 | s.t.Fatal(errors.New("failed to append ca certs")) 275 | } 276 | s.tlsc.RootCAs = pool 277 | } 278 | creds = credentials.NewTLS(s.tlsc) 279 | } 280 | conn, err := grpc.Dial( //nolint:staticcheck 281 | s.listener.Addr().String(), 282 | grpc.WithTransportCredentials(creds), 283 | ) 284 | if err != nil { 285 | s.t.Error(err) 286 | return nil 287 | } 288 | s.cc = conn 289 | return conn 290 | } 291 | 292 | // ClientConn is alias of Conn 293 | func (s *Server) ClientConn() *grpc.ClientConn { 294 | return s.Conn() 295 | } 296 | 297 | func (s *Server) startServer() { 298 | s.mu.Lock() 299 | s.status = status_starting 300 | s.mu.Unlock() 301 | defer func() { 302 | s.mu.Lock() 303 | s.status = status_start 304 | s.mu.Unlock() 305 | }() 306 | s.t.Helper() 307 | if !s.disableReflection { 308 | reflection.Register(s.server) 309 | } 310 | s.registerServer() 311 | l, err := net.Listen("tcp", "127.0.0.1:0") 312 | if err != nil { 313 | s.t.Error(err) 314 | return 315 | } 316 | s.listener = l 317 | go func() { 318 | _ = s.server.Serve(l) 319 | }() 320 | } 321 | 322 | // Match create request matcher with matchFunc (func(req *grpcstub.Request) bool). 323 | func (s *Server) Match(fn func(req *Request) bool) *matcher { 324 | m := &matcher{ 325 | matchFuncs: []matchFunc{fn}, 326 | t: s.t, 327 | } 328 | s.mu.Lock() 329 | defer s.mu.Unlock() 330 | s.addMatcher(m) 331 | return m 332 | } 333 | 334 | // Match append matchFunc (func(req *grpcstub.Request) bool) to request matcher. 335 | func (m *matcher) Match(fn func(req *Request) bool) *matcher { 336 | m.mu.Lock() 337 | defer m.mu.Unlock() 338 | m.matchFuncs = append(m.matchFuncs, fn) 339 | return m 340 | } 341 | 342 | // Service create request matcher using service. 343 | func (s *Server) Service(service string) *matcher { 344 | s.mu.Lock() 345 | defer s.mu.Unlock() 346 | fn := serviceMatchFunc(service) 347 | m := &matcher{ 348 | matchFuncs: []matchFunc{fn}, 349 | t: s.t, 350 | } 351 | s.addMatcher(m) 352 | return m 353 | } 354 | 355 | // Service append request matcher using service. 356 | func (m *matcher) Service(service string) *matcher { 357 | m.mu.Lock() 358 | defer m.mu.Unlock() 359 | fn := serviceMatchFunc(service) 360 | m.matchFuncs = append(m.matchFuncs, fn) 361 | return m 362 | } 363 | 364 | // Servicef create request matcher using sprintf-ed service. 365 | func (s *Server) Servicef(format string, a ...any) *matcher { 366 | return s.Service(fmt.Sprintf(format, a...)) 367 | } 368 | 369 | // Servicef append request matcher using sprintf-ed service. 370 | func (m *matcher) Servicef(format string, a ...any) *matcher { 371 | return m.Service(fmt.Sprintf(format, a...)) 372 | } 373 | 374 | // Method create request matcher using method. 375 | func (s *Server) Method(method string) *matcher { 376 | s.mu.Lock() 377 | defer s.mu.Unlock() 378 | fn := methodMatchFunc(method) 379 | m := &matcher{ 380 | matchFuncs: []matchFunc{fn}, 381 | t: s.t, 382 | } 383 | s.addMatcher(m) 384 | return m 385 | } 386 | 387 | // Method append request matcher using method. 388 | func (m *matcher) Method(method string) *matcher { 389 | m.mu.Lock() 390 | defer m.mu.Unlock() 391 | fn := methodMatchFunc(method) 392 | m.matchFuncs = append(m.matchFuncs, fn) 393 | return m 394 | } 395 | 396 | // Methodf create request matcher using sprintf-ed method. 397 | func (s *Server) Methodf(format string, a ...any) *matcher { 398 | return s.Method(fmt.Sprintf(format, a...)) 399 | } 400 | 401 | // Methodf append request matcher using sprintf-ed method. 402 | func (m *matcher) Methodf(format string, a ...any) *matcher { 403 | return m.Method(fmt.Sprintf(format, a...)) 404 | } 405 | 406 | // Header append handler which append header to response. 407 | func (m *matcher) Header(key, value string) *matcher { 408 | prev := m.handler 409 | m.handler = func(req *Request, md protoreflect.MethodDescriptor) *Response { 410 | var res *Response 411 | if prev == nil { 412 | res = NewResponse() 413 | } else { 414 | res = prev(req, md) 415 | } 416 | res.Headers.Append(key, value) 417 | return res 418 | } 419 | return m 420 | } 421 | 422 | // Trailer append handler which append trailer to response. 423 | func (m *matcher) Trailer(key, value string) *matcher { 424 | prev := m.handler 425 | m.handler = func(req *Request, md protoreflect.MethodDescriptor) *Response { 426 | var res *Response 427 | if prev == nil { 428 | res = NewResponse() 429 | } else { 430 | res = prev(req, md) 431 | } 432 | res.Trailers.Append(key, value) 433 | return res 434 | } 435 | return m 436 | } 437 | 438 | // Handler set handler 439 | func (m *matcher) Handler(fn func(req *Request) *Response) { 440 | m.handler = func(req *Request, md protoreflect.MethodDescriptor) *Response { 441 | return fn(req) 442 | } 443 | } 444 | 445 | // Response set handler which return response. 446 | func (m *matcher) Response(message any) *matcher { 447 | mm := map[string]any{} 448 | switch v := message.(type) { 449 | case map[string]any: 450 | mm = v 451 | default: 452 | b, err := json.Marshal(v) 453 | if err != nil { 454 | m.t.Fatalf("failed to convert message: %v", err) 455 | } 456 | if err := json.Unmarshal(b, &mm); err != nil { 457 | m.t.Fatalf("failed to convert message: %v", err) 458 | } 459 | } 460 | prev := m.handler 461 | m.handler = func(req *Request, md protoreflect.MethodDescriptor) *Response { 462 | var res *Response 463 | if prev == nil { 464 | res = NewResponse() 465 | } else { 466 | res = prev(req, md) 467 | } 468 | res.Messages = append(res.Messages, mm) 469 | return res 470 | } 471 | return m 472 | } 473 | 474 | // ResponseString set handler which return response. 475 | func (m *matcher) ResponseString(message string) *matcher { 476 | mes := make(map[string]any) 477 | _ = json.Unmarshal([]byte(message), &mes) 478 | return m.Response(mes) 479 | } 480 | 481 | // ResponseStringf set handler which return sprintf-ed response. 482 | func (m *matcher) ResponseStringf(format string, a ...any) *matcher { 483 | return m.ResponseString(fmt.Sprintf(format, a...)) 484 | } 485 | 486 | // Status set handler which return response with status 487 | func (m *matcher) Status(s *status.Status) *matcher { 488 | prev := m.handler 489 | m.handler = func(req *Request, md protoreflect.MethodDescriptor) *Response { 490 | var res *Response 491 | if prev == nil { 492 | res = NewResponse() 493 | } else { 494 | res = prev(req, md) 495 | } 496 | res.Status = s 497 | return res 498 | } 499 | return m 500 | } 501 | 502 | // Requests returns []*grpcstub.Request received by router. 503 | func (s *Server) Requests() []*Request { 504 | s.mu.RLock() 505 | defer s.mu.RUnlock() 506 | return s.requests 507 | } 508 | 509 | // UnmatchedRequests returns []*grpcstub.Request received but not matched by router. 510 | func (s *Server) UnmatchedRequests() []*Request { 511 | s.mu.RLock() 512 | defer s.mu.RUnlock() 513 | return s.unmatchedRequests 514 | } 515 | 516 | // ClearMatchers clear matchers. 517 | func (s *Server) ClearMatchers() { 518 | s.matchers = nil 519 | } 520 | 521 | // Prepend prepend matcher. 522 | func (s *Server) Prepend() *Server { 523 | s.mu.Lock() 524 | defer s.mu.Unlock() 525 | s.prependOnce = true 526 | return s 527 | } 528 | 529 | // ClearRequests clear requests. 530 | func (s *Server) ClearRequests() { 531 | s.requests = nil 532 | s.unmatchedRequests = nil 533 | } 534 | 535 | // Requests returns []*grpcstub.Request received by matcher. 536 | func (m *matcher) Requests() []*Request { 537 | m.mu.RLock() 538 | defer m.mu.RUnlock() 539 | return m.requests 540 | } 541 | 542 | func (s *Server) addMatcher(m *matcher) { 543 | if s.prependOnce { 544 | s.matchers = append([]*matcher{m}, s.matchers...) 545 | s.prependOnce = false 546 | return 547 | } 548 | s.matchers = append(s.matchers, m) 549 | } 550 | 551 | func (s *Server) registerServer() { 552 | for _, fd := range s.fds { 553 | for i := 0; i < fd.Services().Len(); i++ { 554 | s.server.RegisterService(s.createServiceDesc(fd.Services().Get(i)), nil) 555 | } 556 | } 557 | if !s.healthCheck { 558 | return 559 | } 560 | healthSrv := health.NewServer() 561 | healthpb.RegisterHealthServer(s.server, healthSrv) 562 | healthSrv.SetServingStatus(HealthCheckService_DEFAULT, healthpb.HealthCheckResponse_SERVING) 563 | go func() { 564 | status := healthpb.HealthCheckResponse_SERVING 565 | healthSrv.SetServingStatus(HealthCheckService_FLAPPING, status) 566 | for { 567 | s.mu.Lock() 568 | ss := s.status 569 | s.mu.Unlock() 570 | switch ss { 571 | case status_start, status_starting: 572 | if status == healthpb.HealthCheckResponse_SERVING { 573 | status = healthpb.HealthCheckResponse_NOT_SERVING 574 | } else { 575 | status = healthpb.HealthCheckResponse_SERVING 576 | } 577 | healthSrv.SetServingStatus(HealthCheckService_FLAPPING, status) 578 | } 579 | time.Sleep(100 * time.Millisecond) 580 | } 581 | }() 582 | } 583 | 584 | func (s *Server) createServiceDesc(sd protoreflect.ServiceDescriptor) *grpc.ServiceDesc { 585 | gsd := &grpc.ServiceDesc{ 586 | ServiceName: string(sd.FullName()), 587 | HandlerType: nil, 588 | Metadata: sd.ParentFile().Name(), 589 | } 590 | 591 | mds := []protoreflect.MethodDescriptor{} 592 | for i := 0; i < sd.Methods().Len(); i++ { 593 | mds = append(mds, sd.Methods().Get(i)) 594 | } 595 | 596 | gsd.Methods, gsd.Streams = s.createMethodDescs(mds) 597 | return gsd 598 | } 599 | 600 | func (s *Server) createMethodDescs(mds []protoreflect.MethodDescriptor) ([]grpc.MethodDesc, []grpc.StreamDesc) { 601 | var methods []grpc.MethodDesc 602 | var streams []grpc.StreamDesc 603 | for _, md := range mds { 604 | if !md.IsStreamingClient() && !md.IsStreamingServer() { 605 | method := grpc.MethodDesc{ 606 | MethodName: string(md.Name()), 607 | Handler: s.createUnaryHandler(md), 608 | } 609 | methods = append(methods, method) 610 | } else { 611 | stream := grpc.StreamDesc{ 612 | StreamName: string(md.Name()), 613 | Handler: s.createStreamHandler(md), 614 | ServerStreams: md.IsStreamingServer(), 615 | ClientStreams: md.IsStreamingClient(), 616 | } 617 | streams = append(streams, stream) 618 | } 619 | } 620 | return methods, streams 621 | } 622 | 623 | func (s *Server) createUnaryHandler(md protoreflect.MethodDescriptor) func(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) { 624 | return func(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) { 625 | in := dynamicpb.NewMessage(md.Input()) 626 | if err := dec(in); err != nil { 627 | return nil, err 628 | } 629 | m, err := MarshalProtoMessage(in) 630 | if err != nil { 631 | return nil, err 632 | } 633 | req := newRequest(md, m) 634 | h, ok := metadata.FromIncomingContext(ctx) 635 | if ok { 636 | req.Headers = h 637 | } 638 | 639 | var mes *dynamicpb.Message 640 | for _, m := range s.matchers { 641 | if !m.matchRequest(req) { 642 | continue 643 | } 644 | s.mu.Lock() 645 | s.requests = append(s.requests, req) 646 | s.mu.Unlock() 647 | m.mu.Lock() 648 | m.requests = append(m.requests, req) 649 | m.mu.Unlock() 650 | res := m.handler(req, md) 651 | for k, v := range res.Headers { 652 | for _, vv := range v { 653 | if err := grpc.SetHeader(ctx, metadata.Pairs(k, vv)); err != nil { 654 | return nil, err 655 | } 656 | } 657 | } 658 | for k, v := range res.Trailers { 659 | for _, vv := range v { 660 | if err := grpc.SetTrailer(ctx, metadata.Pairs(k, vv)); err != nil { 661 | return nil, err 662 | } 663 | } 664 | } 665 | if res.Status != nil && res.Status.Err() != nil { 666 | return nil, res.Status.Err() 667 | } 668 | mes = dynamicpb.NewMessage(md.Output()) 669 | if len(res.Messages) > 0 { 670 | if err := UnmarshalProtoMessage(res.Messages[0], mes); err != nil { 671 | return nil, err 672 | } 673 | } 674 | return mes, nil 675 | } 676 | 677 | s.mu.Lock() 678 | s.unmatchedRequests = append(s.unmatchedRequests, req) 679 | s.mu.Unlock() 680 | return mes, status.Error(codes.NotFound, codes.NotFound.String()) 681 | } 682 | } 683 | 684 | func (s *Server) createStreamHandler(md protoreflect.MethodDescriptor) func(srv any, stream grpc.ServerStream) error { 685 | switch { 686 | case !md.IsStreamingClient() && md.IsStreamingServer(): 687 | return s.createServerStreamingHandler(md) 688 | case md.IsStreamingClient() && !md.IsStreamingServer(): 689 | return s.createClientStreamingHandler(md) 690 | case md.IsStreamingClient() && md.IsStreamingServer(): 691 | return s.createBidiStreamingHandler(md) 692 | default: 693 | return func(srv any, stream grpc.ServerStream) error { 694 | return nil 695 | } 696 | } 697 | } 698 | 699 | func (s *Server) createServerStreamingHandler(md protoreflect.MethodDescriptor) func(srv any, stream grpc.ServerStream) error { 700 | return func(srv any, stream grpc.ServerStream) error { 701 | in := dynamicpb.NewMessage(md.Input()) 702 | if err := stream.RecvMsg(in); err != nil { 703 | return err 704 | } 705 | m, err := MarshalProtoMessage(in) 706 | if err != nil { 707 | return err 708 | } 709 | r := newRequest(md, m) 710 | h, ok := metadata.FromIncomingContext(stream.Context()) 711 | if ok { 712 | r.Headers = h 713 | } 714 | for _, m := range s.matchers { 715 | if !m.matchRequest(r) { 716 | continue 717 | } 718 | m.mu.Lock() 719 | m.requests = append(m.requests, r) 720 | m.mu.Unlock() 721 | s.mu.Lock() 722 | s.requests = append(s.requests, r) 723 | s.mu.Unlock() 724 | res := m.handler(r, md) 725 | for k, v := range res.Headers { 726 | for _, vv := range v { 727 | if err := stream.SendHeader(metadata.Pairs(k, vv)); err != nil { 728 | return err 729 | } 730 | } 731 | } 732 | for k, v := range res.Trailers { 733 | for _, vv := range v { 734 | stream.SetTrailer(metadata.Pairs(k, vv)) 735 | } 736 | } 737 | if res.Status != nil && res.Status.Err() != nil { 738 | return res.Status.Err() 739 | } 740 | if len(res.Messages) > 0 { 741 | for _, resm := range res.Messages { 742 | mes := dynamicpb.NewMessage(md.Output()) 743 | if err := UnmarshalProtoMessage(resm, mes); err != nil { 744 | return err 745 | } 746 | if err := stream.SendMsg(mes); err != nil { 747 | return err 748 | } 749 | } 750 | } 751 | return nil 752 | } 753 | s.mu.Lock() 754 | s.unmatchedRequests = append(s.unmatchedRequests, r) 755 | s.mu.Unlock() 756 | return status.Error(codes.NotFound, codes.NotFound.String()) 757 | } 758 | } 759 | 760 | func (s *Server) createClientStreamingHandler(md protoreflect.MethodDescriptor) func(srv any, stream grpc.ServerStream) error { 761 | return func(srv any, stream grpc.ServerStream) error { 762 | rs := []*Request{} 763 | for { 764 | in := dynamicpb.NewMessage(md.Input()) 765 | err := stream.RecvMsg(in) 766 | if err == nil { 767 | m, err := MarshalProtoMessage(in) 768 | if err != nil { 769 | return err 770 | } 771 | r := newRequest(md, m) 772 | h, ok := metadata.FromIncomingContext(stream.Context()) 773 | if ok { 774 | r.Headers = h 775 | } 776 | rs = append(rs, r) 777 | continue 778 | } 779 | 780 | if err != io.EOF { 781 | s.mu.Lock() 782 | s.unmatchedRequests = append(s.unmatchedRequests, rs...) 783 | s.mu.Unlock() 784 | return err 785 | } 786 | 787 | var mes *dynamicpb.Message 788 | for _, m := range s.matchers { 789 | if !m.matchRequest(rs...) { 790 | continue 791 | } 792 | s.mu.Lock() 793 | s.requests = append(s.requests, rs...) 794 | s.mu.Unlock() 795 | m.mu.Lock() 796 | m.requests = append(m.requests, rs...) 797 | m.mu.Unlock() 798 | last := rs[len(rs)-1] 799 | res := m.handler(last, md) 800 | if res.Status != nil && res.Status.Err() != nil { 801 | return res.Status.Err() 802 | } 803 | mes = dynamicpb.NewMessage(md.Output()) 804 | if len(res.Messages) > 0 { 805 | if err := UnmarshalProtoMessage(res.Messages[0], mes); err != nil { 806 | return err 807 | } 808 | } 809 | for k, v := range res.Headers { 810 | for _, vv := range v { 811 | if err := stream.SendHeader(metadata.Pairs(k, vv)); err != nil { 812 | return err 813 | } 814 | } 815 | } 816 | for k, v := range res.Trailers { 817 | for _, vv := range v { 818 | stream.SetTrailer((metadata.Pairs(k, vv))) 819 | } 820 | } 821 | return stream.SendMsg(mes) 822 | } 823 | s.mu.Lock() 824 | s.unmatchedRequests = append(s.unmatchedRequests, rs...) 825 | s.mu.Unlock() 826 | return status.Error(codes.NotFound, codes.NotFound.String()) 827 | } 828 | } 829 | } 830 | 831 | func (s *Server) createBidiStreamingHandler(md protoreflect.MethodDescriptor) func(srv any, stream grpc.ServerStream) error { 832 | return func(srv any, stream grpc.ServerStream) error { 833 | headerSent := false 834 | L: 835 | for { 836 | in := dynamicpb.NewMessage(md.Input()) 837 | err := stream.RecvMsg(in) 838 | if err == io.EOF { 839 | return nil 840 | } 841 | if err != nil { 842 | return err 843 | } 844 | m, err := MarshalProtoMessage(in) 845 | if err != nil { 846 | return err 847 | } 848 | r := newRequest(md, m) 849 | h, ok := metadata.FromIncomingContext(stream.Context()) 850 | if ok { 851 | r.Headers = h 852 | } 853 | for _, m := range s.matchers { 854 | if !m.matchRequest(r) { 855 | continue 856 | } 857 | s.mu.Lock() 858 | s.requests = append(s.requests, r) 859 | s.mu.Unlock() 860 | m.mu.Lock() 861 | m.requests = append(m.requests, r) 862 | m.mu.Unlock() 863 | res := m.handler(r, md) 864 | if !headerSent { 865 | for k, v := range res.Headers { 866 | for _, vv := range v { 867 | if err := stream.SendHeader(metadata.Pairs(k, vv)); err != nil { 868 | return err 869 | } 870 | headerSent = true 871 | } 872 | } 873 | } 874 | for k, v := range res.Trailers { 875 | for _, vv := range v { 876 | stream.SetTrailer(metadata.Pairs(k, vv)) 877 | } 878 | } 879 | if res.Status != nil && res.Status.Err() != nil { 880 | return res.Status.Err() 881 | } 882 | if len(res.Messages) > 0 { 883 | for _, resm := range res.Messages { 884 | mes := dynamicpb.NewMessage(md.Output()) 885 | if err := UnmarshalProtoMessage(resm, mes); err != nil { 886 | return err 887 | } 888 | if err := stream.SendMsg(mes); err != nil { 889 | return err 890 | } 891 | } 892 | } 893 | continue L 894 | } 895 | s.mu.Lock() 896 | s.unmatchedRequests = append(s.unmatchedRequests, r) 897 | s.mu.Unlock() 898 | return status.Error(codes.NotFound, codes.NotFound.String()) 899 | } 900 | } 901 | } 902 | 903 | // MarshalProtoMessage marshals [proto.Message] to [Message]. 904 | func MarshalProtoMessage(pm protoreflect.ProtoMessage) (Message, error) { 905 | b, err := protojson.MarshalOptions{UseProtoNames: true, UseEnumNumbers: true, EmitUnpopulated: true}.Marshal(pm) 906 | if err != nil { 907 | return nil, err 908 | } 909 | m := Message{} 910 | if err := json.Unmarshal(b, &m); err != nil { 911 | return nil, err 912 | } 913 | return m, nil 914 | } 915 | 916 | // UnmarshalProtoMessage unmarshals [Message] to [proto.Message]. 917 | func UnmarshalProtoMessage(m Message, pm protoreflect.ProtoMessage) error { 918 | b, err := json.Marshal(m) 919 | if err != nil { 920 | return err 921 | } 922 | if err := (protojson.UnmarshalOptions{}).Unmarshal(b, pm); err != nil { 923 | return err 924 | } 925 | return nil 926 | } 927 | 928 | func (m *matcher) matchRequest(rs ...*Request) bool { 929 | for _, r := range rs { 930 | for _, fn := range m.matchFuncs { 931 | if !fn(r) { 932 | return false 933 | } 934 | } 935 | } 936 | return true 937 | } 938 | 939 | func serviceMatchFunc(service string) matchFunc { 940 | return func(req *Request) bool { 941 | return req.Service == strings.TrimPrefix(service, "/") 942 | } 943 | } 944 | 945 | func methodMatchFunc(method string) matchFunc { 946 | return func(req *Request) bool { 947 | if !strings.Contains(method, "/") { 948 | return req.Method == method 949 | } 950 | splitted := strings.Split(strings.TrimPrefix(method, "/"), "/") 951 | s := strings.Join(splitted[:len(splitted)-1], "/") 952 | m := splitted[len(splitted)-1] 953 | return req.Service == s && req.Method == m 954 | } 955 | } 956 | 957 | func (s *Server) resolveProtos(ctx context.Context, c *config) error { 958 | pr, err := protoresolv.New(c.importPaths, protoresolv.Proto(c.protos...)) 959 | if err != nil { 960 | return err 961 | } 962 | var bufresolvOpts []bufresolv.Option 963 | for _, dir := range c.bufDirs { 964 | bufresolvOpts = append(bufresolvOpts, bufresolv.BufDir(dir)) 965 | } 966 | for _, config := range c.bufConfigs { 967 | bufresolvOpts = append(bufresolvOpts, bufresolv.BufConfig(config)) 968 | } 969 | for _, lock := range c.bufLocks { 970 | bufresolvOpts = append(bufresolvOpts, bufresolv.BufLock(lock)) 971 | } 972 | bufresolvOpts = append(bufresolvOpts, bufresolv.BufModule(c.bufModules...)) 973 | br, err := bufresolv.New(bufresolvOpts...) 974 | if err != nil { 975 | return err 976 | } 977 | comp := protocompile.Compiler{ 978 | Resolver: protocompile.WithStandardImports(protocompile.CompositeResolver([]protocompile.Resolver{ 979 | pr, br, 980 | })), 981 | } 982 | protos := unique(slices.Concat(pr.Paths(), br.Paths())) 983 | fds, err := comp.Compile(ctx, protos...) 984 | if err != nil { 985 | return err 986 | } 987 | if err := registerFiles(fds); err != nil { 988 | return err 989 | } 990 | s.fds = fds 991 | return nil 992 | } 993 | 994 | func registerFiles(fds linker.Files) (err error) { 995 | for _, fd := range fds { 996 | // Skip registration of already registered descriptors 997 | if _, err := protoregistry.GlobalFiles.FindFileByPath(fd.Path()); !errors.Is(err, protoregistry.NotFound) { 998 | continue 999 | } 1000 | // Skip registration of conflicted descriptors 1001 | conflict := false 1002 | rangeTopLevelDescriptors(fd, func(d protoreflect.Descriptor) { 1003 | if _, err := protoregistry.GlobalFiles.FindDescriptorByName(d.FullName()); err == nil { 1004 | conflict = true 1005 | } 1006 | }) 1007 | if conflict { 1008 | continue 1009 | } 1010 | 1011 | if err := protoregistry.GlobalFiles.RegisterFile(fd); err != nil { 1012 | return err 1013 | } 1014 | } 1015 | return nil 1016 | } 1017 | 1018 | // copy from google.golang.org/protobuf/reflect/protoregistry 1019 | func rangeTopLevelDescriptors(fd protoreflect.FileDescriptor, f func(protoreflect.Descriptor)) { 1020 | eds := fd.Enums() 1021 | for i := eds.Len() - 1; i >= 0; i-- { 1022 | f(eds.Get(i)) 1023 | vds := eds.Get(i).Values() 1024 | for i := vds.Len() - 1; i >= 0; i-- { 1025 | f(vds.Get(i)) 1026 | } 1027 | } 1028 | mds := fd.Messages() 1029 | for i := mds.Len() - 1; i >= 0; i-- { 1030 | f(mds.Get(i)) 1031 | } 1032 | xds := fd.Extensions() 1033 | for i := xds.Len() - 1; i >= 0; i-- { 1034 | f(xds.Get(i)) 1035 | } 1036 | sds := fd.Services() 1037 | for i := sds.Len() - 1; i >= 0; i-- { 1038 | f(sds.Get(i)) 1039 | } 1040 | } 1041 | 1042 | func splitMethodFullName(mn protoreflect.FullName) (string, string) { 1043 | splitted := strings.Split(string(mn), ".") 1044 | service := strings.Join(splitted[:len(splitted)-1], ".") 1045 | method := splitted[len(splitted)-1] 1046 | return service, method 1047 | } 1048 | -------------------------------------------------------------------------------- /grpcstub_test.go: -------------------------------------------------------------------------------- 1 | package grpcstub 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "connectrpc.com/connect" 14 | "github.com/google/go-cmp/cmp" 15 | "github.com/jhump/protoreflect/v2/grpcreflect" 16 | "github.com/k1LoW/grpcstub/testdata/bsr/protobuf/gen/go/pinger" 17 | "github.com/k1LoW/grpcstub/testdata/bsr/protobuf/gen/go/pinger/pingerconnect" 18 | "github.com/k1LoW/grpcstub/testdata/hello" 19 | "github.com/k1LoW/grpcstub/testdata/routeguide" 20 | "github.com/tenntenn/golden" 21 | "golang.org/x/net/http2" 22 | "google.golang.org/grpc" 23 | "google.golang.org/grpc/codes" 24 | healthpb "google.golang.org/grpc/health/grpc_health_v1" 25 | "google.golang.org/grpc/metadata" 26 | "google.golang.org/grpc/status" 27 | "google.golang.org/protobuf/types/known/timestamppb" 28 | ) 29 | 30 | func TestAddr(t *testing.T) { 31 | ts := NewServer(t, "testdata/route_guide.proto") 32 | t.Cleanup(func() { 33 | ts.Close() 34 | }) 35 | got := ts.Addr() 36 | if !strings.HasPrefix(got, "127.0.0.1:") { 37 | t.Errorf("got %v\nwant 127.0.0.1:*", got) 38 | } 39 | } 40 | 41 | func TestServerMatch(t *testing.T) { 42 | ctx := context.Background() 43 | ts := NewServer(t, "testdata/route_guide.proto") 44 | t.Cleanup(func() { 45 | ts.Close() 46 | }) 47 | ts.Match(func(req *Request) bool { 48 | return req.Method == "GetFeature" 49 | }).Response(map[string]any{"name": "hello"}) 50 | 51 | client := routeguide.NewRouteGuideClient(ts.Conn()) 52 | res, err := client.GetFeature(ctx, &routeguide.Point{ 53 | Latitude: 10, 54 | Longitude: 13, 55 | }) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | got := res.Name 60 | if want := "hello"; got != want { 61 | t.Errorf("got %v\nwant %v", got, want) 62 | } 63 | } 64 | 65 | func TestMatcherMatch(t *testing.T) { 66 | ctx := context.Background() 67 | ts := NewServer(t, "testdata/route_guide.proto") 68 | t.Cleanup(func() { 69 | ts.Close() 70 | }) 71 | ts.Service("routeguide.RouteGuide").Match(func(req *Request) bool { 72 | return req.Method == "GetFeature" 73 | }).Response(map[string]any{"name": "hello"}) 74 | 75 | client := routeguide.NewRouteGuideClient(ts.Conn()) 76 | res, err := client.GetFeature(ctx, &routeguide.Point{ 77 | Latitude: 10, 78 | Longitude: 13, 79 | }) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | got := res.Name 84 | if want := "hello"; got != want { 85 | t.Errorf("got %v\nwant %v", got, want) 86 | } 87 | } 88 | 89 | func TestServerService(t *testing.T) { 90 | ctx := context.Background() 91 | ts := NewServer(t, "testdata/route_guide.proto") 92 | t.Cleanup(func() { 93 | ts.Close() 94 | }) 95 | ts.Service("routeguide.RouteGuide").Response(map[string]any{"name": "hello"}) 96 | 97 | client := routeguide.NewRouteGuideClient(ts.Conn()) 98 | res, err := client.GetFeature(ctx, &routeguide.Point{ 99 | Latitude: 10, 100 | Longitude: 13, 101 | }) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | got := res.Name 106 | if want := "hello"; got != want { 107 | t.Errorf("got %v\nwant %v", got, want) 108 | } 109 | } 110 | 111 | func TestMatcherService(t *testing.T) { 112 | ctx := context.Background() 113 | ts := NewServer(t, "testdata/route_guide.proto") 114 | t.Cleanup(func() { 115 | ts.Close() 116 | }) 117 | ts.Method("GetFeature").Service("routeguide.RouteGuide").Response(map[string]any{"name": "hello"}) 118 | 119 | client := routeguide.NewRouteGuideClient(ts.Conn()) 120 | res, err := client.GetFeature(ctx, &routeguide.Point{ 121 | Latitude: 10, 122 | Longitude: 13, 123 | }) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | got := res.Name 128 | if want := "hello"; got != want { 129 | t.Errorf("got %v\nwant %v", got, want) 130 | } 131 | } 132 | 133 | func TestMatcherMethod(t *testing.T) { 134 | ctx := context.Background() 135 | ts := NewServer(t, "testdata/route_guide.proto") 136 | t.Cleanup(func() { 137 | ts.Close() 138 | }) 139 | ts.Service("routeguide.RouteGuide").Method("GetFeature").Response(map[string]any{"name": "hello"}) 140 | 141 | client := routeguide.NewRouteGuideClient(ts.Conn()) 142 | res, err := client.GetFeature(ctx, &routeguide.Point{ 143 | Latitude: 10, 144 | Longitude: 13, 145 | }) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | got := res.Name 150 | if want := "hello"; got != want { 151 | t.Errorf("got %v\nwant %v", got, want) 152 | } 153 | } 154 | 155 | func TestHeader(t *testing.T) { 156 | ctx := context.Background() 157 | ts := NewServer(t, "testdata/route_guide.proto") 158 | t.Cleanup(func() { 159 | ts.Close() 160 | }) 161 | ts.Method("GetFeature").Header("session", "XXXxxXXX").Header("size", "213").Response(map[string]any{"name": "hello"}) 162 | 163 | client := routeguide.NewRouteGuideClient(ts.Conn()) 164 | var header metadata.MD 165 | if _, err := client.GetFeature(ctx, &routeguide.Point{}, grpc.Header(&header)); err != nil { 166 | t.Fatal(err) 167 | } 168 | { 169 | got := header.Get("session") 170 | if want := "XXXxxXXX"; got[0] != want { 171 | t.Errorf("got %v\nwant %v", got[0], want) 172 | } 173 | } 174 | { 175 | got := header.Get("size") 176 | if want := "213"; got[0] != want { 177 | t.Errorf("got %v\nwant %v", got[0], want) 178 | } 179 | } 180 | } 181 | 182 | func TestTrailer(t *testing.T) { 183 | ctx := context.Background() 184 | ts := NewServer(t, "testdata/route_guide.proto") 185 | t.Cleanup(func() { 186 | ts.Close() 187 | }) 188 | ts.Method("GetFeature").Trailer("session", "XXXxxXXX").Trailer("size", "213").Response(map[string]any{"name": "hello"}) 189 | 190 | client := routeguide.NewRouteGuideClient(ts.Conn()) 191 | var trailer metadata.MD 192 | if _, err := client.GetFeature(ctx, &routeguide.Point{}, grpc.Trailer(&trailer)); err != nil { 193 | t.Fatal(err) 194 | } 195 | { 196 | got := trailer.Get("session") 197 | if want := "XXXxxXXX"; got[0] != want { 198 | t.Errorf("got %v\nwant %v", got[0], want) 199 | } 200 | } 201 | { 202 | got := trailer.Get("size") 203 | if want := "213"; got[0] != want { 204 | t.Errorf("got %v\nwant %v", got[0], want) 205 | } 206 | } 207 | } 208 | 209 | func TestResponseHeader(t *testing.T) { 210 | ctx := context.Background() 211 | ts := NewServer(t, "testdata/route_guide.proto") 212 | t.Cleanup(func() { 213 | ts.Close() 214 | }) 215 | ts.Method("GetFeature").Response(map[string]any{"name": "hello"}) 216 | 217 | client := routeguide.NewRouteGuideClient(ts.Conn()) 218 | ctx = metadata.AppendToOutgoingContext(ctx, "authentication", "XXXXxxxxXXXX") 219 | if _, err := client.GetFeature(ctx, &routeguide.Point{}); err != nil { 220 | t.Fatal(err) 221 | } 222 | r := ts.Requests()[0] 223 | got := r.Headers.Get("authentication") 224 | if want := "XXXXxxxxXXXX"; got[0] != want { 225 | t.Errorf("got %v\nwant %v", got[0], want) 226 | } 227 | } 228 | 229 | func TestStatusUnary(t *testing.T) { 230 | ctx := context.Background() 231 | ts := NewServer(t, "testdata/route_guide.proto") 232 | t.Cleanup(func() { 233 | ts.Close() 234 | }) 235 | ts.Method("GetFeature").Status(status.New(codes.Aborted, "aborted")) 236 | client := routeguide.NewRouteGuideClient(ts.Conn()) 237 | 238 | _, err := client.GetFeature(ctx, &routeguide.Point{}) 239 | if err == nil { 240 | t.Error("want error") 241 | return 242 | } 243 | 244 | s, ok := status.FromError(err) 245 | if !ok { 246 | t.Error("want status.Status") 247 | return 248 | } 249 | { 250 | got := s.Code() 251 | if want := codes.Aborted; got != want { 252 | t.Errorf("got %v\nwant %v", got, want) 253 | } 254 | } 255 | { 256 | got := s.Message() 257 | if want := "aborted"; got != want { 258 | t.Errorf("got %v\nwant %v", got, want) 259 | } 260 | } 261 | } 262 | 263 | func TestStatusServerStreaming(t *testing.T) { 264 | ctx := context.Background() 265 | ts := NewServer(t, "testdata/route_guide.proto") 266 | t.Cleanup(func() { 267 | ts.Close() 268 | }) 269 | ts.Method("ListFeatures").Status(status.New(codes.Aborted, "aborted")) 270 | 271 | client := routeguide.NewRouteGuideClient(ts.Conn()) 272 | stream, err := client.ListFeatures(ctx, &routeguide.Rectangle{ 273 | Lo: &routeguide.Point{ 274 | Latitude: int32(10), 275 | Longitude: int32(2), 276 | }, 277 | Hi: &routeguide.Point{ 278 | Latitude: int32(20), 279 | Longitude: int32(7), 280 | }, 281 | }) 282 | if err != nil { 283 | t.Fatal(err) 284 | } 285 | 286 | _, err = stream.Recv() 287 | if err == nil { 288 | t.Error("want error") 289 | } 290 | s, ok := status.FromError(err) 291 | if !ok { 292 | t.Error("want status.Status") 293 | return 294 | } 295 | { 296 | got := s.Code() 297 | if want := codes.Aborted; got != want { 298 | t.Errorf("got %v\nwant %v", got, want) 299 | } 300 | } 301 | { 302 | got := s.Message() 303 | if want := "aborted"; got != want { 304 | t.Errorf("got %v\nwant %v", got, want) 305 | } 306 | } 307 | } 308 | 309 | func TestStatusClientStreaming(t *testing.T) { 310 | ctx := context.Background() 311 | ts := NewServer(t, "testdata/route_guide.proto") 312 | t.Cleanup(func() { 313 | ts.Close() 314 | }) 315 | ts.Method("RecordRoute").Status(status.New(codes.Aborted, "aborted")) 316 | 317 | client := routeguide.NewRouteGuideClient(ts.Conn()) 318 | stream, err := client.RecordRoute(ctx) 319 | if err != nil { 320 | t.Fatal(err) 321 | } 322 | 323 | c := 2 324 | for i := 0; i < c; i++ { 325 | if err := stream.Send(&routeguide.Point{ 326 | Latitude: int32(i + 10), 327 | Longitude: int32(i * i * 2), 328 | }); err != nil { 329 | t.Fatal(err) 330 | } 331 | } 332 | _, err = stream.CloseAndRecv() 333 | if err == nil { 334 | t.Error("want error") 335 | return 336 | } 337 | 338 | s, ok := status.FromError(err) 339 | if !ok { 340 | t.Error("want status.Status") 341 | return 342 | } 343 | { 344 | got := s.Code() 345 | if want := codes.Aborted; got != want { 346 | t.Errorf("got %v\nwant %v", got, want) 347 | } 348 | } 349 | { 350 | got := s.Message() 351 | if want := "aborted"; got != want { 352 | t.Errorf("got %v\nwant %v", got, want) 353 | } 354 | } 355 | } 356 | 357 | func TestStatusBiStreaming(t *testing.T) { 358 | ctx := context.Background() 359 | ts := NewServer(t, "testdata/route_guide.proto") 360 | t.Cleanup(func() { 361 | ts.Close() 362 | }) 363 | ts.Method("RouteChat").Header("hello", "header").Trailer("hello", "trailer").Status(status.New(codes.Aborted, "aborted")) 364 | 365 | client := routeguide.NewRouteGuideClient(ts.Conn()) 366 | stream, err := client.RouteChat(ctx) 367 | if err != nil { 368 | t.Fatal(err) 369 | } 370 | if err := stream.SendMsg(&routeguide.RouteNote{ 371 | Message: "hello from client", 372 | }); err != nil { 373 | t.Fatal(err) 374 | } 375 | _, err = stream.Recv() 376 | if err == nil { 377 | t.Error("want error") 378 | return 379 | } 380 | 381 | s, ok := status.FromError(err) 382 | if !ok { 383 | t.Error("want status.Status") 384 | return 385 | } 386 | { 387 | got := s.Code() 388 | if want := codes.Aborted; got != want { 389 | t.Errorf("got %v\nwant %v", got, want) 390 | } 391 | } 392 | { 393 | got := s.Message() 394 | if want := "aborted"; got != want { 395 | t.Errorf("got %v\nwant %v", got, want) 396 | } 397 | } 398 | h, err := stream.Header() 399 | if err != nil { 400 | t.Error(err) 401 | } 402 | { 403 | got := h.Get("hello") 404 | want := []string{"header"} 405 | if diff := cmp.Diff(got, want, nil); diff != "" { 406 | t.Errorf("%s", diff) 407 | } 408 | } 409 | { 410 | got := stream.Trailer().Get("hello") 411 | want := []string{"trailer"} 412 | if diff := cmp.Diff(got, want, nil); diff != "" { 413 | t.Errorf("%s", diff) 414 | } 415 | } 416 | } 417 | 418 | func TestLoadProto(t *testing.T) { 419 | tests := []struct { 420 | proto string 421 | }{ 422 | {"testdata/route_guide.proto"}, 423 | {"testdata/hello.proto"}, 424 | {"testdata/*.proto"}, 425 | {"testdata/bsr/protobuf"}, 426 | } 427 | ctx := context.Background() 428 | for _, tt := range tests { 429 | t.Run(tt.proto, func(t *testing.T) { 430 | ts := NewServer(t, tt.proto) 431 | t.Cleanup(func() { 432 | ts.Close() 433 | }) 434 | cc := ts.ClientConn() 435 | client := grpcreflect.NewClientAuto(ctx, cc) 436 | svcs, err := client.ListServices() 437 | if err != nil { 438 | t.Fatal(err) 439 | } 440 | if len(svcs) == 0 { 441 | t.Error("no services") 442 | } 443 | }) 444 | } 445 | } 446 | 447 | func TestTime(t *testing.T) { 448 | now := time.Now() 449 | tests := []struct { 450 | name string 451 | res map[string]any 452 | wantTime time.Time 453 | }{ 454 | { 455 | "empty is 0 of UNIX timestamp", 456 | map[string]any{ 457 | "message": "hello", 458 | "num": 3, 459 | "hellos": []string{"hello", "world"}, 460 | }, 461 | time.Unix(0, 0), 462 | }, 463 | { 464 | "timestamppb.Timestamp", 465 | map[string]any{ 466 | "message": "hello", 467 | "num": 3, 468 | "hellos": []string{"hello", "world"}, 469 | "create_time": now.Format(time.RFC3339Nano), 470 | }, 471 | now, 472 | }, 473 | } 474 | ctx := context.Background() 475 | for _, tt := range tests { 476 | t.Run(tt.name, func(t *testing.T) { 477 | ts := NewServer(t, "testdata/hello.proto") 478 | t.Cleanup(func() { 479 | ts.Close() 480 | }) 481 | ts.Method("Hello").Response(tt.res) 482 | client := hello.NewGrpcTestServiceClient(ts.Conn()) 483 | got, err := client.Hello(ctx, &hello.HelloRequest{ 484 | Name: "alice", 485 | Num: 35, 486 | RequestTime: timestamppb.New(now), 487 | }) 488 | if err != nil { 489 | t.Error(err) 490 | return 491 | } 492 | if got.CreateTime.AsTime().Unix() != tt.wantTime.Unix() { 493 | t.Errorf("got %v\nwant %v", got.CreateTime.AsTime(), tt.wantTime) 494 | } 495 | }) 496 | } 497 | } 498 | 499 | func TestTLSServer(t *testing.T) { 500 | ctx := context.Background() 501 | cacert, err := os.ReadFile("testdata/cacert.pem") 502 | if err != nil { 503 | t.Fatal(err) 504 | } 505 | cert, err := os.ReadFile("testdata/cert.pem") 506 | if err != nil { 507 | t.Fatal(err) 508 | } 509 | key, err := os.ReadFile("testdata/key.pem") 510 | if err != nil { 511 | t.Fatal(err) 512 | } 513 | ts := NewTLSServer(t, "testdata/route_guide.proto", cacert, cert, key) 514 | t.Cleanup(func() { 515 | ts.Close() 516 | }) 517 | ts.Method("GetFeature").Response(map[string]any{"name": "hello", "location": map[string]any{"latitude": 10, "longitude": 13}}) 518 | client := routeguide.NewRouteGuideClient(ts.Conn()) 519 | res, err := client.GetFeature(ctx, &routeguide.Point{ 520 | Latitude: 10, 521 | Longitude: 13, 522 | }) 523 | if err != nil { 524 | t.Fatal(err) 525 | } 526 | { 527 | got := res.Name 528 | if want := "hello"; got != want { 529 | t.Errorf("got %v\nwant %v", got, want) 530 | return 531 | } 532 | } 533 | { 534 | got := res.Location.Latitude 535 | if want := int32(10); got != want { 536 | t.Errorf("got %v\nwant %v", got, want) 537 | } 538 | } 539 | 540 | { 541 | got := len(ts.Requests()) 542 | if want := 1; got != want { 543 | t.Errorf("got %v\nwant %v", got, want) 544 | } 545 | } 546 | } 547 | 548 | func TestHealthCheck(t *testing.T) { 549 | tests := []struct { 550 | enable bool 551 | wantErr bool 552 | }{ 553 | {true, false}, 554 | {false, true}, 555 | } 556 | ctx := context.Background() 557 | for i, tt := range tests { 558 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 559 | var ts *Server 560 | if tt.enable { 561 | ts = NewServer(t, "testdata/*.proto", EnableHealthCheck()) 562 | } else { 563 | ts = NewServer(t, "testdata/*.proto") 564 | } 565 | t.Cleanup(func() { 566 | ts.Close() 567 | }) 568 | client := healthpb.NewHealthClient(ts.ClientConn()) 569 | _, err := client.Check(ctx, &healthpb.HealthCheckRequest{ 570 | Service: HealthCheckService_DEFAULT, 571 | }) 572 | if err != nil { 573 | if !tt.wantErr { 574 | t.Errorf("got error: %s", err) 575 | } 576 | return 577 | } 578 | if tt.wantErr { 579 | t.Error("want error") 580 | } 581 | }) 582 | } 583 | } 584 | 585 | func TestReflection(t *testing.T) { 586 | tests := []struct { 587 | disableReflection bool 588 | wantErr bool 589 | }{ 590 | {false, false}, 591 | {true, true}, 592 | } 593 | proto := "testdata/route_guide.proto" 594 | ctx := context.Background() 595 | for i, tt := range tests { 596 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 597 | opts := []Option{} 598 | if tt.disableReflection { 599 | opts = append(opts, DisableReflection()) 600 | } 601 | ts := NewServer(t, proto, opts...) 602 | t.Cleanup(func() { 603 | ts.Close() 604 | }) 605 | cc := ts.ClientConn() 606 | client := grpcreflect.NewClientAuto(ctx, cc) 607 | _, err := client.ListServices() 608 | if err != nil { 609 | if !tt.wantErr { 610 | t.Errorf("got error: %v", err) 611 | } 612 | return 613 | } 614 | if tt.wantErr { 615 | t.Error("want error") 616 | } 617 | }) 618 | } 619 | } 620 | 621 | func TestRequestStringer(t *testing.T) { 622 | tests := []struct { 623 | r *Request 624 | }{ 625 | { 626 | &Request{ 627 | Service: "helloworld.Greeter", 628 | Method: "SayHello", 629 | Message: map[string]any{"name": "alice"}, 630 | Headers: map[string][]string{"foo": {"bar", "barbar"}, "baz": {"qux"}}, 631 | }, 632 | }, 633 | { 634 | &Request{ 635 | Service: "helloworld.Greeter", 636 | Method: "SayHello", 637 | }, 638 | }, 639 | } 640 | for i, tt := range tests { 641 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 642 | got := tt.r.String() 643 | f := fmt.Sprintf("request_stringer_%d", i) 644 | if os.Getenv("UPDATE_GOLDEN") != "" { 645 | golden.Update(t, "testdata", f, got) 646 | return 647 | } 648 | if diff := golden.Diff(t, "testdata", f, got); diff != "" { 649 | t.Error(diff) 650 | } 651 | }) 652 | } 653 | } 654 | 655 | func TestResponseAny(t *testing.T) { 656 | ctx := context.Background() 657 | ts := NewServer(t, "testdata/route_guide.proto") 658 | t.Cleanup(func() { 659 | ts.Close() 660 | }) 661 | ts.Service("routeguide.RouteGuide").Method("GetFeature").Response(&routeguide.Feature{ 662 | Name: "hello", 663 | }) 664 | 665 | client := routeguide.NewRouteGuideClient(ts.Conn()) 666 | res, err := client.GetFeature(ctx, &routeguide.Point{ 667 | Latitude: 10, 668 | Longitude: 13, 669 | }) 670 | if err != nil { 671 | t.Fatal(err) 672 | } 673 | got := res.Name 674 | if want := "hello"; got != want { 675 | t.Errorf("got %v\nwant %v", got, want) 676 | } 677 | } 678 | 679 | func TestResponseAnyFields(t *testing.T) { 680 | ctx := context.Background() 681 | ts := NewServer(t, "testdata/hello.proto") 682 | t.Cleanup(func() { 683 | ts.Close() 684 | }) 685 | ts.Service("hello.GrpcTestService").Method("HelloFields").ResponseString(`{"field_bytes": "aGVsbG8="}`) // Base64 encoding to pass bytes type 686 | 687 | client := hello.NewGrpcTestServiceClient(ts.Conn()) 688 | res, err := client.HelloFields(ctx, &hello.HelloFieldsRequest{ 689 | FieldBytes: []byte("hello"), 690 | }) 691 | if err != nil { 692 | t.Fatal(err) 693 | } 694 | got := res.FieldBytes 695 | if want := "hello"; string(got) != want { 696 | t.Errorf("got %v\nwant %v", got, want) 697 | } 698 | } 699 | 700 | func TestBufProtoRegistry(t *testing.T) { 701 | t.Run("Use buf.lock", func(t *testing.T) { 702 | ts := NewServer(t, "testdata/bsr/protobuf/pinger/pinger.proto", BufLock("testdata/bsr/protobuf/buf.lock")) 703 | t.Cleanup(func() { 704 | ts.Close() 705 | }) 706 | ts.Service("pinger.PingerService").Method("Ping").Response(map[string]any{ 707 | "message": "hello", 708 | }) 709 | }) 710 | 711 | t.Run("Use buf.yaml", func(t *testing.T) { 712 | ts := NewServer(t, "testdata/bsr/protobuf/pinger/pinger.proto", BufConfig("testdata/bsr/protobuf/buf.yaml")) 713 | t.Cleanup(func() { 714 | ts.Close() 715 | }) 716 | ts.Service("pinger.PingerService").Method("Ping").Response(map[string]any{ 717 | "message": "hello", 718 | }) 719 | }) 720 | 721 | t.Run("Specify modules", func(t *testing.T) { 722 | ts := NewServer(t, "testdata/bsr/protobuf/pinger/pinger.proto", BufModule("buf.build/bufbuild/protovalidate/tree/b983156c5e994cc9892e0ce3e64e17e0")) 723 | t.Cleanup(func() { 724 | ts.Close() 725 | }) 726 | ts.Service("pinger.PingerService").Method("Ping").Response(map[string]any{ 727 | "message": "hello", 728 | }) 729 | }) 730 | } 731 | 732 | func TestWithConnectClient(t *testing.T) { 733 | ctx := context.Background() 734 | cacert, err := os.ReadFile("testdata/cacert.pem") 735 | if err != nil { 736 | t.Fatal(err) 737 | } 738 | cert, err := os.ReadFile("testdata/cert.pem") 739 | if err != nil { 740 | t.Fatal(err) 741 | } 742 | key, err := os.ReadFile("testdata/key.pem") 743 | if err != nil { 744 | t.Fatal(err) 745 | } 746 | ts := NewTLSServer(t, "testdata/bsr/protobuf", cacert, cert, key) 747 | ts.Service("pinger.PingerService").Method("Ping").Response(&pinger.PingResponse{ 748 | Message: "hello", 749 | }) 750 | t.Cleanup(func() { 751 | ts.Close() 752 | }) 753 | u := fmt.Sprintf("https://%s", ts.Addr()) 754 | httpClient := &http.Client{ 755 | Transport: &http2.Transport{ 756 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint: gosec 757 | }, 758 | } 759 | client := pingerconnect.NewPingerServiceClient(httpClient, u, connect.WithGRPC()) 760 | res, err := client.Ping(ctx, connect.NewRequest(&pinger.PingRequest{ 761 | Message: "hello", 762 | })) 763 | if err != nil { 764 | t.Fatal(err) 765 | } 766 | if want := "hello"; res.Msg.GetMessage() != want { 767 | t.Errorf("got %v\nwant %v", res.Msg.GetMessage(), want) 768 | } 769 | } 770 | 771 | func TestUnmarshalProtoMessage(t *testing.T) { 772 | ctx := context.Background() 773 | ts := NewServer(t, "testdata/route_guide.proto") 774 | t.Cleanup(func() { 775 | ts.Close() 776 | }) 777 | ts.Match(func(req *Request) bool { 778 | return req.Method == "GetFeature" 779 | }).Handler(func(req *Request) *Response { 780 | m := &routeguide.Point{} 781 | if err := UnmarshalProtoMessage(req.Message, m); err != nil { 782 | t.Fatal(err) 783 | } 784 | if m.Latitude != 10 || m.Longitude != 13 { 785 | t.Errorf("got %v\nwant %v", m, &routeguide.Point{Latitude: 10, Longitude: 13}) 786 | } 787 | return &Response{ 788 | Messages: []Message{ 789 | {"name": "hello"}, 790 | }, 791 | } 792 | }) 793 | 794 | client := routeguide.NewRouteGuideClient(ts.Conn()) 795 | res, err := client.GetFeature(ctx, &routeguide.Point{ 796 | Latitude: 10, 797 | Longitude: 13, 798 | }) 799 | if err != nil { 800 | t.Fatal(err) 801 | } 802 | got := res.Name 803 | if want := "hello"; got != want { 804 | t.Errorf("got %v\nwant %v", got, want) 805 | } 806 | } 807 | 808 | func TestPrepend(t *testing.T) { 809 | t.Run("Default", func(t *testing.T) { 810 | ctx := context.Background() 811 | ts := NewServer(t, "testdata/route_guide.proto") 812 | t.Cleanup(func() { 813 | ts.Close() 814 | }) 815 | ts.Service("routeguide.RouteGuide").Response(map[string]any{"name": "hello"}) 816 | ts.Service("routeguide.RouteGuide").Response(map[string]any{"name": "world"}) 817 | ts.Service("routeguide.RouteGuide").Response(map[string]any{"name": "!!!"}) 818 | 819 | client := routeguide.NewRouteGuideClient(ts.Conn()) 820 | res, err := client.GetFeature(ctx, &routeguide.Point{ 821 | Latitude: 10, 822 | Longitude: 13, 823 | }) 824 | if err != nil { 825 | t.Fatal(err) 826 | } 827 | got := res.Name 828 | if want := "hello"; got != want { 829 | t.Errorf("got %v\nwant %v", got, want) 830 | } 831 | }) 832 | 833 | t.Run("Prepend", func(t *testing.T) { 834 | ctx := context.Background() 835 | ts := NewServer(t, "testdata/route_guide.proto") 836 | t.Cleanup(func() { 837 | ts.Close() 838 | }) 839 | ts.Service("routeguide.RouteGuide").Response(map[string]any{"name": "hello"}) 840 | ts.Prepend().Service("routeguide.RouteGuide").Response(map[string]any{"name": "world"}) 841 | ts.Service("routeguide.RouteGuide").Response(map[string]any{"name": "!!!"}) 842 | 843 | client := routeguide.NewRouteGuideClient(ts.Conn()) 844 | res, err := client.GetFeature(ctx, &routeguide.Point{ 845 | Latitude: 10, 846 | Longitude: 13, 847 | }) 848 | if err != nil { 849 | t.Fatal(err) 850 | } 851 | got := res.Name 852 | if want := "world"; got != want { 853 | t.Errorf("got %v\nwant %v", got, want) 854 | } 855 | }) 856 | } 857 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package grpcstub 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/bmatcuk/doublestar/v4" 9 | ) 10 | 11 | type config struct { 12 | protos []string 13 | importPaths []string 14 | useTLS bool 15 | cacert, cert, key []byte 16 | healthCheck bool 17 | disableReflection bool 18 | bufDirs []string 19 | bufLocks []string 20 | bufConfigs []string 21 | bufModules []string 22 | } 23 | 24 | type Option func(*config) error 25 | 26 | // Proto append protos 27 | func Proto(protos ...string) Option { 28 | return func(c *config) error { 29 | for _, p := range protos { 30 | opt := proto(p) 31 | if err := opt(c); err != nil { 32 | return err 33 | } 34 | } 35 | return nil 36 | } 37 | } 38 | 39 | // ImportPath set import paths 40 | func ImportPath(paths ...string) Option { 41 | return func(c *config) error { 42 | c.importPaths = unique(append(c.importPaths, paths...)) 43 | return nil 44 | } 45 | } 46 | 47 | // UseTLS enable TLS 48 | func UseTLS(cacert, cert, key []byte) Option { 49 | return func(c *config) error { 50 | c.useTLS = true 51 | c.cacert = cacert 52 | c.cert = cert 53 | c.key = key 54 | return nil 55 | } 56 | } 57 | 58 | // EnableHealthCheck enable grpc.health.v1 59 | func EnableHealthCheck() Option { 60 | return func(c *config) error { 61 | c.healthCheck = true 62 | return nil 63 | } 64 | } 65 | 66 | // DisableReflection disable Server Reflection Protocol 67 | func DisableReflection() Option { 68 | return func(c *config) error { 69 | c.disableReflection = true 70 | return nil 71 | } 72 | } 73 | 74 | // BufDir use buf directory. 75 | func BufDir(dirs ...string) Option { 76 | return func(c *config) error { 77 | c.bufDirs = unique(append(c.bufDirs, dirs...)) 78 | return nil 79 | } 80 | } 81 | 82 | // BufLock use buf.lock for BSR. 83 | func BufLock(locks ...string) Option { 84 | return func(c *config) error { 85 | c.bufLocks = unique(append(c.bufLocks, locks...)) 86 | return nil 87 | } 88 | } 89 | 90 | // BufConfig use buf.yaml for BSR. 91 | func BufConfig(configs ...string) Option { 92 | return func(c *config) error { 93 | c.bufConfigs = unique(append(c.bufConfigs, configs...)) 94 | return nil 95 | } 96 | } 97 | 98 | // BufModule use buf modules for BSR. 99 | func BufModule(module string) Option { 100 | return func(c *config) error { 101 | c.bufModules = unique(append(c.bufModules, module)) 102 | return nil 103 | } 104 | } 105 | 106 | // BufModules use buf modules for BSR. 107 | func BufModules(modules []string) Option { 108 | return func(c *config) error { 109 | for _, m := range modules { 110 | opt := BufModule(m) 111 | if err := opt(c); err != nil { 112 | return err 113 | } 114 | } 115 | return nil 116 | } 117 | } 118 | 119 | func proto(proto string) Option { 120 | return func(c *config) error { 121 | protos := []string{} 122 | if f, err := os.Stat(proto); err == nil { 123 | if !f.IsDir() { 124 | c.protos = unique(append(c.protos, proto)) 125 | return nil 126 | } 127 | proto = filepath.Join(proto, "*") 128 | } 129 | base, pattern := doublestar.SplitPattern(filepath.ToSlash(proto)) 130 | abs, err := filepath.Abs(base) 131 | if err != nil { 132 | return err 133 | } 134 | fsys := os.DirFS(abs) 135 | if err := doublestar.GlobWalk(fsys, pattern, func(p string, d fs.DirEntry) error { 136 | if d.IsDir() { 137 | return nil 138 | } 139 | protos = unique(append(protos, filepath.Join(base, p))) 140 | return nil 141 | }); err != nil { 142 | return err 143 | } 144 | if len(protos) == 0 { 145 | c.protos = unique(append(c.protos, proto)) 146 | } else { 147 | c.protos = unique(append(c.protos, protos...)) 148 | } 149 | return nil 150 | } 151 | } 152 | 153 | func unique(in []string) []string { 154 | u := []string{} 155 | m := map[string]struct{}{} 156 | for _, s := range in { 157 | if _, ok := m[s]; ok { 158 | continue 159 | } 160 | u = append(u, s) 161 | m[s] = struct{}{} 162 | } 163 | return u 164 | } 165 | -------------------------------------------------------------------------------- /serverstreaming_test.go: -------------------------------------------------------------------------------- 1 | package grpcstub 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "testing" 8 | 9 | "github.com/k1LoW/grpcstub/testdata/routeguide" 10 | ) 11 | 12 | func TestServerStreaming(t *testing.T) { 13 | ctx := context.Background() 14 | ts := NewServer(t, "testdata/route_guide.proto") 15 | t.Cleanup(func() { 16 | ts.Close() 17 | }) 18 | ts.Method("ListFeatures").Response(map[string]any{"name": "hello"}).Response(map[string]any{"name": "world"}) 19 | 20 | client := routeguide.NewRouteGuideClient(ts.Conn()) 21 | stream, err := client.ListFeatures(ctx, &routeguide.Rectangle{ 22 | Lo: &routeguide.Point{ 23 | Latitude: int32(10), 24 | Longitude: int32(2), 25 | }, 26 | Hi: &routeguide.Point{ 27 | Latitude: int32(20), 28 | Longitude: int32(7), 29 | }, 30 | }) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | c := 0 36 | for { 37 | res, err := stream.Recv() 38 | if errors.Is(err, io.EOF) { 39 | break 40 | } 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | switch c { 45 | case 0: 46 | got := res.Name 47 | if want := "hello"; got != want { 48 | t.Errorf("got %v\nwant %v", got, want) 49 | } 50 | case 1: 51 | got := res.Name 52 | if want := "world"; got != want { 53 | t.Errorf("got %v\nwant %v", got, want) 54 | } 55 | default: 56 | t.Errorf("recv messages got %v\nwant %v", c+1, 2) 57 | } 58 | c++ 59 | } 60 | 61 | { 62 | got := len(ts.Requests()) 63 | if want := 1; got != want { 64 | t.Errorf("got %v\nwant %v", got, want) 65 | } 66 | } 67 | } 68 | 69 | func TestServerStreamingUnmatched(t *testing.T) { 70 | ctx := context.Background() 71 | ts := NewServer(t, "testdata/route_guide.proto") 72 | t.Cleanup(func() { 73 | ts.Close() 74 | }) 75 | ts.Method("ListFeatures").Match(func(req *Request) bool { 76 | return false 77 | }).Response(map[string]any{"name": "hello"}).Response(map[string]any{"name": "world"}) 78 | 79 | client := routeguide.NewRouteGuideClient(ts.Conn()) 80 | stream, err := client.ListFeatures(ctx, &routeguide.Rectangle{ 81 | Lo: &routeguide.Point{ 82 | Latitude: int32(10), 83 | Longitude: int32(2), 84 | }, 85 | Hi: &routeguide.Point{ 86 | Latitude: int32(20), 87 | Longitude: int32(7), 88 | }, 89 | }) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | if _, err := stream.Recv(); err == nil || errors.Is(err, io.EOF) { 95 | t.Error("want error") 96 | } 97 | 98 | { 99 | got := len(ts.Requests()) 100 | if want := 0; got != want { 101 | t.Errorf("got %v\nwant %v", got, want) 102 | } 103 | } 104 | { 105 | got := len(ts.UnmatchedRequests()) 106 | if want := 1; got != want { 107 | t.Errorf("got %v\nwant %v", got, want) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /testdata/bsr/protobuf/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | managed: 3 | enabled: true 4 | go_package_prefix: 5 | default: github.com/k1LoW/grpcstub/testdata/bsr/protobuf/gen/go 6 | except: 7 | - buf.build/bufbuild/protovalidate 8 | plugins: 9 | - name: go 10 | out: gen/go 11 | opt: paths=source_relative 12 | - name: go-grpc 13 | out: gen/go 14 | opt: paths=source_relative 15 | - name: connect-go 16 | out: gen/go 17 | opt: paths=source_relative 18 | -------------------------------------------------------------------------------- /testdata/bsr/protobuf/buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v1 3 | deps: 4 | - remote: buf.build 5 | owner: bufbuild 6 | repository: protovalidate 7 | commit: 5a7b106cbb87462d9a8c9ffecdbd2e38 8 | digest: shake256:2f7efa5a904668219f039d4f6eeb51e871f8f7f5966055a10663cba335bd65f76cac84da3fa758ab7b5dcb489ec599521390ce3951d119fb56df1fc2def16bb0 9 | -------------------------------------------------------------------------------- /testdata/bsr/protobuf/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | breaking: 3 | use: 4 | - FILE 5 | lint: 6 | use: 7 | - DEFAULT 8 | deps: 9 | - buf.build/bufbuild/protovalidate 10 | -------------------------------------------------------------------------------- /testdata/bsr/protobuf/gen/go/pinger/pinger.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.34.2 4 | // protoc (unknown) 5 | // source: pinger/pinger.proto 6 | 7 | package pinger 8 | 9 | import ( 10 | _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type PingRequest struct { 25 | state protoimpl.MessageState 26 | sizeCache protoimpl.SizeCache 27 | unknownFields protoimpl.UnknownFields 28 | 29 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 30 | } 31 | 32 | func (x *PingRequest) Reset() { 33 | *x = PingRequest{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_pinger_pinger_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *PingRequest) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*PingRequest) ProtoMessage() {} 46 | 47 | func (x *PingRequest) ProtoReflect() protoreflect.Message { 48 | mi := &file_pinger_pinger_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. 60 | func (*PingRequest) Descriptor() ([]byte, []int) { 61 | return file_pinger_pinger_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *PingRequest) GetMessage() string { 65 | if x != nil { 66 | return x.Message 67 | } 68 | return "" 69 | } 70 | 71 | type PingResponse struct { 72 | state protoimpl.MessageState 73 | sizeCache protoimpl.SizeCache 74 | unknownFields protoimpl.UnknownFields 75 | 76 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 77 | } 78 | 79 | func (x *PingResponse) Reset() { 80 | *x = PingResponse{} 81 | if protoimpl.UnsafeEnabled { 82 | mi := &file_pinger_pinger_proto_msgTypes[1] 83 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 84 | ms.StoreMessageInfo(mi) 85 | } 86 | } 87 | 88 | func (x *PingResponse) String() string { 89 | return protoimpl.X.MessageStringOf(x) 90 | } 91 | 92 | func (*PingResponse) ProtoMessage() {} 93 | 94 | func (x *PingResponse) ProtoReflect() protoreflect.Message { 95 | mi := &file_pinger_pinger_proto_msgTypes[1] 96 | if protoimpl.UnsafeEnabled && x != nil { 97 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 98 | if ms.LoadMessageInfo() == nil { 99 | ms.StoreMessageInfo(mi) 100 | } 101 | return ms 102 | } 103 | return mi.MessageOf(x) 104 | } 105 | 106 | // Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. 107 | func (*PingResponse) Descriptor() ([]byte, []int) { 108 | return file_pinger_pinger_proto_rawDescGZIP(), []int{1} 109 | } 110 | 111 | func (x *PingResponse) GetMessage() string { 112 | if x != nil { 113 | return x.Message 114 | } 115 | return "" 116 | } 117 | 118 | var File_pinger_pinger_proto protoreflect.FileDescriptor 119 | 120 | var file_pinger_pinger_proto_rawDesc = []byte{ 121 | 0x0a, 0x13, 0x70, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x2e, 122 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x70, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x1a, 0x1b, 0x62, 123 | 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 124 | 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x30, 0x0a, 0x0b, 0x50, 0x69, 125 | 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x07, 0x6d, 0x65, 0x73, 126 | 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 127 | 0x02, 0x10, 0x01, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x31, 0x0a, 0x0c, 128 | 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x07, 129 | 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 130 | 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 131 | 0x42, 0x0a, 0x0d, 0x50, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 132 | 0x12, 0x31, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x13, 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x65, 133 | 0x72, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 134 | 0x70, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 135 | 0x6e, 0x73, 0x65, 0x42, 0x90, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x69, 0x6e, 0x67, 136 | 0x65, 0x72, 0x42, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 137 | 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x31, 138 | 0x4c, 0x6f, 0x57, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x73, 0x74, 0x75, 0x62, 0x2f, 0x74, 0x65, 0x73, 139 | 0x74, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x62, 0x73, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 140 | 0x75, 0x66, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x69, 0x6e, 0x67, 0x65, 0x72, 141 | 0xa2, 0x02, 0x03, 0x50, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x50, 0x69, 0x6e, 0x67, 0x65, 0x72, 0xca, 142 | 0x02, 0x06, 0x50, 0x69, 0x6e, 0x67, 0x65, 0x72, 0xe2, 0x02, 0x12, 0x50, 0x69, 0x6e, 0x67, 0x65, 143 | 0x72, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x06, 144 | 0x50, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 145 | } 146 | 147 | var ( 148 | file_pinger_pinger_proto_rawDescOnce sync.Once 149 | file_pinger_pinger_proto_rawDescData = file_pinger_pinger_proto_rawDesc 150 | ) 151 | 152 | func file_pinger_pinger_proto_rawDescGZIP() []byte { 153 | file_pinger_pinger_proto_rawDescOnce.Do(func() { 154 | file_pinger_pinger_proto_rawDescData = protoimpl.X.CompressGZIP(file_pinger_pinger_proto_rawDescData) 155 | }) 156 | return file_pinger_pinger_proto_rawDescData 157 | } 158 | 159 | var file_pinger_pinger_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 160 | var file_pinger_pinger_proto_goTypes = []any{ 161 | (*PingRequest)(nil), // 0: pinger.PingRequest 162 | (*PingResponse)(nil), // 1: pinger.PingResponse 163 | } 164 | var file_pinger_pinger_proto_depIdxs = []int32{ 165 | 0, // 0: pinger.PingerService.Ping:input_type -> pinger.PingRequest 166 | 1, // 1: pinger.PingerService.Ping:output_type -> pinger.PingResponse 167 | 1, // [1:2] is the sub-list for method output_type 168 | 0, // [0:1] is the sub-list for method input_type 169 | 0, // [0:0] is the sub-list for extension type_name 170 | 0, // [0:0] is the sub-list for extension extendee 171 | 0, // [0:0] is the sub-list for field type_name 172 | } 173 | 174 | func init() { file_pinger_pinger_proto_init() } 175 | func file_pinger_pinger_proto_init() { 176 | if File_pinger_pinger_proto != nil { 177 | return 178 | } 179 | if !protoimpl.UnsafeEnabled { 180 | file_pinger_pinger_proto_msgTypes[0].Exporter = func(v any, i int) any { 181 | switch v := v.(*PingRequest); i { 182 | case 0: 183 | return &v.state 184 | case 1: 185 | return &v.sizeCache 186 | case 2: 187 | return &v.unknownFields 188 | default: 189 | return nil 190 | } 191 | } 192 | file_pinger_pinger_proto_msgTypes[1].Exporter = func(v any, i int) any { 193 | switch v := v.(*PingResponse); i { 194 | case 0: 195 | return &v.state 196 | case 1: 197 | return &v.sizeCache 198 | case 2: 199 | return &v.unknownFields 200 | default: 201 | return nil 202 | } 203 | } 204 | } 205 | type x struct{} 206 | out := protoimpl.TypeBuilder{ 207 | File: protoimpl.DescBuilder{ 208 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 209 | RawDescriptor: file_pinger_pinger_proto_rawDesc, 210 | NumEnums: 0, 211 | NumMessages: 2, 212 | NumExtensions: 0, 213 | NumServices: 1, 214 | }, 215 | GoTypes: file_pinger_pinger_proto_goTypes, 216 | DependencyIndexes: file_pinger_pinger_proto_depIdxs, 217 | MessageInfos: file_pinger_pinger_proto_msgTypes, 218 | }.Build() 219 | File_pinger_pinger_proto = out.File 220 | file_pinger_pinger_proto_rawDesc = nil 221 | file_pinger_pinger_proto_goTypes = nil 222 | file_pinger_pinger_proto_depIdxs = nil 223 | } 224 | -------------------------------------------------------------------------------- /testdata/bsr/protobuf/gen/go/pinger/pinger_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.4.0 4 | // - protoc (unknown) 5 | // source: pinger/pinger.proto 6 | 7 | package pinger 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.62.0 or later. 19 | const _ = grpc.SupportPackageIsVersion8 20 | 21 | const ( 22 | PingerService_Ping_FullMethodName = "/pinger.PingerService/Ping" 23 | ) 24 | 25 | // PingerServiceClient is the client API for PingerService service. 26 | // 27 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 28 | type PingerServiceClient interface { 29 | Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) 30 | } 31 | 32 | type pingerServiceClient struct { 33 | cc grpc.ClientConnInterface 34 | } 35 | 36 | func NewPingerServiceClient(cc grpc.ClientConnInterface) PingerServiceClient { 37 | return &pingerServiceClient{cc} 38 | } 39 | 40 | func (c *pingerServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { 41 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 42 | out := new(PingResponse) 43 | err := c.cc.Invoke(ctx, PingerService_Ping_FullMethodName, in, out, cOpts...) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return out, nil 48 | } 49 | 50 | // PingerServiceServer is the server API for PingerService service. 51 | // All implementations must embed UnimplementedPingerServiceServer 52 | // for forward compatibility 53 | type PingerServiceServer interface { 54 | Ping(context.Context, *PingRequest) (*PingResponse, error) 55 | mustEmbedUnimplementedPingerServiceServer() 56 | } 57 | 58 | // UnimplementedPingerServiceServer must be embedded to have forward compatible implementations. 59 | type UnimplementedPingerServiceServer struct { 60 | } 61 | 62 | func (UnimplementedPingerServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { 63 | return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented") 64 | } 65 | func (UnimplementedPingerServiceServer) mustEmbedUnimplementedPingerServiceServer() {} 66 | 67 | // UnsafePingerServiceServer may be embedded to opt out of forward compatibility for this service. 68 | // Use of this interface is not recommended, as added methods to PingerServiceServer will 69 | // result in compilation errors. 70 | type UnsafePingerServiceServer interface { 71 | mustEmbedUnimplementedPingerServiceServer() 72 | } 73 | 74 | func RegisterPingerServiceServer(s grpc.ServiceRegistrar, srv PingerServiceServer) { 75 | s.RegisterService(&PingerService_ServiceDesc, srv) 76 | } 77 | 78 | func _PingerService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 79 | in := new(PingRequest) 80 | if err := dec(in); err != nil { 81 | return nil, err 82 | } 83 | if interceptor == nil { 84 | return srv.(PingerServiceServer).Ping(ctx, in) 85 | } 86 | info := &grpc.UnaryServerInfo{ 87 | Server: srv, 88 | FullMethod: PingerService_Ping_FullMethodName, 89 | } 90 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 91 | return srv.(PingerServiceServer).Ping(ctx, req.(*PingRequest)) 92 | } 93 | return interceptor(ctx, in, info, handler) 94 | } 95 | 96 | // PingerService_ServiceDesc is the grpc.ServiceDesc for PingerService service. 97 | // It's only intended for direct use with grpc.RegisterService, 98 | // and not to be introspected or modified (even as a copy) 99 | var PingerService_ServiceDesc = grpc.ServiceDesc{ 100 | ServiceName: "pinger.PingerService", 101 | HandlerType: (*PingerServiceServer)(nil), 102 | Methods: []grpc.MethodDesc{ 103 | { 104 | MethodName: "Ping", 105 | Handler: _PingerService_Ping_Handler, 106 | }, 107 | }, 108 | Streams: []grpc.StreamDesc{}, 109 | Metadata: "pinger/pinger.proto", 110 | } 111 | -------------------------------------------------------------------------------- /testdata/bsr/protobuf/gen/go/pinger/pingerconnect/pinger.connect.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-connect-go. DO NOT EDIT. 2 | // 3 | // Source: pinger/pinger.proto 4 | 5 | package pingerconnect 6 | 7 | import ( 8 | connect "connectrpc.com/connect" 9 | context "context" 10 | errors "errors" 11 | pinger "github.com/k1LoW/grpcstub/testdata/bsr/protobuf/gen/go/pinger" 12 | http "net/http" 13 | strings "strings" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file and the connect package are 17 | // compatible. If you get a compiler error that this constant is not defined, this code was 18 | // generated with a version of connect newer than the one compiled into your binary. You can fix the 19 | // problem by either regenerating this code with an older version of connect or updating the connect 20 | // version compiled into your binary. 21 | const _ = connect.IsAtLeastVersion1_13_0 22 | 23 | const ( 24 | // PingerServiceName is the fully-qualified name of the PingerService service. 25 | PingerServiceName = "pinger.PingerService" 26 | ) 27 | 28 | // These constants are the fully-qualified names of the RPCs defined in this package. They're 29 | // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. 30 | // 31 | // Note that these are different from the fully-qualified method names used by 32 | // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to 33 | // reflection-formatted method names, remove the leading slash and convert the remaining slash to a 34 | // period. 35 | const ( 36 | // PingerServicePingProcedure is the fully-qualified name of the PingerService's Ping RPC. 37 | PingerServicePingProcedure = "/pinger.PingerService/Ping" 38 | ) 39 | 40 | // These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. 41 | var ( 42 | pingerServiceServiceDescriptor = pinger.File_pinger_pinger_proto.Services().ByName("PingerService") 43 | pingerServicePingMethodDescriptor = pingerServiceServiceDescriptor.Methods().ByName("Ping") 44 | ) 45 | 46 | // PingerServiceClient is a client for the pinger.PingerService service. 47 | type PingerServiceClient interface { 48 | Ping(context.Context, *connect.Request[pinger.PingRequest]) (*connect.Response[pinger.PingResponse], error) 49 | } 50 | 51 | // NewPingerServiceClient constructs a client for the pinger.PingerService service. By default, it 52 | // uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends 53 | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or 54 | // connect.WithGRPCWeb() options. 55 | // 56 | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, 57 | // http://api.acme.com or https://acme.com/grpc). 58 | func NewPingerServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PingerServiceClient { 59 | baseURL = strings.TrimRight(baseURL, "/") 60 | return &pingerServiceClient{ 61 | ping: connect.NewClient[pinger.PingRequest, pinger.PingResponse]( 62 | httpClient, 63 | baseURL+PingerServicePingProcedure, 64 | connect.WithSchema(pingerServicePingMethodDescriptor), 65 | connect.WithClientOptions(opts...), 66 | ), 67 | } 68 | } 69 | 70 | // pingerServiceClient implements PingerServiceClient. 71 | type pingerServiceClient struct { 72 | ping *connect.Client[pinger.PingRequest, pinger.PingResponse] 73 | } 74 | 75 | // Ping calls pinger.PingerService.Ping. 76 | func (c *pingerServiceClient) Ping(ctx context.Context, req *connect.Request[pinger.PingRequest]) (*connect.Response[pinger.PingResponse], error) { 77 | return c.ping.CallUnary(ctx, req) 78 | } 79 | 80 | // PingerServiceHandler is an implementation of the pinger.PingerService service. 81 | type PingerServiceHandler interface { 82 | Ping(context.Context, *connect.Request[pinger.PingRequest]) (*connect.Response[pinger.PingResponse], error) 83 | } 84 | 85 | // NewPingerServiceHandler builds an HTTP handler from the service implementation. It returns the 86 | // path on which to mount the handler and the handler itself. 87 | // 88 | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf 89 | // and JSON codecs. They also support gzip compression. 90 | func NewPingerServiceHandler(svc PingerServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { 91 | pingerServicePingHandler := connect.NewUnaryHandler( 92 | PingerServicePingProcedure, 93 | svc.Ping, 94 | connect.WithSchema(pingerServicePingMethodDescriptor), 95 | connect.WithHandlerOptions(opts...), 96 | ) 97 | return "/pinger.PingerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 98 | switch r.URL.Path { 99 | case PingerServicePingProcedure: 100 | pingerServicePingHandler.ServeHTTP(w, r) 101 | default: 102 | http.NotFound(w, r) 103 | } 104 | }) 105 | } 106 | 107 | // UnimplementedPingerServiceHandler returns CodeUnimplemented from all methods. 108 | type UnimplementedPingerServiceHandler struct{} 109 | 110 | func (UnimplementedPingerServiceHandler) Ping(context.Context, *connect.Request[pinger.PingRequest]) (*connect.Response[pinger.PingResponse], error) { 111 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("pinger.PingerService.Ping is not implemented")) 112 | } 113 | -------------------------------------------------------------------------------- /testdata/bsr/protobuf/pinger/pinger.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pinger; 4 | 5 | import "buf/validate/validate.proto"; 6 | 7 | service PingerService { 8 | rpc Ping(PingRequest) returns (PingResponse); 9 | } 10 | 11 | message PingRequest { 12 | string message = 1 [(buf.validate.field).string.min_len = 1]; 13 | } 14 | 15 | message PingResponse { 16 | string message = 1 [(buf.validate.field).string.min_len = 1]; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /testdata/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/timestamp.proto"; 4 | 5 | option go_package="./;hello"; 6 | 7 | package hello; 8 | 9 | service GrpcTestService { 10 | rpc Hello (HelloRequest) returns (HelloResponse); 11 | rpc HelloFields (HelloFieldsRequest) returns (HelloFieldsResponse); 12 | } 13 | 14 | message HelloRequest { 15 | string name = 1; 16 | 17 | int64 num = 2; 18 | 19 | google.protobuf.Timestamp request_time = 3; 20 | } 21 | 22 | message HelloResponse { 23 | string message = 1; 24 | 25 | int64 num = 2; 26 | 27 | repeated string hellos = 3; 28 | 29 | optional string world = 4; 30 | 31 | google.protobuf.Timestamp create_time = 5; 32 | } 33 | 34 | message HelloFieldsRequest { 35 | bytes field_bytes = 1; 36 | } 37 | 38 | message HelloFieldsResponse { 39 | bytes field_bytes = 1; 40 | } 41 | -------------------------------------------------------------------------------- /testdata/hello/hello.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.34.2 4 | // protoc v4.25.3 5 | // source: hello.proto 6 | 7 | package hello 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | timestamppb "google.golang.org/protobuf/types/known/timestamppb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type HelloRequest struct { 25 | state protoimpl.MessageState 26 | sizeCache protoimpl.SizeCache 27 | unknownFields protoimpl.UnknownFields 28 | 29 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 30 | Num int64 `protobuf:"varint,2,opt,name=num,proto3" json:"num,omitempty"` 31 | RequestTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=request_time,json=requestTime,proto3" json:"request_time,omitempty"` 32 | } 33 | 34 | func (x *HelloRequest) Reset() { 35 | *x = HelloRequest{} 36 | if protoimpl.UnsafeEnabled { 37 | mi := &file_hello_proto_msgTypes[0] 38 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 39 | ms.StoreMessageInfo(mi) 40 | } 41 | } 42 | 43 | func (x *HelloRequest) String() string { 44 | return protoimpl.X.MessageStringOf(x) 45 | } 46 | 47 | func (*HelloRequest) ProtoMessage() {} 48 | 49 | func (x *HelloRequest) ProtoReflect() protoreflect.Message { 50 | mi := &file_hello_proto_msgTypes[0] 51 | if protoimpl.UnsafeEnabled && x != nil { 52 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 53 | if ms.LoadMessageInfo() == nil { 54 | ms.StoreMessageInfo(mi) 55 | } 56 | return ms 57 | } 58 | return mi.MessageOf(x) 59 | } 60 | 61 | // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. 62 | func (*HelloRequest) Descriptor() ([]byte, []int) { 63 | return file_hello_proto_rawDescGZIP(), []int{0} 64 | } 65 | 66 | func (x *HelloRequest) GetName() string { 67 | if x != nil { 68 | return x.Name 69 | } 70 | return "" 71 | } 72 | 73 | func (x *HelloRequest) GetNum() int64 { 74 | if x != nil { 75 | return x.Num 76 | } 77 | return 0 78 | } 79 | 80 | func (x *HelloRequest) GetRequestTime() *timestamppb.Timestamp { 81 | if x != nil { 82 | return x.RequestTime 83 | } 84 | return nil 85 | } 86 | 87 | type HelloResponse struct { 88 | state protoimpl.MessageState 89 | sizeCache protoimpl.SizeCache 90 | unknownFields protoimpl.UnknownFields 91 | 92 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 93 | Num int64 `protobuf:"varint,2,opt,name=num,proto3" json:"num,omitempty"` 94 | Hellos []string `protobuf:"bytes,3,rep,name=hellos,proto3" json:"hellos,omitempty"` 95 | World *string `protobuf:"bytes,4,opt,name=world,proto3,oneof" json:"world,omitempty"` 96 | CreateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` 97 | } 98 | 99 | func (x *HelloResponse) Reset() { 100 | *x = HelloResponse{} 101 | if protoimpl.UnsafeEnabled { 102 | mi := &file_hello_proto_msgTypes[1] 103 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 104 | ms.StoreMessageInfo(mi) 105 | } 106 | } 107 | 108 | func (x *HelloResponse) String() string { 109 | return protoimpl.X.MessageStringOf(x) 110 | } 111 | 112 | func (*HelloResponse) ProtoMessage() {} 113 | 114 | func (x *HelloResponse) ProtoReflect() protoreflect.Message { 115 | mi := &file_hello_proto_msgTypes[1] 116 | if protoimpl.UnsafeEnabled && x != nil { 117 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 118 | if ms.LoadMessageInfo() == nil { 119 | ms.StoreMessageInfo(mi) 120 | } 121 | return ms 122 | } 123 | return mi.MessageOf(x) 124 | } 125 | 126 | // Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead. 127 | func (*HelloResponse) Descriptor() ([]byte, []int) { 128 | return file_hello_proto_rawDescGZIP(), []int{1} 129 | } 130 | 131 | func (x *HelloResponse) GetMessage() string { 132 | if x != nil { 133 | return x.Message 134 | } 135 | return "" 136 | } 137 | 138 | func (x *HelloResponse) GetNum() int64 { 139 | if x != nil { 140 | return x.Num 141 | } 142 | return 0 143 | } 144 | 145 | func (x *HelloResponse) GetHellos() []string { 146 | if x != nil { 147 | return x.Hellos 148 | } 149 | return nil 150 | } 151 | 152 | func (x *HelloResponse) GetWorld() string { 153 | if x != nil && x.World != nil { 154 | return *x.World 155 | } 156 | return "" 157 | } 158 | 159 | func (x *HelloResponse) GetCreateTime() *timestamppb.Timestamp { 160 | if x != nil { 161 | return x.CreateTime 162 | } 163 | return nil 164 | } 165 | 166 | type HelloFieldsRequest struct { 167 | state protoimpl.MessageState 168 | sizeCache protoimpl.SizeCache 169 | unknownFields protoimpl.UnknownFields 170 | 171 | FieldBytes []byte `protobuf:"bytes,1,opt,name=field_bytes,json=fieldBytes,proto3" json:"field_bytes,omitempty"` 172 | } 173 | 174 | func (x *HelloFieldsRequest) Reset() { 175 | *x = HelloFieldsRequest{} 176 | if protoimpl.UnsafeEnabled { 177 | mi := &file_hello_proto_msgTypes[2] 178 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 179 | ms.StoreMessageInfo(mi) 180 | } 181 | } 182 | 183 | func (x *HelloFieldsRequest) String() string { 184 | return protoimpl.X.MessageStringOf(x) 185 | } 186 | 187 | func (*HelloFieldsRequest) ProtoMessage() {} 188 | 189 | func (x *HelloFieldsRequest) ProtoReflect() protoreflect.Message { 190 | mi := &file_hello_proto_msgTypes[2] 191 | if protoimpl.UnsafeEnabled && x != nil { 192 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 193 | if ms.LoadMessageInfo() == nil { 194 | ms.StoreMessageInfo(mi) 195 | } 196 | return ms 197 | } 198 | return mi.MessageOf(x) 199 | } 200 | 201 | // Deprecated: Use HelloFieldsRequest.ProtoReflect.Descriptor instead. 202 | func (*HelloFieldsRequest) Descriptor() ([]byte, []int) { 203 | return file_hello_proto_rawDescGZIP(), []int{2} 204 | } 205 | 206 | func (x *HelloFieldsRequest) GetFieldBytes() []byte { 207 | if x != nil { 208 | return x.FieldBytes 209 | } 210 | return nil 211 | } 212 | 213 | type HelloFieldsResponse struct { 214 | state protoimpl.MessageState 215 | sizeCache protoimpl.SizeCache 216 | unknownFields protoimpl.UnknownFields 217 | 218 | FieldBytes []byte `protobuf:"bytes,1,opt,name=field_bytes,json=fieldBytes,proto3" json:"field_bytes,omitempty"` 219 | } 220 | 221 | func (x *HelloFieldsResponse) Reset() { 222 | *x = HelloFieldsResponse{} 223 | if protoimpl.UnsafeEnabled { 224 | mi := &file_hello_proto_msgTypes[3] 225 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 226 | ms.StoreMessageInfo(mi) 227 | } 228 | } 229 | 230 | func (x *HelloFieldsResponse) String() string { 231 | return protoimpl.X.MessageStringOf(x) 232 | } 233 | 234 | func (*HelloFieldsResponse) ProtoMessage() {} 235 | 236 | func (x *HelloFieldsResponse) ProtoReflect() protoreflect.Message { 237 | mi := &file_hello_proto_msgTypes[3] 238 | if protoimpl.UnsafeEnabled && x != nil { 239 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 240 | if ms.LoadMessageInfo() == nil { 241 | ms.StoreMessageInfo(mi) 242 | } 243 | return ms 244 | } 245 | return mi.MessageOf(x) 246 | } 247 | 248 | // Deprecated: Use HelloFieldsResponse.ProtoReflect.Descriptor instead. 249 | func (*HelloFieldsResponse) Descriptor() ([]byte, []int) { 250 | return file_hello_proto_rawDescGZIP(), []int{3} 251 | } 252 | 253 | func (x *HelloFieldsResponse) GetFieldBytes() []byte { 254 | if x != nil { 255 | return x.FieldBytes 256 | } 257 | return nil 258 | } 259 | 260 | var File_hello_proto protoreflect.FileDescriptor 261 | 262 | var file_hello_proto_rawDesc = []byte{ 263 | 0x0a, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x68, 264 | 0x65, 0x6c, 0x6c, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 265 | 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 266 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x73, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 267 | 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 268 | 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6e, 0x75, 0x6d, 269 | 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6e, 0x75, 0x6d, 0x12, 0x3d, 0x0a, 0x0c, 0x72, 270 | 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 271 | 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 272 | 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x72, 273 | 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x48, 274 | 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 275 | 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 276 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6e, 0x75, 0x6d, 0x18, 0x02, 0x20, 277 | 0x01, 0x28, 0x03, 0x52, 0x03, 0x6e, 0x75, 0x6d, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x6c, 0x6c, 278 | 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 279 | 0x12, 0x19, 0x0a, 0x05, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 280 | 0x00, 0x52, 0x05, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x88, 0x01, 0x01, 0x12, 0x3b, 0x0a, 0x0b, 0x63, 281 | 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 282 | 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 283 | 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 284 | 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x77, 0x6f, 0x72, 285 | 0x6c, 0x64, 0x22, 0x35, 0x0a, 0x12, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x46, 0x69, 0x65, 0x6c, 0x64, 286 | 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x69, 0x65, 0x6c, 287 | 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x66, 288 | 0x69, 0x65, 0x6c, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0x36, 0x0a, 0x13, 0x48, 0x65, 0x6c, 289 | 0x6c, 0x6f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 290 | 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 291 | 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x42, 0x79, 0x74, 0x65, 292 | 0x73, 0x32, 0x8b, 0x01, 0x0a, 0x0f, 0x47, 0x72, 0x70, 0x63, 0x54, 0x65, 0x73, 0x74, 0x53, 0x65, 293 | 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x13, 294 | 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 295 | 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 296 | 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x48, 0x65, 0x6c, 297 | 0x6c, 0x6f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x19, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 298 | 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 299 | 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 300 | 0x6f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 301 | 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x3b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 302 | 0x74, 0x6f, 0x33, 303 | } 304 | 305 | var ( 306 | file_hello_proto_rawDescOnce sync.Once 307 | file_hello_proto_rawDescData = file_hello_proto_rawDesc 308 | ) 309 | 310 | func file_hello_proto_rawDescGZIP() []byte { 311 | file_hello_proto_rawDescOnce.Do(func() { 312 | file_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_hello_proto_rawDescData) 313 | }) 314 | return file_hello_proto_rawDescData 315 | } 316 | 317 | var file_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 318 | var file_hello_proto_goTypes = []any{ 319 | (*HelloRequest)(nil), // 0: hello.HelloRequest 320 | (*HelloResponse)(nil), // 1: hello.HelloResponse 321 | (*HelloFieldsRequest)(nil), // 2: hello.HelloFieldsRequest 322 | (*HelloFieldsResponse)(nil), // 3: hello.HelloFieldsResponse 323 | (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp 324 | } 325 | var file_hello_proto_depIdxs = []int32{ 326 | 4, // 0: hello.HelloRequest.request_time:type_name -> google.protobuf.Timestamp 327 | 4, // 1: hello.HelloResponse.create_time:type_name -> google.protobuf.Timestamp 328 | 0, // 2: hello.GrpcTestService.Hello:input_type -> hello.HelloRequest 329 | 2, // 3: hello.GrpcTestService.HelloFields:input_type -> hello.HelloFieldsRequest 330 | 1, // 4: hello.GrpcTestService.Hello:output_type -> hello.HelloResponse 331 | 3, // 5: hello.GrpcTestService.HelloFields:output_type -> hello.HelloFieldsResponse 332 | 4, // [4:6] is the sub-list for method output_type 333 | 2, // [2:4] is the sub-list for method input_type 334 | 2, // [2:2] is the sub-list for extension type_name 335 | 2, // [2:2] is the sub-list for extension extendee 336 | 0, // [0:2] is the sub-list for field type_name 337 | } 338 | 339 | func init() { file_hello_proto_init() } 340 | func file_hello_proto_init() { 341 | if File_hello_proto != nil { 342 | return 343 | } 344 | if !protoimpl.UnsafeEnabled { 345 | file_hello_proto_msgTypes[0].Exporter = func(v any, i int) any { 346 | switch v := v.(*HelloRequest); i { 347 | case 0: 348 | return &v.state 349 | case 1: 350 | return &v.sizeCache 351 | case 2: 352 | return &v.unknownFields 353 | default: 354 | return nil 355 | } 356 | } 357 | file_hello_proto_msgTypes[1].Exporter = func(v any, i int) any { 358 | switch v := v.(*HelloResponse); i { 359 | case 0: 360 | return &v.state 361 | case 1: 362 | return &v.sizeCache 363 | case 2: 364 | return &v.unknownFields 365 | default: 366 | return nil 367 | } 368 | } 369 | file_hello_proto_msgTypes[2].Exporter = func(v any, i int) any { 370 | switch v := v.(*HelloFieldsRequest); i { 371 | case 0: 372 | return &v.state 373 | case 1: 374 | return &v.sizeCache 375 | case 2: 376 | return &v.unknownFields 377 | default: 378 | return nil 379 | } 380 | } 381 | file_hello_proto_msgTypes[3].Exporter = func(v any, i int) any { 382 | switch v := v.(*HelloFieldsResponse); i { 383 | case 0: 384 | return &v.state 385 | case 1: 386 | return &v.sizeCache 387 | case 2: 388 | return &v.unknownFields 389 | default: 390 | return nil 391 | } 392 | } 393 | } 394 | file_hello_proto_msgTypes[1].OneofWrappers = []any{} 395 | type x struct{} 396 | out := protoimpl.TypeBuilder{ 397 | File: protoimpl.DescBuilder{ 398 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 399 | RawDescriptor: file_hello_proto_rawDesc, 400 | NumEnums: 0, 401 | NumMessages: 4, 402 | NumExtensions: 0, 403 | NumServices: 1, 404 | }, 405 | GoTypes: file_hello_proto_goTypes, 406 | DependencyIndexes: file_hello_proto_depIdxs, 407 | MessageInfos: file_hello_proto_msgTypes, 408 | }.Build() 409 | File_hello_proto = out.File 410 | file_hello_proto_rawDesc = nil 411 | file_hello_proto_goTypes = nil 412 | file_hello_proto_depIdxs = nil 413 | } 414 | -------------------------------------------------------------------------------- /testdata/hello/hello_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.4.0 4 | // - protoc v4.25.3 5 | // source: hello.proto 6 | 7 | package hello 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.62.0 or later. 19 | const _ = grpc.SupportPackageIsVersion8 20 | 21 | const ( 22 | GrpcTestService_Hello_FullMethodName = "/hello.GrpcTestService/Hello" 23 | GrpcTestService_HelloFields_FullMethodName = "/hello.GrpcTestService/HelloFields" 24 | ) 25 | 26 | // GrpcTestServiceClient is the client API for GrpcTestService service. 27 | // 28 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 29 | type GrpcTestServiceClient interface { 30 | Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) 31 | HelloFields(ctx context.Context, in *HelloFieldsRequest, opts ...grpc.CallOption) (*HelloFieldsResponse, error) 32 | } 33 | 34 | type grpcTestServiceClient struct { 35 | cc grpc.ClientConnInterface 36 | } 37 | 38 | func NewGrpcTestServiceClient(cc grpc.ClientConnInterface) GrpcTestServiceClient { 39 | return &grpcTestServiceClient{cc} 40 | } 41 | 42 | func (c *grpcTestServiceClient) Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) { 43 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 44 | out := new(HelloResponse) 45 | err := c.cc.Invoke(ctx, GrpcTestService_Hello_FullMethodName, in, out, cOpts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return out, nil 50 | } 51 | 52 | func (c *grpcTestServiceClient) HelloFields(ctx context.Context, in *HelloFieldsRequest, opts ...grpc.CallOption) (*HelloFieldsResponse, error) { 53 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 54 | out := new(HelloFieldsResponse) 55 | err := c.cc.Invoke(ctx, GrpcTestService_HelloFields_FullMethodName, in, out, cOpts...) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return out, nil 60 | } 61 | 62 | // GrpcTestServiceServer is the server API for GrpcTestService service. 63 | // All implementations must embed UnimplementedGrpcTestServiceServer 64 | // for forward compatibility 65 | type GrpcTestServiceServer interface { 66 | Hello(context.Context, *HelloRequest) (*HelloResponse, error) 67 | HelloFields(context.Context, *HelloFieldsRequest) (*HelloFieldsResponse, error) 68 | mustEmbedUnimplementedGrpcTestServiceServer() 69 | } 70 | 71 | // UnimplementedGrpcTestServiceServer must be embedded to have forward compatible implementations. 72 | type UnimplementedGrpcTestServiceServer struct { 73 | } 74 | 75 | func (UnimplementedGrpcTestServiceServer) Hello(context.Context, *HelloRequest) (*HelloResponse, error) { 76 | return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented") 77 | } 78 | func (UnimplementedGrpcTestServiceServer) HelloFields(context.Context, *HelloFieldsRequest) (*HelloFieldsResponse, error) { 79 | return nil, status.Errorf(codes.Unimplemented, "method HelloFields not implemented") 80 | } 81 | func (UnimplementedGrpcTestServiceServer) mustEmbedUnimplementedGrpcTestServiceServer() {} 82 | 83 | // UnsafeGrpcTestServiceServer may be embedded to opt out of forward compatibility for this service. 84 | // Use of this interface is not recommended, as added methods to GrpcTestServiceServer will 85 | // result in compilation errors. 86 | type UnsafeGrpcTestServiceServer interface { 87 | mustEmbedUnimplementedGrpcTestServiceServer() 88 | } 89 | 90 | func RegisterGrpcTestServiceServer(s grpc.ServiceRegistrar, srv GrpcTestServiceServer) { 91 | s.RegisterService(&GrpcTestService_ServiceDesc, srv) 92 | } 93 | 94 | func _GrpcTestService_Hello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 95 | in := new(HelloRequest) 96 | if err := dec(in); err != nil { 97 | return nil, err 98 | } 99 | if interceptor == nil { 100 | return srv.(GrpcTestServiceServer).Hello(ctx, in) 101 | } 102 | info := &grpc.UnaryServerInfo{ 103 | Server: srv, 104 | FullMethod: GrpcTestService_Hello_FullMethodName, 105 | } 106 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 107 | return srv.(GrpcTestServiceServer).Hello(ctx, req.(*HelloRequest)) 108 | } 109 | return interceptor(ctx, in, info, handler) 110 | } 111 | 112 | func _GrpcTestService_HelloFields_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 113 | in := new(HelloFieldsRequest) 114 | if err := dec(in); err != nil { 115 | return nil, err 116 | } 117 | if interceptor == nil { 118 | return srv.(GrpcTestServiceServer).HelloFields(ctx, in) 119 | } 120 | info := &grpc.UnaryServerInfo{ 121 | Server: srv, 122 | FullMethod: GrpcTestService_HelloFields_FullMethodName, 123 | } 124 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 125 | return srv.(GrpcTestServiceServer).HelloFields(ctx, req.(*HelloFieldsRequest)) 126 | } 127 | return interceptor(ctx, in, info, handler) 128 | } 129 | 130 | // GrpcTestService_ServiceDesc is the grpc.ServiceDesc for GrpcTestService service. 131 | // It's only intended for direct use with grpc.RegisterService, 132 | // and not to be introspected or modified (even as a copy) 133 | var GrpcTestService_ServiceDesc = grpc.ServiceDesc{ 134 | ServiceName: "hello.GrpcTestService", 135 | HandlerType: (*GrpcTestServiceServer)(nil), 136 | Methods: []grpc.MethodDesc{ 137 | { 138 | MethodName: "Hello", 139 | Handler: _GrpcTestService_Hello_Handler, 140 | }, 141 | { 142 | MethodName: "HelloFields", 143 | Handler: _GrpcTestService_HelloFields_Handler, 144 | }, 145 | }, 146 | Streams: []grpc.StreamDesc{}, 147 | Metadata: "hello.proto", 148 | } 149 | -------------------------------------------------------------------------------- /testdata/openssl.cnf: -------------------------------------------------------------------------------- 1 | subjectAltName=DNS:*.example.com,IP:0.0.0.0,IP:127.0.0.1 2 | -------------------------------------------------------------------------------- /testdata/request_stringer_0.golden: -------------------------------------------------------------------------------- 1 | helloworld.Greeter/SayHello 2 | baz: qux 3 | foo: bar, barbar 4 | 5 | { 6 | "name": "alice" 7 | } 8 | -------------------------------------------------------------------------------- /testdata/request_stringer_1.golden: -------------------------------------------------------------------------------- 1 | helloworld.Greeter/SayHello 2 | 3 | -------------------------------------------------------------------------------- /testdata/route_guide.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015 gRPC authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | option go_package = "google.golang.org/grpc/examples/route_guide/routeguide"; 18 | option java_multiple_files = true; 19 | option java_package = "io.grpc.examples.routeguide"; 20 | option java_outer_classname = "RouteGuideProto"; 21 | 22 | package routeguide; 23 | 24 | // Interface exported by the server. 25 | service RouteGuide { 26 | // A simple RPC. 27 | // 28 | // Obtains the feature at a given position. 29 | // 30 | // A feature with an empty name is returned if there's no feature at the given 31 | // position. 32 | rpc GetFeature(Point) returns (Feature) {} 33 | 34 | // A server-to-client streaming RPC. 35 | // 36 | // Obtains the Features available within the given Rectangle. Results are 37 | // streamed rather than returned at once (e.g. in a response message with a 38 | // repeated field), as the rectangle may cover a large area and contain a 39 | // huge number of features. 40 | rpc ListFeatures(Rectangle) returns (stream Feature) {} 41 | 42 | // A client-to-server streaming RPC. 43 | // 44 | // Accepts a stream of Points on a route being traversed, returning a 45 | // RouteSummary when traversal is completed. 46 | rpc RecordRoute(stream Point) returns (RouteSummary) {} 47 | 48 | // A Bidirectional streaming RPC. 49 | // 50 | // Accepts a stream of RouteNotes sent while a route is being traversed, 51 | // while receiving other RouteNotes (e.g. from other users). 52 | rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} 53 | } 54 | 55 | // Points are represented as latitude-longitude pairs in the E7 representation 56 | // (degrees multiplied by 10**7 and rounded to the nearest integer). 57 | // Latitudes should be in the range +/- 90 degrees and longitude should be in 58 | // the range +/- 180 degrees (inclusive). 59 | message Point { 60 | int32 latitude = 1; 61 | int32 longitude = 2; 62 | } 63 | 64 | // A latitude-longitude rectangle, represented as two diagonally opposite 65 | // points "lo" and "hi". 66 | message Rectangle { 67 | // One corner of the rectangle. 68 | Point lo = 1; 69 | 70 | // The other corner of the rectangle. 71 | Point hi = 2; 72 | } 73 | 74 | // A feature names something at a given point. 75 | // 76 | // If a feature could not be named, the name is empty. 77 | message Feature { 78 | // The name of the feature. 79 | string name = 1; 80 | 81 | // The point where the feature is detected. 82 | Point location = 2; 83 | } 84 | 85 | // A RouteNote is a message sent while at a given point. 86 | message RouteNote { 87 | // The location from which the message is sent. 88 | Point location = 1; 89 | 90 | // The message to be sent. 91 | string message = 2; 92 | } 93 | 94 | // A RouteSummary is received in response to a RecordRoute rpc. 95 | // 96 | // It contains the number of individual points received, the number of 97 | // detected features, and the total distance covered as the cumulative sum of 98 | // the distance between each point. 99 | message RouteSummary { 100 | // The number of points received. 101 | int32 point_count = 1; 102 | 103 | // The number of known features passed while traversing the route. 104 | int32 feature_count = 2; 105 | 106 | // The distance covered in metres. 107 | int32 distance = 3; 108 | 109 | // The duration of the traversal in seconds. 110 | int32 elapsed_time = 4; 111 | } 112 | -------------------------------------------------------------------------------- /testdata/routeguide/route_guide.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 gRPC authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by protoc-gen-go. DO NOT EDIT. 16 | // versions: 17 | // protoc-gen-go v1.34.2 18 | // protoc v4.25.3 19 | // source: route_guide.proto 20 | 21 | package routeguide 22 | 23 | import ( 24 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 25 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 26 | reflect "reflect" 27 | sync "sync" 28 | ) 29 | 30 | const ( 31 | // Verify that this generated code is sufficiently up-to-date. 32 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 33 | // Verify that runtime/protoimpl is sufficiently up-to-date. 34 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 35 | ) 36 | 37 | // Points are represented as latitude-longitude pairs in the E7 representation 38 | // (degrees multiplied by 10**7 and rounded to the nearest integer). 39 | // Latitudes should be in the range +/- 90 degrees and longitude should be in 40 | // the range +/- 180 degrees (inclusive). 41 | type Point struct { 42 | state protoimpl.MessageState 43 | sizeCache protoimpl.SizeCache 44 | unknownFields protoimpl.UnknownFields 45 | 46 | Latitude int32 `protobuf:"varint,1,opt,name=latitude,proto3" json:"latitude,omitempty"` 47 | Longitude int32 `protobuf:"varint,2,opt,name=longitude,proto3" json:"longitude,omitempty"` 48 | } 49 | 50 | func (x *Point) Reset() { 51 | *x = Point{} 52 | if protoimpl.UnsafeEnabled { 53 | mi := &file_route_guide_proto_msgTypes[0] 54 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 55 | ms.StoreMessageInfo(mi) 56 | } 57 | } 58 | 59 | func (x *Point) String() string { 60 | return protoimpl.X.MessageStringOf(x) 61 | } 62 | 63 | func (*Point) ProtoMessage() {} 64 | 65 | func (x *Point) ProtoReflect() protoreflect.Message { 66 | mi := &file_route_guide_proto_msgTypes[0] 67 | if protoimpl.UnsafeEnabled && x != nil { 68 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 69 | if ms.LoadMessageInfo() == nil { 70 | ms.StoreMessageInfo(mi) 71 | } 72 | return ms 73 | } 74 | return mi.MessageOf(x) 75 | } 76 | 77 | // Deprecated: Use Point.ProtoReflect.Descriptor instead. 78 | func (*Point) Descriptor() ([]byte, []int) { 79 | return file_route_guide_proto_rawDescGZIP(), []int{0} 80 | } 81 | 82 | func (x *Point) GetLatitude() int32 { 83 | if x != nil { 84 | return x.Latitude 85 | } 86 | return 0 87 | } 88 | 89 | func (x *Point) GetLongitude() int32 { 90 | if x != nil { 91 | return x.Longitude 92 | } 93 | return 0 94 | } 95 | 96 | // A latitude-longitude rectangle, represented as two diagonally opposite 97 | // points "lo" and "hi". 98 | type Rectangle struct { 99 | state protoimpl.MessageState 100 | sizeCache protoimpl.SizeCache 101 | unknownFields protoimpl.UnknownFields 102 | 103 | // One corner of the rectangle. 104 | Lo *Point `protobuf:"bytes,1,opt,name=lo,proto3" json:"lo,omitempty"` 105 | // The other corner of the rectangle. 106 | Hi *Point `protobuf:"bytes,2,opt,name=hi,proto3" json:"hi,omitempty"` 107 | } 108 | 109 | func (x *Rectangle) Reset() { 110 | *x = Rectangle{} 111 | if protoimpl.UnsafeEnabled { 112 | mi := &file_route_guide_proto_msgTypes[1] 113 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 114 | ms.StoreMessageInfo(mi) 115 | } 116 | } 117 | 118 | func (x *Rectangle) String() string { 119 | return protoimpl.X.MessageStringOf(x) 120 | } 121 | 122 | func (*Rectangle) ProtoMessage() {} 123 | 124 | func (x *Rectangle) ProtoReflect() protoreflect.Message { 125 | mi := &file_route_guide_proto_msgTypes[1] 126 | if protoimpl.UnsafeEnabled && x != nil { 127 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 128 | if ms.LoadMessageInfo() == nil { 129 | ms.StoreMessageInfo(mi) 130 | } 131 | return ms 132 | } 133 | return mi.MessageOf(x) 134 | } 135 | 136 | // Deprecated: Use Rectangle.ProtoReflect.Descriptor instead. 137 | func (*Rectangle) Descriptor() ([]byte, []int) { 138 | return file_route_guide_proto_rawDescGZIP(), []int{1} 139 | } 140 | 141 | func (x *Rectangle) GetLo() *Point { 142 | if x != nil { 143 | return x.Lo 144 | } 145 | return nil 146 | } 147 | 148 | func (x *Rectangle) GetHi() *Point { 149 | if x != nil { 150 | return x.Hi 151 | } 152 | return nil 153 | } 154 | 155 | // A feature names something at a given point. 156 | // 157 | // If a feature could not be named, the name is empty. 158 | type Feature struct { 159 | state protoimpl.MessageState 160 | sizeCache protoimpl.SizeCache 161 | unknownFields protoimpl.UnknownFields 162 | 163 | // The name of the feature. 164 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 165 | // The point where the feature is detected. 166 | Location *Point `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` 167 | } 168 | 169 | func (x *Feature) Reset() { 170 | *x = Feature{} 171 | if protoimpl.UnsafeEnabled { 172 | mi := &file_route_guide_proto_msgTypes[2] 173 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 174 | ms.StoreMessageInfo(mi) 175 | } 176 | } 177 | 178 | func (x *Feature) String() string { 179 | return protoimpl.X.MessageStringOf(x) 180 | } 181 | 182 | func (*Feature) ProtoMessage() {} 183 | 184 | func (x *Feature) ProtoReflect() protoreflect.Message { 185 | mi := &file_route_guide_proto_msgTypes[2] 186 | if protoimpl.UnsafeEnabled && x != nil { 187 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 188 | if ms.LoadMessageInfo() == nil { 189 | ms.StoreMessageInfo(mi) 190 | } 191 | return ms 192 | } 193 | return mi.MessageOf(x) 194 | } 195 | 196 | // Deprecated: Use Feature.ProtoReflect.Descriptor instead. 197 | func (*Feature) Descriptor() ([]byte, []int) { 198 | return file_route_guide_proto_rawDescGZIP(), []int{2} 199 | } 200 | 201 | func (x *Feature) GetName() string { 202 | if x != nil { 203 | return x.Name 204 | } 205 | return "" 206 | } 207 | 208 | func (x *Feature) GetLocation() *Point { 209 | if x != nil { 210 | return x.Location 211 | } 212 | return nil 213 | } 214 | 215 | // A RouteNote is a message sent while at a given point. 216 | type RouteNote struct { 217 | state protoimpl.MessageState 218 | sizeCache protoimpl.SizeCache 219 | unknownFields protoimpl.UnknownFields 220 | 221 | // The location from which the message is sent. 222 | Location *Point `protobuf:"bytes,1,opt,name=location,proto3" json:"location,omitempty"` 223 | // The message to be sent. 224 | Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` 225 | } 226 | 227 | func (x *RouteNote) Reset() { 228 | *x = RouteNote{} 229 | if protoimpl.UnsafeEnabled { 230 | mi := &file_route_guide_proto_msgTypes[3] 231 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 232 | ms.StoreMessageInfo(mi) 233 | } 234 | } 235 | 236 | func (x *RouteNote) String() string { 237 | return protoimpl.X.MessageStringOf(x) 238 | } 239 | 240 | func (*RouteNote) ProtoMessage() {} 241 | 242 | func (x *RouteNote) ProtoReflect() protoreflect.Message { 243 | mi := &file_route_guide_proto_msgTypes[3] 244 | if protoimpl.UnsafeEnabled && x != nil { 245 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 246 | if ms.LoadMessageInfo() == nil { 247 | ms.StoreMessageInfo(mi) 248 | } 249 | return ms 250 | } 251 | return mi.MessageOf(x) 252 | } 253 | 254 | // Deprecated: Use RouteNote.ProtoReflect.Descriptor instead. 255 | func (*RouteNote) Descriptor() ([]byte, []int) { 256 | return file_route_guide_proto_rawDescGZIP(), []int{3} 257 | } 258 | 259 | func (x *RouteNote) GetLocation() *Point { 260 | if x != nil { 261 | return x.Location 262 | } 263 | return nil 264 | } 265 | 266 | func (x *RouteNote) GetMessage() string { 267 | if x != nil { 268 | return x.Message 269 | } 270 | return "" 271 | } 272 | 273 | // A RouteSummary is received in response to a RecordRoute rpc. 274 | // 275 | // It contains the number of individual points received, the number of 276 | // detected features, and the total distance covered as the cumulative sum of 277 | // the distance between each point. 278 | type RouteSummary struct { 279 | state protoimpl.MessageState 280 | sizeCache protoimpl.SizeCache 281 | unknownFields protoimpl.UnknownFields 282 | 283 | // The number of points received. 284 | PointCount int32 `protobuf:"varint,1,opt,name=point_count,json=pointCount,proto3" json:"point_count,omitempty"` 285 | // The number of known features passed while traversing the route. 286 | FeatureCount int32 `protobuf:"varint,2,opt,name=feature_count,json=featureCount,proto3" json:"feature_count,omitempty"` 287 | // The distance covered in metres. 288 | Distance int32 `protobuf:"varint,3,opt,name=distance,proto3" json:"distance,omitempty"` 289 | // The duration of the traversal in seconds. 290 | ElapsedTime int32 `protobuf:"varint,4,opt,name=elapsed_time,json=elapsedTime,proto3" json:"elapsed_time,omitempty"` 291 | } 292 | 293 | func (x *RouteSummary) Reset() { 294 | *x = RouteSummary{} 295 | if protoimpl.UnsafeEnabled { 296 | mi := &file_route_guide_proto_msgTypes[4] 297 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 298 | ms.StoreMessageInfo(mi) 299 | } 300 | } 301 | 302 | func (x *RouteSummary) String() string { 303 | return protoimpl.X.MessageStringOf(x) 304 | } 305 | 306 | func (*RouteSummary) ProtoMessage() {} 307 | 308 | func (x *RouteSummary) ProtoReflect() protoreflect.Message { 309 | mi := &file_route_guide_proto_msgTypes[4] 310 | if protoimpl.UnsafeEnabled && x != nil { 311 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 312 | if ms.LoadMessageInfo() == nil { 313 | ms.StoreMessageInfo(mi) 314 | } 315 | return ms 316 | } 317 | return mi.MessageOf(x) 318 | } 319 | 320 | // Deprecated: Use RouteSummary.ProtoReflect.Descriptor instead. 321 | func (*RouteSummary) Descriptor() ([]byte, []int) { 322 | return file_route_guide_proto_rawDescGZIP(), []int{4} 323 | } 324 | 325 | func (x *RouteSummary) GetPointCount() int32 { 326 | if x != nil { 327 | return x.PointCount 328 | } 329 | return 0 330 | } 331 | 332 | func (x *RouteSummary) GetFeatureCount() int32 { 333 | if x != nil { 334 | return x.FeatureCount 335 | } 336 | return 0 337 | } 338 | 339 | func (x *RouteSummary) GetDistance() int32 { 340 | if x != nil { 341 | return x.Distance 342 | } 343 | return 0 344 | } 345 | 346 | func (x *RouteSummary) GetElapsedTime() int32 { 347 | if x != nil { 348 | return x.ElapsedTime 349 | } 350 | return 0 351 | } 352 | 353 | var File_route_guide_proto protoreflect.FileDescriptor 354 | 355 | var file_route_guide_proto_rawDesc = []byte{ 356 | 0x0a, 0x11, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x5f, 0x67, 0x75, 0x69, 0x64, 0x65, 0x2e, 0x70, 0x72, 357 | 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 0x22, 358 | 0x41, 0x0a, 0x05, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x74, 0x69, 359 | 0x74, 0x75, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x6c, 0x61, 0x74, 0x69, 360 | 0x74, 0x75, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 361 | 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x6c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 362 | 0x64, 0x65, 0x22, 0x51, 0x0a, 0x09, 0x52, 0x65, 0x63, 0x74, 0x61, 0x6e, 0x67, 0x6c, 0x65, 0x12, 363 | 0x21, 0x0a, 0x02, 0x6c, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, 0x6f, 364 | 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 0x2e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x02, 365 | 0x6c, 0x6f, 0x12, 0x21, 0x0a, 0x02, 0x68, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 366 | 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 0x2e, 0x50, 0x6f, 0x69, 0x6e, 367 | 0x74, 0x52, 0x02, 0x68, 0x69, 0x22, 0x4c, 0x0a, 0x07, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 368 | 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 369 | 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 370 | 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 371 | 0x69, 0x64, 0x65, 0x2e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 372 | 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x09, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 373 | 0x12, 0x2d, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 374 | 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 0x2e, 375 | 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 376 | 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 377 | 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x93, 0x01, 0x0a, 0x0c, 0x52, 0x6f, 378 | 0x75, 0x74, 0x65, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x6f, 379 | 0x69, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 380 | 0x0a, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x66, 381 | 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 382 | 0x28, 0x05, 0x52, 0x0c, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 383 | 0x12, 0x1a, 0x0a, 0x08, 0x64, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 384 | 0x28, 0x05, 0x52, 0x08, 0x64, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x21, 0x0a, 0x0c, 385 | 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 386 | 0x28, 0x05, 0x52, 0x0b, 0x65, 0x6c, 0x61, 0x70, 0x73, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x32, 387 | 0x85, 0x02, 0x0a, 0x0a, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x47, 0x75, 0x69, 0x64, 0x65, 0x12, 0x36, 388 | 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x11, 0x2e, 0x72, 389 | 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 0x2e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x1a, 390 | 0x13, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 0x2e, 0x46, 0x65, 0x61, 391 | 0x74, 0x75, 0x72, 0x65, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x65, 392 | 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x15, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 393 | 0x69, 0x64, 0x65, 0x2e, 0x52, 0x65, 0x63, 0x74, 0x61, 0x6e, 0x67, 0x6c, 0x65, 0x1a, 0x13, 0x2e, 394 | 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 395 | 0x72, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x3e, 0x0a, 0x0b, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 396 | 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x11, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 397 | 0x64, 0x65, 0x2e, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 398 | 0x67, 0x75, 0x69, 0x64, 0x65, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x53, 0x75, 0x6d, 0x6d, 0x61, 399 | 0x72, 0x79, 0x22, 0x00, 0x28, 0x01, 0x12, 0x3f, 0x0a, 0x09, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x43, 400 | 0x68, 0x61, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 401 | 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x1a, 0x15, 0x2e, 0x72, 0x6f, 0x75, 402 | 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x4e, 0x6f, 0x74, 403 | 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x68, 0x0a, 0x1b, 0x69, 0x6f, 0x2e, 0x67, 0x72, 404 | 0x70, 0x63, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x72, 0x6f, 0x75, 0x74, 405 | 0x65, 0x67, 0x75, 0x69, 0x64, 0x65, 0x42, 0x0f, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x47, 0x75, 0x69, 406 | 0x64, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x36, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 407 | 0x65, 0x2e, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x67, 0x72, 0x70, 408 | 0x63, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 409 | 0x5f, 0x67, 0x75, 0x69, 0x64, 0x65, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x67, 0x75, 0x69, 0x64, 410 | 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 411 | } 412 | 413 | var ( 414 | file_route_guide_proto_rawDescOnce sync.Once 415 | file_route_guide_proto_rawDescData = file_route_guide_proto_rawDesc 416 | ) 417 | 418 | func file_route_guide_proto_rawDescGZIP() []byte { 419 | file_route_guide_proto_rawDescOnce.Do(func() { 420 | file_route_guide_proto_rawDescData = protoimpl.X.CompressGZIP(file_route_guide_proto_rawDescData) 421 | }) 422 | return file_route_guide_proto_rawDescData 423 | } 424 | 425 | var file_route_guide_proto_msgTypes = make([]protoimpl.MessageInfo, 5) 426 | var file_route_guide_proto_goTypes = []any{ 427 | (*Point)(nil), // 0: routeguide.Point 428 | (*Rectangle)(nil), // 1: routeguide.Rectangle 429 | (*Feature)(nil), // 2: routeguide.Feature 430 | (*RouteNote)(nil), // 3: routeguide.RouteNote 431 | (*RouteSummary)(nil), // 4: routeguide.RouteSummary 432 | } 433 | var file_route_guide_proto_depIdxs = []int32{ 434 | 0, // 0: routeguide.Rectangle.lo:type_name -> routeguide.Point 435 | 0, // 1: routeguide.Rectangle.hi:type_name -> routeguide.Point 436 | 0, // 2: routeguide.Feature.location:type_name -> routeguide.Point 437 | 0, // 3: routeguide.RouteNote.location:type_name -> routeguide.Point 438 | 0, // 4: routeguide.RouteGuide.GetFeature:input_type -> routeguide.Point 439 | 1, // 5: routeguide.RouteGuide.ListFeatures:input_type -> routeguide.Rectangle 440 | 0, // 6: routeguide.RouteGuide.RecordRoute:input_type -> routeguide.Point 441 | 3, // 7: routeguide.RouteGuide.RouteChat:input_type -> routeguide.RouteNote 442 | 2, // 8: routeguide.RouteGuide.GetFeature:output_type -> routeguide.Feature 443 | 2, // 9: routeguide.RouteGuide.ListFeatures:output_type -> routeguide.Feature 444 | 4, // 10: routeguide.RouteGuide.RecordRoute:output_type -> routeguide.RouteSummary 445 | 3, // 11: routeguide.RouteGuide.RouteChat:output_type -> routeguide.RouteNote 446 | 8, // [8:12] is the sub-list for method output_type 447 | 4, // [4:8] is the sub-list for method input_type 448 | 4, // [4:4] is the sub-list for extension type_name 449 | 4, // [4:4] is the sub-list for extension extendee 450 | 0, // [0:4] is the sub-list for field type_name 451 | } 452 | 453 | func init() { file_route_guide_proto_init() } 454 | func file_route_guide_proto_init() { 455 | if File_route_guide_proto != nil { 456 | return 457 | } 458 | if !protoimpl.UnsafeEnabled { 459 | file_route_guide_proto_msgTypes[0].Exporter = func(v any, i int) any { 460 | switch v := v.(*Point); i { 461 | case 0: 462 | return &v.state 463 | case 1: 464 | return &v.sizeCache 465 | case 2: 466 | return &v.unknownFields 467 | default: 468 | return nil 469 | } 470 | } 471 | file_route_guide_proto_msgTypes[1].Exporter = func(v any, i int) any { 472 | switch v := v.(*Rectangle); i { 473 | case 0: 474 | return &v.state 475 | case 1: 476 | return &v.sizeCache 477 | case 2: 478 | return &v.unknownFields 479 | default: 480 | return nil 481 | } 482 | } 483 | file_route_guide_proto_msgTypes[2].Exporter = func(v any, i int) any { 484 | switch v := v.(*Feature); i { 485 | case 0: 486 | return &v.state 487 | case 1: 488 | return &v.sizeCache 489 | case 2: 490 | return &v.unknownFields 491 | default: 492 | return nil 493 | } 494 | } 495 | file_route_guide_proto_msgTypes[3].Exporter = func(v any, i int) any { 496 | switch v := v.(*RouteNote); i { 497 | case 0: 498 | return &v.state 499 | case 1: 500 | return &v.sizeCache 501 | case 2: 502 | return &v.unknownFields 503 | default: 504 | return nil 505 | } 506 | } 507 | file_route_guide_proto_msgTypes[4].Exporter = func(v any, i int) any { 508 | switch v := v.(*RouteSummary); i { 509 | case 0: 510 | return &v.state 511 | case 1: 512 | return &v.sizeCache 513 | case 2: 514 | return &v.unknownFields 515 | default: 516 | return nil 517 | } 518 | } 519 | } 520 | type x struct{} 521 | out := protoimpl.TypeBuilder{ 522 | File: protoimpl.DescBuilder{ 523 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 524 | RawDescriptor: file_route_guide_proto_rawDesc, 525 | NumEnums: 0, 526 | NumMessages: 5, 527 | NumExtensions: 0, 528 | NumServices: 1, 529 | }, 530 | GoTypes: file_route_guide_proto_goTypes, 531 | DependencyIndexes: file_route_guide_proto_depIdxs, 532 | MessageInfos: file_route_guide_proto_msgTypes, 533 | }.Build() 534 | File_route_guide_proto = out.File 535 | file_route_guide_proto_rawDesc = nil 536 | file_route_guide_proto_goTypes = nil 537 | file_route_guide_proto_depIdxs = nil 538 | } 539 | -------------------------------------------------------------------------------- /testdata/routeguide/route_guide_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 gRPC authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 16 | // versions: 17 | // - protoc-gen-go-grpc v1.4.0 18 | // - protoc v4.25.3 19 | // source: route_guide.proto 20 | 21 | package routeguide 22 | 23 | import ( 24 | context "context" 25 | grpc "google.golang.org/grpc" 26 | codes "google.golang.org/grpc/codes" 27 | status "google.golang.org/grpc/status" 28 | ) 29 | 30 | // This is a compile-time assertion to ensure that this generated file 31 | // is compatible with the grpc package it is being compiled against. 32 | // Requires gRPC-Go v1.62.0 or later. 33 | const _ = grpc.SupportPackageIsVersion8 34 | 35 | const ( 36 | RouteGuide_GetFeature_FullMethodName = "/routeguide.RouteGuide/GetFeature" 37 | RouteGuide_ListFeatures_FullMethodName = "/routeguide.RouteGuide/ListFeatures" 38 | RouteGuide_RecordRoute_FullMethodName = "/routeguide.RouteGuide/RecordRoute" 39 | RouteGuide_RouteChat_FullMethodName = "/routeguide.RouteGuide/RouteChat" 40 | ) 41 | 42 | // RouteGuideClient is the client API for RouteGuide service. 43 | // 44 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 45 | // 46 | // Interface exported by the server. 47 | type RouteGuideClient interface { 48 | // A simple RPC. 49 | // 50 | // Obtains the feature at a given position. 51 | // 52 | // A feature with an empty name is returned if there's no feature at the given 53 | // position. 54 | GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) 55 | // A server-to-client streaming RPC. 56 | // 57 | // Obtains the Features available within the given Rectangle. Results are 58 | // streamed rather than returned at once (e.g. in a response message with a 59 | // repeated field), as the rectangle may cover a large area and contain a 60 | // huge number of features. 61 | ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error) 62 | // A client-to-server streaming RPC. 63 | // 64 | // Accepts a stream of Points on a route being traversed, returning a 65 | // RouteSummary when traversal is completed. 66 | RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error) 67 | // A Bidirectional streaming RPC. 68 | // 69 | // Accepts a stream of RouteNotes sent while a route is being traversed, 70 | // while receiving other RouteNotes (e.g. from other users). 71 | RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error) 72 | } 73 | 74 | type routeGuideClient struct { 75 | cc grpc.ClientConnInterface 76 | } 77 | 78 | func NewRouteGuideClient(cc grpc.ClientConnInterface) RouteGuideClient { 79 | return &routeGuideClient{cc} 80 | } 81 | 82 | func (c *routeGuideClient) GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) { 83 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 84 | out := new(Feature) 85 | err := c.cc.Invoke(ctx, RouteGuide_GetFeature_FullMethodName, in, out, cOpts...) 86 | if err != nil { 87 | return nil, err 88 | } 89 | return out, nil 90 | } 91 | 92 | func (c *routeGuideClient) ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error) { 93 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 94 | stream, err := c.cc.NewStream(ctx, &RouteGuide_ServiceDesc.Streams[0], RouteGuide_ListFeatures_FullMethodName, cOpts...) 95 | if err != nil { 96 | return nil, err 97 | } 98 | x := &routeGuideListFeaturesClient{ClientStream: stream} 99 | if err := x.ClientStream.SendMsg(in); err != nil { 100 | return nil, err 101 | } 102 | if err := x.ClientStream.CloseSend(); err != nil { 103 | return nil, err 104 | } 105 | return x, nil 106 | } 107 | 108 | type RouteGuide_ListFeaturesClient interface { 109 | Recv() (*Feature, error) 110 | grpc.ClientStream 111 | } 112 | 113 | type routeGuideListFeaturesClient struct { 114 | grpc.ClientStream 115 | } 116 | 117 | func (x *routeGuideListFeaturesClient) Recv() (*Feature, error) { 118 | m := new(Feature) 119 | if err := x.ClientStream.RecvMsg(m); err != nil { 120 | return nil, err 121 | } 122 | return m, nil 123 | } 124 | 125 | func (c *routeGuideClient) RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error) { 126 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 127 | stream, err := c.cc.NewStream(ctx, &RouteGuide_ServiceDesc.Streams[1], RouteGuide_RecordRoute_FullMethodName, cOpts...) 128 | if err != nil { 129 | return nil, err 130 | } 131 | x := &routeGuideRecordRouteClient{ClientStream: stream} 132 | return x, nil 133 | } 134 | 135 | type RouteGuide_RecordRouteClient interface { 136 | Send(*Point) error 137 | CloseAndRecv() (*RouteSummary, error) 138 | grpc.ClientStream 139 | } 140 | 141 | type routeGuideRecordRouteClient struct { 142 | grpc.ClientStream 143 | } 144 | 145 | func (x *routeGuideRecordRouteClient) Send(m *Point) error { 146 | return x.ClientStream.SendMsg(m) 147 | } 148 | 149 | func (x *routeGuideRecordRouteClient) CloseAndRecv() (*RouteSummary, error) { 150 | if err := x.ClientStream.CloseSend(); err != nil { 151 | return nil, err 152 | } 153 | m := new(RouteSummary) 154 | if err := x.ClientStream.RecvMsg(m); err != nil { 155 | return nil, err 156 | } 157 | return m, nil 158 | } 159 | 160 | func (c *routeGuideClient) RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error) { 161 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 162 | stream, err := c.cc.NewStream(ctx, &RouteGuide_ServiceDesc.Streams[2], RouteGuide_RouteChat_FullMethodName, cOpts...) 163 | if err != nil { 164 | return nil, err 165 | } 166 | x := &routeGuideRouteChatClient{ClientStream: stream} 167 | return x, nil 168 | } 169 | 170 | type RouteGuide_RouteChatClient interface { 171 | Send(*RouteNote) error 172 | Recv() (*RouteNote, error) 173 | grpc.ClientStream 174 | } 175 | 176 | type routeGuideRouteChatClient struct { 177 | grpc.ClientStream 178 | } 179 | 180 | func (x *routeGuideRouteChatClient) Send(m *RouteNote) error { 181 | return x.ClientStream.SendMsg(m) 182 | } 183 | 184 | func (x *routeGuideRouteChatClient) Recv() (*RouteNote, error) { 185 | m := new(RouteNote) 186 | if err := x.ClientStream.RecvMsg(m); err != nil { 187 | return nil, err 188 | } 189 | return m, nil 190 | } 191 | 192 | // RouteGuideServer is the server API for RouteGuide service. 193 | // All implementations must embed UnimplementedRouteGuideServer 194 | // for forward compatibility 195 | // 196 | // Interface exported by the server. 197 | type RouteGuideServer interface { 198 | // A simple RPC. 199 | // 200 | // Obtains the feature at a given position. 201 | // 202 | // A feature with an empty name is returned if there's no feature at the given 203 | // position. 204 | GetFeature(context.Context, *Point) (*Feature, error) 205 | // A server-to-client streaming RPC. 206 | // 207 | // Obtains the Features available within the given Rectangle. Results are 208 | // streamed rather than returned at once (e.g. in a response message with a 209 | // repeated field), as the rectangle may cover a large area and contain a 210 | // huge number of features. 211 | ListFeatures(*Rectangle, RouteGuide_ListFeaturesServer) error 212 | // A client-to-server streaming RPC. 213 | // 214 | // Accepts a stream of Points on a route being traversed, returning a 215 | // RouteSummary when traversal is completed. 216 | RecordRoute(RouteGuide_RecordRouteServer) error 217 | // A Bidirectional streaming RPC. 218 | // 219 | // Accepts a stream of RouteNotes sent while a route is being traversed, 220 | // while receiving other RouteNotes (e.g. from other users). 221 | RouteChat(RouteGuide_RouteChatServer) error 222 | mustEmbedUnimplementedRouteGuideServer() 223 | } 224 | 225 | // UnimplementedRouteGuideServer must be embedded to have forward compatible implementations. 226 | type UnimplementedRouteGuideServer struct { 227 | } 228 | 229 | func (UnimplementedRouteGuideServer) GetFeature(context.Context, *Point) (*Feature, error) { 230 | return nil, status.Errorf(codes.Unimplemented, "method GetFeature not implemented") 231 | } 232 | func (UnimplementedRouteGuideServer) ListFeatures(*Rectangle, RouteGuide_ListFeaturesServer) error { 233 | return status.Errorf(codes.Unimplemented, "method ListFeatures not implemented") 234 | } 235 | func (UnimplementedRouteGuideServer) RecordRoute(RouteGuide_RecordRouteServer) error { 236 | return status.Errorf(codes.Unimplemented, "method RecordRoute not implemented") 237 | } 238 | func (UnimplementedRouteGuideServer) RouteChat(RouteGuide_RouteChatServer) error { 239 | return status.Errorf(codes.Unimplemented, "method RouteChat not implemented") 240 | } 241 | func (UnimplementedRouteGuideServer) mustEmbedUnimplementedRouteGuideServer() {} 242 | 243 | // UnsafeRouteGuideServer may be embedded to opt out of forward compatibility for this service. 244 | // Use of this interface is not recommended, as added methods to RouteGuideServer will 245 | // result in compilation errors. 246 | type UnsafeRouteGuideServer interface { 247 | mustEmbedUnimplementedRouteGuideServer() 248 | } 249 | 250 | func RegisterRouteGuideServer(s grpc.ServiceRegistrar, srv RouteGuideServer) { 251 | s.RegisterService(&RouteGuide_ServiceDesc, srv) 252 | } 253 | 254 | func _RouteGuide_GetFeature_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 255 | in := new(Point) 256 | if err := dec(in); err != nil { 257 | return nil, err 258 | } 259 | if interceptor == nil { 260 | return srv.(RouteGuideServer).GetFeature(ctx, in) 261 | } 262 | info := &grpc.UnaryServerInfo{ 263 | Server: srv, 264 | FullMethod: RouteGuide_GetFeature_FullMethodName, 265 | } 266 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 267 | return srv.(RouteGuideServer).GetFeature(ctx, req.(*Point)) 268 | } 269 | return interceptor(ctx, in, info, handler) 270 | } 271 | 272 | func _RouteGuide_ListFeatures_Handler(srv interface{}, stream grpc.ServerStream) error { 273 | m := new(Rectangle) 274 | if err := stream.RecvMsg(m); err != nil { 275 | return err 276 | } 277 | return srv.(RouteGuideServer).ListFeatures(m, &routeGuideListFeaturesServer{ServerStream: stream}) 278 | } 279 | 280 | type RouteGuide_ListFeaturesServer interface { 281 | Send(*Feature) error 282 | grpc.ServerStream 283 | } 284 | 285 | type routeGuideListFeaturesServer struct { 286 | grpc.ServerStream 287 | } 288 | 289 | func (x *routeGuideListFeaturesServer) Send(m *Feature) error { 290 | return x.ServerStream.SendMsg(m) 291 | } 292 | 293 | func _RouteGuide_RecordRoute_Handler(srv interface{}, stream grpc.ServerStream) error { 294 | return srv.(RouteGuideServer).RecordRoute(&routeGuideRecordRouteServer{ServerStream: stream}) 295 | } 296 | 297 | type RouteGuide_RecordRouteServer interface { 298 | SendAndClose(*RouteSummary) error 299 | Recv() (*Point, error) 300 | grpc.ServerStream 301 | } 302 | 303 | type routeGuideRecordRouteServer struct { 304 | grpc.ServerStream 305 | } 306 | 307 | func (x *routeGuideRecordRouteServer) SendAndClose(m *RouteSummary) error { 308 | return x.ServerStream.SendMsg(m) 309 | } 310 | 311 | func (x *routeGuideRecordRouteServer) Recv() (*Point, error) { 312 | m := new(Point) 313 | if err := x.ServerStream.RecvMsg(m); err != nil { 314 | return nil, err 315 | } 316 | return m, nil 317 | } 318 | 319 | func _RouteGuide_RouteChat_Handler(srv interface{}, stream grpc.ServerStream) error { 320 | return srv.(RouteGuideServer).RouteChat(&routeGuideRouteChatServer{ServerStream: stream}) 321 | } 322 | 323 | type RouteGuide_RouteChatServer interface { 324 | Send(*RouteNote) error 325 | Recv() (*RouteNote, error) 326 | grpc.ServerStream 327 | } 328 | 329 | type routeGuideRouteChatServer struct { 330 | grpc.ServerStream 331 | } 332 | 333 | func (x *routeGuideRouteChatServer) Send(m *RouteNote) error { 334 | return x.ServerStream.SendMsg(m) 335 | } 336 | 337 | func (x *routeGuideRouteChatServer) Recv() (*RouteNote, error) { 338 | m := new(RouteNote) 339 | if err := x.ServerStream.RecvMsg(m); err != nil { 340 | return nil, err 341 | } 342 | return m, nil 343 | } 344 | 345 | // RouteGuide_ServiceDesc is the grpc.ServiceDesc for RouteGuide service. 346 | // It's only intended for direct use with grpc.RegisterService, 347 | // and not to be introspected or modified (even as a copy) 348 | var RouteGuide_ServiceDesc = grpc.ServiceDesc{ 349 | ServiceName: "routeguide.RouteGuide", 350 | HandlerType: (*RouteGuideServer)(nil), 351 | Methods: []grpc.MethodDesc{ 352 | { 353 | MethodName: "GetFeature", 354 | Handler: _RouteGuide_GetFeature_Handler, 355 | }, 356 | }, 357 | Streams: []grpc.StreamDesc{ 358 | { 359 | StreamName: "ListFeatures", 360 | Handler: _RouteGuide_ListFeatures_Handler, 361 | ServerStreams: true, 362 | }, 363 | { 364 | StreamName: "RecordRoute", 365 | Handler: _RouteGuide_RecordRoute_Handler, 366 | ClientStreams: true, 367 | }, 368 | { 369 | StreamName: "RouteChat", 370 | Handler: _RouteGuide_RouteChat_Handler, 371 | ServerStreams: true, 372 | ClientStreams: true, 373 | }, 374 | }, 375 | Metadata: "route_guide.proto", 376 | } 377 | -------------------------------------------------------------------------------- /unary_test.go: -------------------------------------------------------------------------------- 1 | package grpcstub 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/k1LoW/grpcstub/testdata/routeguide" 8 | ) 9 | 10 | func TestUnary(t *testing.T) { 11 | ctx := context.Background() 12 | ts := NewServer(t, "testdata/route_guide.proto") 13 | t.Cleanup(func() { 14 | ts.Close() 15 | }) 16 | ts.Method("GetFeature").Response(map[string]any{"name": "hello", "location": map[string]any{"latitude": 10, "longitude": 13}}) 17 | ts.Method("GetFeature").Response(map[string]any{"name": "hello", "location": map[string]any{"latitude": 99, "longitude": 99}}) 18 | 19 | client := routeguide.NewRouteGuideClient(ts.Conn()) 20 | res, err := client.GetFeature(ctx, &routeguide.Point{ 21 | Latitude: 10, 22 | Longitude: 13, 23 | }) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | { 28 | got := res.Name 29 | if want := "hello"; got != want { 30 | t.Errorf("got %v\nwant %v", got, want) 31 | return 32 | } 33 | } 34 | { 35 | got := res.Location.Latitude 36 | if want := int32(10); got != want { 37 | t.Errorf("got %v\nwant %v", got, want) 38 | } 39 | } 40 | 41 | { 42 | got := len(ts.Requests()) 43 | if want := 1; got != want { 44 | t.Errorf("got %v\nwant %v", got, want) 45 | return 46 | } 47 | } 48 | 49 | req := ts.Requests()[0] 50 | { 51 | got := int32(req.Message["longitude"].(float64)) 52 | if want := int32(13); got != want { 53 | t.Errorf("got %v\nwant %v", got, want) 54 | } 55 | } 56 | } 57 | 58 | func TestUnaryUnmatched(t *testing.T) { 59 | ctx := context.Background() 60 | ts := NewServer(t, "testdata/route_guide.proto") 61 | t.Cleanup(func() { 62 | ts.Close() 63 | }) 64 | ts.Method("GetFeature").Match(func(req *Request) bool { 65 | return false 66 | }).Response(map[string]any{"name": "hello", "location": map[string]any{"latitude": 10, "longitude": 13}}) 67 | 68 | client := routeguide.NewRouteGuideClient(ts.Conn()) 69 | _, err := client.GetFeature(ctx, &routeguide.Point{ 70 | Latitude: 10, 71 | Longitude: 13, 72 | }) 73 | if err == nil { 74 | t.Error("want error") 75 | } 76 | 77 | { 78 | got := len(ts.Requests()) 79 | if want := 0; got != want { 80 | t.Errorf("got %v\nwant %v", got, want) 81 | } 82 | } 83 | { 84 | got := len(ts.UnmatchedRequests()) 85 | if want := 1; got != want { 86 | t.Errorf("got %v\nwant %v", got, want) 87 | } 88 | } 89 | } 90 | --------------------------------------------------------------------------------