├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── Taskfile.yaml ├── api ├── buf.lock ├── buf.yaml ├── example │ ├── constructsserver │ │ ├── constructs.pb.go │ │ ├── constructs.proto │ │ └── constructs_grpc.pb.go │ ├── helloworld │ │ ├── helloworld.pb.go │ │ ├── helloworld.proto │ │ └── helloworld_grpc.pb.go │ └── optionsserver │ │ ├── options.pb.go │ │ ├── options.proto │ │ └── options_grpc.pb.go ├── graphql │ └── v1 │ │ ├── extend.pb.go │ │ └── extend.proto └── test │ ├── constructs-input.pb.go │ ├── constructs-input.proto │ ├── constructs-input_grpc.pb.go │ ├── options-input.pb.go │ ├── options-input.proto │ └── options-input_grpc.pb.go ├── assets ├── arch.excalidraw.png ├── callgraph.png ├── pyroscope.png └── uptrace.png ├── buf.gen.yaml ├── buf.work.yaml ├── cmd └── gateway │ ├── kod_gen.go │ └── main.go ├── deployment ├── docker-compose.yml ├── grafana │ ├── custom.ini │ └── datasource.yml ├── otel-collector.yaml ├── prometheus │ └── prometheus.yml ├── uptrace.yml └── vector.toml ├── example └── gateway │ ├── config.yaml │ ├── constructsserver │ ├── kod_gen.go │ └── main.go │ ├── helloworld │ ├── kod_gen.go │ └── main.go │ └── optionsserver │ ├── kod_gen.go │ └── main.go ├── go.mod ├── go.sum ├── internal ├── config │ ├── config.go │ ├── config_test.go │ ├── kod_gen.go │ ├── kod_gen_interface.go │ └── kod_gen_mock.go └── server │ ├── gateway.go │ ├── gateway_middleware.go │ ├── gateway_pool.go │ ├── graphql_caller.go │ ├── graphql_caller_registry.go │ ├── graphql_fetch.go │ ├── graphql_query.go │ ├── http_upstream_invoker.go │ ├── http_uptream.go │ ├── kod_gen.go │ ├── kod_gen_interface.go │ └── kod_gen_mock.go ├── pkg ├── header │ └── headerprocessor.go ├── protographql │ ├── marshal.go │ ├── marshal_test.go │ ├── query_test.go │ ├── schema.go │ ├── schema.graphql │ ├── schema_test.go │ ├── unmarshal.go │ ├── unmarshal_test.go │ └── util.go └── protojson │ ├── descriptorsource.go │ ├── eventhandler.go │ └── requestparser.go ├── test ├── bench.sh ├── integration │ ├── fieldmask_test.go │ ├── graphql2grpc_test.go │ ├── graphql_schema_test.go │ ├── http_grpc_test.go │ ├── jwt_test.go │ ├── query.go │ ├── reflection_exit_test.go │ ├── singleflight_test.go │ └── testdata │ │ ├── constructs-expect.graphql │ │ ├── gateway-expect-without-unbound-method.graphql │ │ ├── gateway-expect.graphql │ │ ├── gateway-generate-without-unbound-method.graphql │ │ ├── gateway-generate.graphql │ │ └── options-expect.graphql ├── post1.json ├── post2.json └── util.go └── tools.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: / 6 | labels: 7 | - dependencies 8 | schedule: 9 | interval: "weekly" 10 | groups: 11 | gomod-normal-deps: 12 | update-types: 13 | - patch 14 | - minor 15 | gomod-breaking-deps: 16 | update-types: 17 | - major 18 | 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | labels: 22 | - dependencies 23 | schedule: 24 | interval: "weekly" 25 | groups: 26 | actions-deps: 27 | patterns: 28 | - "*" 29 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | version: ["stable"] 16 | 17 | steps: 18 | - name: Check out repository 19 | uses: actions/checkout@v4 20 | 21 | - name: docker-compose 22 | run: docker compose -f deployment/docker-compose.yml up -d 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.version }} 28 | cache: true 29 | 30 | - name: Install Task 31 | uses: arduino/setup-task@v2 32 | with: 33 | version: 3.x 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: generate and test 37 | run: | 38 | task 39 | 40 | - uses: codecov/codecov-action@v5 41 | with: 42 | files: ./coverage.out.final # optional 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,go 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,go 4 | 5 | ### Go ### 6 | # If you prefer the allow list template instead of the deny list, see community template: 7 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 8 | # 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | 25 | # Go workspace file 26 | go.work 27 | 28 | ### macOS ### 29 | # General 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Icon must end with two \r 35 | Icon 36 | 37 | 38 | # Thumbnails 39 | ._* 40 | 41 | # Files that might appear in the root of a volume 42 | .DocumentRevisions-V100 43 | .fseventsd 44 | .Spotlight-V100 45 | .TemporaryItems 46 | .Trashes 47 | .VolumeIcon.icns 48 | .com.apple.timemachine.donotpresent 49 | 50 | # Directories potentially created on remote AFP share 51 | .AppleDB 52 | .AppleDesktop 53 | Network Trash Folder 54 | Temporary Items 55 | .apdisk 56 | 57 | ### macOS Patch ### 58 | # iCloud generated files 59 | *.icloud 60 | 61 | ### VisualStudioCode ### 62 | .vscode/* 63 | !.vscode/settings.json 64 | !.vscode/tasks.json 65 | !.vscode/launch.json 66 | !.vscode/extensions.json 67 | !.vscode/*.code-snippets 68 | 69 | # Local History for Visual Studio Code 70 | .history/ 71 | 72 | # Built Visual Studio Code Extensions 73 | *.vsix 74 | 75 | ### VisualStudioCode Patch ### 76 | # Ignore all local history of files 77 | .history 78 | .ionide 79 | 80 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,go 81 | 82 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 83 | 84 | .task/ 85 | coverage.out* 86 | mockgen 87 | golangci-lint 88 | kod 89 | buf 90 | default.etcd/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "gateway", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "cwd": "${workspaceFolder}", 13 | "env": { 14 | "KOD_CONFIG": "${workspaceFolder}/example/gateway/config.yaml", 15 | "KOD_NAME": "gateway" 16 | }, 17 | "program": "./cmd/gateway" 18 | }, 19 | { 20 | "name": "optionsserver", 21 | "type": "go", 22 | "request": "launch", 23 | "mode": "auto", 24 | "cwd": "${workspaceFolder}", 25 | "env": { 26 | "KOD_NAME": "optionsserver" 27 | }, 28 | "program": "./example/gateway/optionsserver" 29 | }, 30 | { 31 | "name": "constructsserver", 32 | "type": "go", 33 | "request": "launch", 34 | "mode": "auto", 35 | "cwd": "${workspaceFolder}", 36 | "env": { 37 | "KOD_NAME": "constructsserver" 38 | }, 39 | "program": "./example/gateway/constructsserver" 40 | }, 41 | { 42 | "name": "helloworld", 43 | "type": "go", 44 | "request": "launch", 45 | "mode": "auto", 46 | "cwd": "${workspaceFolder}", 47 | "env": { 48 | "KOD_NAME": "helloworld" 49 | }, 50 | "program": "./example/gateway/helloworld" 51 | } 52 | ], 53 | "compounds": [ 54 | { 55 | "name": "All", 56 | "configurations": [ 57 | "constructsserver", 58 | "optionsserver", 59 | "helloworld", 60 | "gateway" 61 | ], 62 | "stopAll": true 63 | } 64 | ] 65 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.testFlags": [ 3 | "-v", 4 | "-coverpkg=github.com/go-kod/grpc-gateway/..." 5 | ] 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gRPC gateway 2 | 3 | [![Build and Test](https://github.com/go-kod/grpc-gateway/actions/workflows/go.yml/badge.svg)](https://github.com/go-kod/grpc-gateway/actions/workflows/go.yml) 4 | [![codecov](https://codecov.io/gh/go-kod/grpc-gateway/graph/badge.svg?token=UO18DMG15Z)](https://codecov.io/gh/go-kod/grpc-gateway) 5 | 6 | This is a simple gateway that can be used to expose multiple gRPC services as a GraphQL/HTTP server. 7 | 8 | ![arch](./assets//arch.excalidraw.png) 9 | 10 | ## Pre-requisites 11 | 12 | - [Task](https://taskfile.dev/#/installation) 13 | - [Docker](https://docs.docker.com/get-docker/) 14 | - [Docker Compose](https://docs.docker.com/compose/install/) 15 | - [Go](https://golang.org/doc/install) 16 | 17 | ## Installation 18 | 19 | To install the dependencies, run the following command: 20 | 21 | ```bash 22 | docker-compose -f "deployment/docker-compose.yml" up -d 23 | ``` 24 | 25 | ## Getting started 26 | 27 | To start the gateway, run the following command: 28 | 29 | ```bash 30 | task run 31 | ``` 32 | 33 | ```text 34 | task: [optionsserver] go run ./example/gateway/optionsserver 35 | task: [gateway] KOD_CONFIG=./example/gateway/config.yaml go run ./cmd/gateway 36 | task: [constructsserver] go run ./example/gateway/constructsserver 37 | 2024/07/18 19:53:26 INFO grpc server started on: [::]:8082 38 | 2024/07/18 19:53:26 INFO grpc server started on: [::]:8081 39 | 2024/07/18 19:53:26 [INFO] Gateway listening on address: [::]:8080 40 | 2024/07/18 19:53:27 INFO Register service key=local/optionsserver/grpc/172.19.43.45:8082 41 | 2024/07/18 19:53:27 INFO Register service key=local/constructsserver/grpc/172.19.43.45:8081 42 | ``` 43 | 44 | ## Test the gateway 45 | 46 | ```bash 47 | task curl 48 | ``` 49 | 50 | ```text 51 | task: [curl] curl 'http://localhost:8080/playground' -H 'Content-Type: application/json' --data-binary @test/post2.json -v 52 | * Trying 127.0.0.1:8080... 53 | * Connected to localhost (127.0.0.1) port 8080 (#0) 54 | > POST /playground HTTP/1.1 55 | > Host: localhost:8080 56 | > User-Agent: curl/8.1.2 57 | > Accept: */* 58 | > Content-Type: application/json 59 | > Content-Length: 423 60 | > 61 | < HTTP/1.1 200 OK 62 | < Content-Type: application/json 63 | < Date: Thu, 18 Jul 2024 11:55:21 GMT 64 | < Content-Length: 320 65 | < 66 | * Connection #0 to host localhost left intact 67 | {"data":{"serviceQuery1":{"float":[111],"foo":{"param1":"para"},"string":"sdfs","string2":"ssdfsd"},"serviceQuery2":{"float":[111],"foo":{"param1":"para"},"string":"sdfs","string2":"ssdfsd"}},"extensions":{"persistedQuery":{"sha265Hash":"5c898c064b9290d7b66dce6267db15478f8cd52e19498cde4b040f0e456e371d","version":"1"}}} 68 | ``` 69 | 70 | ## Benchmark 71 | 72 | To run the benchmark, run the following command: 73 | 74 | ```bash 75 | task bench 76 | ``` 77 | 78 | ```text 79 | task: [bench] ab -n 50000 -kc 500 -T 'application/json' -H 'Accept-Encoding: gzip, deflate, br' -p test/post1.json http://localhost:8080/query 80 | This is ApacheBench, Version 2.3 <$Revision: 1903618 $> 81 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 82 | Licensed to The Apache Software Foundation, http://www.apache.org/ 83 | 84 | Benchmarking localhost (be patient) 85 | Completed 5000 requests 86 | Completed 10000 requests 87 | Completed 15000 requests 88 | Completed 20000 requests 89 | Completed 25000 requests 90 | Completed 30000 requests 91 | Completed 35000 requests 92 | Completed 40000 requests 93 | Completed 45000 requests 94 | Completed 50000 requests 95 | Finished 50000 requests 96 | 97 | 98 | Server Software: 99 | Server Hostname: localhost 100 | Server Port: 8080 101 | 102 | Document Path: /query 103 | Document Length: 229 bytes 104 | 105 | Concurrency Level: 500 106 | Time taken for tests: 1.278 seconds 107 | Complete requests: 50000 108 | Failed requests: 0 109 | Keep-Alive requests: 50000 110 | Total transferred: 18100000 bytes 111 | Total body sent: 21100000 112 | HTML transferred: 11450000 bytes 113 | Requests per second: 39131.19 [#/sec] (mean) 114 | Time per request: 12.778 [ms] (mean) 115 | Time per request: 0.026 [ms] (mean, across all concurrent requests) 116 | Transfer rate: 13833.49 [Kbytes/sec] received 117 | 16126.33 kb/s sent 118 | 29959.82 kb/s total 119 | 120 | Connection Times (ms) 121 | min mean[+/-sd] median max 122 | Connect: 0 0 1.6 0 23 123 | Processing: 0 12 11.0 9 102 124 | Waiting: 0 12 11.0 9 102 125 | Total: 0 13 11.2 9 102 126 | 127 | Percentage of the requests served within a certain time (ms) 128 | 50% 9 129 | 66% 13 130 | 75% 16 131 | 80% 18 132 | 90% 28 133 | 95% 37 134 | 98% 48 135 | 99% 53 136 | 100% 102 (longest request) 137 | ``` 138 | 139 | ## Call Graph 140 | 141 | ![callgraph](./assets/callgraph.png) 142 | 143 | ## Pyroscope 144 | 145 | Visiting [http://localhost:4040](http://localhost:4040) will show the Pyroscope dashboard. 146 | 147 | ![pyroscope](./assets/pyroscope.png) 148 | 149 | ## Uptrace 150 | 151 | Visiting [http://localhost:14318/](http://localhost:14318/) will show the Uptrace dashboard. 152 | 153 | ![uptrace](./assets/uptrace.png) 154 | -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | env: 3 | GOBIN: { sh: pwd } 4 | # OTEL_SDK_DISABLED: true 5 | # OTEL_TRACES_EXPORTER: console 6 | OTEL_EXPORTER_OTLP_ENDPOINT: http://localhost:14318 7 | OTEL_EXPORTER_OTLP_HEADERS: "uptrace-dsn=http://project2_secret_token@localhost:14318?grpc=14317" 8 | OTEL_EXPORTER_OTLP_COMPRESSION: gzip 9 | OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: BASE2_EXPONENTIAL_BUCKET_HISTOGRAM 10 | OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: DELTA 11 | OTEL_TRACES_SAMPLER: parentbased_traceidratio 12 | OTEL_TRACES_SAMPLER_ARG: 1 13 | 14 | tasks: 15 | run: 16 | deps: 17 | - constructsserver 18 | - optionsserver 19 | - helloworld 20 | - gateway 21 | 22 | gateway: 23 | cmds: 24 | - KOD_CONFIG=./example/gateway/config.yaml go run ./cmd/gateway 25 | 26 | constructsserver: 27 | cmds: 28 | - go run ./example/gateway/constructsserver 29 | 30 | optionsserver: 31 | cmds: 32 | - go run ./example/gateway/optionsserver 33 | 34 | helloworld: 35 | cmds: 36 | - go run ./example/gateway/helloworld 37 | 38 | bench: 39 | cmds: 40 | - "ab -n 50000 -kc 500 -T 'application/json' -p test/post1.json http://localhost:8080/query" 41 | 42 | bench2: 43 | cmds: 44 | - "ab -n 50000 -kc 500 -T 'application/json' -p test/post2.json http://localhost:8080/query" 45 | 46 | bench3: 47 | cmds: 48 | - "ab -n 50000 -kc 500 'http://localhost:9090/say/bob'" 49 | 50 | curl: 51 | cmds: 52 | - "curl 'http://localhost:8080/playground' -H 'Content-Type: application/json' --data-binary @test/post2.json -v" 53 | 54 | curlhttp: 55 | cmds: 56 | - "curl 'http://localhost:9090/say/bob'" 57 | 58 | test: 59 | cmds: 60 | - go test -race -cover -coverprofile=coverage.out -covermode=atomic ./... -v 61 | sources: 62 | - "**/**.go" 63 | generates: 64 | - coverage.out 65 | test:coverage: 66 | cmds: 67 | - cat coverage.out | egrep -v "kod_|.pb.go|_test.go|example/gateway|/test|main.go|pkg/generator" > coverage.out.tmp 68 | - mv coverage.out.tmp coverage.out 69 | - go tool cover -func=coverage.out 70 | deps: 71 | - test 72 | 73 | install:mockgen: 74 | vars: 75 | VERSION: 76 | sh: | 77 | cat go.mod|grep go.uber.org/mock |awk -F ' ' '{print $2}' 78 | status: 79 | - test -f mockgen 80 | - go version -m $GOBIN/mockgen | grep go.uber.org/mock | grep {{.VERSION}} 81 | cmd: | 82 | go install go.uber.org/mock/mockgen@{{.VERSION}} 83 | 84 | install:golangci-lint: 85 | vars: 86 | VERSION: v1.60.1 87 | status: 88 | - test -f golangci-lint 89 | - go version -m $GOBIN/golangci-lint | grep github.com/golangci/golangci-lint | grep {{.VERSION}} 90 | cmd: | 91 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@{{.VERSION}} 92 | 93 | install:buf: 94 | vars: 95 | VERSION: v1.34.0 96 | status: 97 | - test -f buf 98 | cmd: | 99 | curl -sSL \ 100 | "https://github.com/bufbuild/buf/releases/download/{{.VERSION}}/buf-$(uname -s)-$(uname -m)" \ 101 | -o "$GOBIN/buf" && \ 102 | chmod +x "$GOBIN/buf" 103 | 104 | install:kod: 105 | vars: 106 | VERSION: 107 | sh: | 108 | cat go.mod|egrep "github.com/go-kod/kod " |awk -F ' ' '{print $2}' 109 | status: 110 | - test -f kod 111 | - go version -m $GOBIN/kod | grep github.com/go-kod/kod | grep {{.VERSION}} 112 | cmd: | 113 | go install github.com/go-kod/kod/cmd/kod@{{.VERSION}} 114 | 115 | mod: 116 | cmds: 117 | - go mod tidy 118 | 119 | default: 120 | cmds: 121 | - task generate 122 | - task golangci-lint 123 | - task test:coverage 124 | 125 | golangci-lint: 126 | cmds: 127 | - $GOBIN/golangci-lint run -v 128 | deps: 129 | - install:golangci-lint 130 | 131 | generate: 132 | cmds: 133 | - $GOBIN/buf generate 134 | - $GOBIN/kod generate -s ./... 135 | deps: 136 | - mod 137 | - install:mockgen 138 | - install:buf 139 | - install:kod 140 | -------------------------------------------------------------------------------- /api/buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v1 3 | deps: 4 | - remote: buf.build 5 | owner: googleapis 6 | repository: googleapis 7 | commit: 553fd4b4b3a640be9b69a3fa0c17b383 8 | digest: shake256:e30e3247f84b7ff9d09941ce391eb4b6f04734e1e5fae796bfc471f167e6f90813630cc39397ee46b8bc0ea7d6935c416d15c219cc5732d9778cbfdf73a1ed6e 9 | -------------------------------------------------------------------------------- /api/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | 3 | deps: 4 | - buf.build/googleapis/googleapis 5 | 6 | lint: 7 | rpc_allow_same_request_response: false 8 | rpc_allow_google_protobuf_empty_requests: true 9 | rpc_allow_google_protobuf_empty_responses: true 10 | 11 | ignore: 12 | - vendor 13 | use: 14 | - DEFAULT 15 | - COMMENTS 16 | - UNARY_RPC 17 | - PACKAGE_NO_IMPORT_CYCLE 18 | 19 | breaking: 20 | use: 21 | - FILE 22 | 23 | except: 24 | - RPC_NO_DELETE -------------------------------------------------------------------------------- /api/example/constructsserver/constructs.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package constructsserver; 3 | option go_package = "github.com/go-kod/grpc-gateway/gateway/constructsserver/pb;constructsserver"; 4 | 5 | import "google/protobuf/any.proto"; 6 | import "google/protobuf/empty.proto"; 7 | import "google/protobuf/timestamp.proto"; 8 | 9 | service Constructs { 10 | rpc Scalars_ (Scalars) returns (Scalars); // all possible scalars and same message as input and output 11 | rpc Repeated_ (Repeated) returns (Repeated); // all scalars messages and enums as repeated 12 | rpc Maps_ (Maps) returns (Maps); // all possible maps and different messages as input and output 13 | rpc Any_ (google.protobuf.Any) returns (google.protobuf.Any); // same name different types 14 | rpc Anyway_ (Any) returns (AnyInput); // some fake any types 15 | rpc Empty_ (google.protobuf.Empty) returns (Empty); // empty input and empty output 16 | rpc Empty2_ (EmptyRecursive) returns (EmptyNested); // messages with all empty fields 17 | rpc Empty3_ (Empty3) returns (Empty3); // messages with all empty fields 18 | rpc Ref_ (Ref) returns (Ref); 19 | rpc Oneof_ (Oneof) returns (Oneof); 20 | rpc CallWithId (Empty) returns (Empty); 21 | } 22 | message AnyInput { 23 | google.protobuf.Any any = 1; 24 | } 25 | message Empty3 { 26 | message Int { 27 | Empty3 e = 1; 28 | } 29 | Int i = 1; 30 | } 31 | 32 | message Foo { 33 | message Foo2 { 34 | string param1 = 1; 35 | } 36 | string param1 = 1; 37 | string param2 = 2; 38 | } 39 | 40 | enum Bar { 41 | BAR1 = 0; 42 | BAR2 = 1; 43 | BAR3 = 2; 44 | } 45 | 46 | message Baz { 47 | string param1 = 1; 48 | } 49 | 50 | message Scalars { 51 | double double = 1; 52 | float float = 2; 53 | int32 int32 = 3; 54 | int64 int64 = 4; 55 | uint32 uint32 = 5; 56 | uint64 uint64 = 6; 57 | sint32 sint32 = 7; 58 | sint64 sint64 = 8; 59 | fixed32 fixed32 = 9; 60 | fixed64 fixed64 = 10; 61 | sfixed32 sfixed32 = 11; 62 | sfixed64 sfixed64 = 12; 63 | bool bool = 13; 64 | string string = 14; 65 | bytes bytes = 15; 66 | Bar enum = 16; 67 | } 68 | 69 | message Repeated { 70 | repeated double double = 1; 71 | repeated float float = 2; 72 | repeated int32 int32 = 3; 73 | repeated int64 int64 = 4; 74 | repeated uint32 uint32 = 5; 75 | repeated uint64 uint64 = 6; 76 | repeated sint32 sint32 = 7; 77 | repeated sint64 sint64 = 8; 78 | repeated fixed32 fixed32 = 9; 79 | repeated fixed64 fixed64 = 10; 80 | repeated sfixed32 sfixed32 = 11; 81 | repeated sfixed64 sfixed64 = 12; 82 | repeated bool bool = 13; 83 | repeated string string = 14; 84 | repeated bytes bytes = 15; 85 | repeated Foo foo = 16; 86 | repeated Bar bar = 17; 87 | } 88 | 89 | message Maps { 90 | map int32_int32 = 1; 91 | map int64_int64 = 2; 92 | map uint32_uint32 = 3; 93 | map uint64_uint64 = 4; 94 | map sint32_sint32 = 5; 95 | map sint64_sint64 = 6; 96 | map fixed32_fixed32 = 7; 97 | map fixed64_fixed64 = 8; 98 | map sfixed32_sfixed32 = 9; 99 | map sfixed64_sfixed64 = 10; 100 | map bool_bool = 11; 101 | map string_string = 12; 102 | map string_bytes = 15; 103 | map string_float = 16; 104 | map string_double = 17; 105 | map string_foo = 13; 106 | map string_bar = 14; 107 | } 108 | 109 | message Any { 110 | google.protobuf.Any any = 1; 111 | } 112 | 113 | message Empty { 114 | } 115 | 116 | message EmptyRecursive { 117 | // google.protobuf.Empty nested1 = 1; 118 | EmptyRecursive empty = 2; 119 | } 120 | 121 | message EmptyNested { 122 | message EmptyNested1 { 123 | message EmptyNested2 { 124 | } 125 | EmptyNested2 nested2 = 1; 126 | } 127 | EmptyNested1 nested1 = 1; 128 | } 129 | 130 | message Timestamp { 131 | string time = 1; 132 | } 133 | 134 | message Ref { 135 | // google.protobuf.Empty empty = 10; // must disappear as part of is empty validation 136 | message Bar { 137 | string param1 = 1; 138 | } 139 | message Foo { 140 | enum En { 141 | A0 = 0; 142 | A1 = 1; 143 | } 144 | message Baz { 145 | message Gz { 146 | string param1 = 1; 147 | } 148 | } 149 | message Bar { 150 | enum En { 151 | A0 = 0; 152 | A1 = 1; 153 | } 154 | string param1 = 1; 155 | } 156 | Bar bar1 = 1; 157 | Timestamp local_time2 = 12; 158 | google.protobuf.Timestamp external_time1 = 13; 159 | Ref.Bar bar2 = 2; 160 | En en1 = 3; 161 | Bar.En en2 = 4; 162 | } 163 | Timestamp local_time2 = 12; 164 | google.protobuf.Timestamp external = 1; 165 | Timestamp local_time = 11; 166 | Baz file = 2; 167 | constructsserver.Foo file_msg = 8; 168 | .constructsserver.Bar file_enum = 9; 169 | Foo local = 3; 170 | .constructsserver.Foo.Foo2 foreign = 4; 171 | Foo.En en1 = 5; 172 | Foo.Bar.En en2 = 6; 173 | Foo.Baz.Gz gz = 7; 174 | } 175 | 176 | message Oneof { 177 | string param1 = 1; 178 | oneof Oneof1 { 179 | string param2 = 2; 180 | string param3 = 3; 181 | } 182 | oneof Oneof2 { 183 | string param4 = 4; 184 | string param5 = 5; 185 | } 186 | oneof Oneof3 { 187 | string param6 = 6; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /api/example/helloworld/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package helloworld; 4 | 5 | import "google/api/annotations.proto"; 6 | import "google/protobuf/wrappers.proto"; 7 | 8 | option go_package = "github.com/sysulq/gateway-grpc-gateway/api/example/helloworld"; 9 | 10 | service Greeter { 11 | rpc SayHello(HelloRequest) returns (HelloReply) { 12 | option (google.api.http) = { 13 | get: "/say/{name}" 14 | additional_bindings: {get: "/say/strval/{strVal}"} 15 | additional_bindings: {get: "/say/floatval/{floatVal}"} 16 | additional_bindings: {get: "/say/doubleval/{doubleVal}"} 17 | additional_bindings: {get: "/say/boolval/{boolVal}"} 18 | additional_bindings: {get: "/say/bytesval/{bytesVal}"} 19 | additional_bindings: {get: "/say/int32val/{int32Val}"} 20 | additional_bindings: {get: "/say/uint32val/{uint32Val}"} 21 | additional_bindings: {get: "/say/int64val/{int64Val}"} 22 | additional_bindings: {get: "/say/uint64val/{uint64Val}"} 23 | }; 24 | } 25 | } 26 | 27 | message HelloRequest { 28 | string name = 1; 29 | google.protobuf.StringValue strVal = 2; 30 | google.protobuf.FloatValue floatVal = 3; 31 | google.protobuf.DoubleValue doubleVal = 4; 32 | google.protobuf.BoolValue boolVal = 5; 33 | google.protobuf.BytesValue bytesVal = 6; 34 | google.protobuf.Int32Value int32Val = 7; 35 | google.protobuf.UInt32Value uint32Val = 8; 36 | google.protobuf.Int64Value int64Val = 9; 37 | google.protobuf.UInt64Value uint64Val = 10; 38 | } 39 | 40 | message HelloReply { 41 | string message = 1; 42 | } 43 | -------------------------------------------------------------------------------- /api/example/helloworld/helloworld_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc (unknown) 5 | // source: example/helloworld/helloworld.proto 6 | 7 | package helloworld 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.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | Greeter_SayHello_FullMethodName = "/helloworld.Greeter/SayHello" 23 | ) 24 | 25 | // GreeterClient is the client API for Greeter 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 GreeterClient interface { 29 | SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) 30 | } 31 | 32 | type greeterClient struct { 33 | cc grpc.ClientConnInterface 34 | } 35 | 36 | func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient { 37 | return &greeterClient{cc} 38 | } 39 | 40 | func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { 41 | out := new(HelloReply) 42 | err := c.cc.Invoke(ctx, Greeter_SayHello_FullMethodName, in, out, opts...) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return out, nil 47 | } 48 | 49 | // GreeterServer is the server API for Greeter service. 50 | // All implementations should embed UnimplementedGreeterServer 51 | // for forward compatibility 52 | type GreeterServer interface { 53 | SayHello(context.Context, *HelloRequest) (*HelloReply, error) 54 | } 55 | 56 | // UnimplementedGreeterServer should be embedded to have forward compatible implementations. 57 | type UnimplementedGreeterServer struct { 58 | } 59 | 60 | func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { 61 | return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") 62 | } 63 | 64 | // UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service. 65 | // Use of this interface is not recommended, as added methods to GreeterServer will 66 | // result in compilation errors. 67 | type UnsafeGreeterServer interface { 68 | mustEmbedUnimplementedGreeterServer() 69 | } 70 | 71 | func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) { 72 | s.RegisterService(&Greeter_ServiceDesc, srv) 73 | } 74 | 75 | func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 76 | in := new(HelloRequest) 77 | if err := dec(in); err != nil { 78 | return nil, err 79 | } 80 | if interceptor == nil { 81 | return srv.(GreeterServer).SayHello(ctx, in) 82 | } 83 | info := &grpc.UnaryServerInfo{ 84 | Server: srv, 85 | FullMethod: Greeter_SayHello_FullMethodName, 86 | } 87 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 88 | return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) 89 | } 90 | return interceptor(ctx, in, info, handler) 91 | } 92 | 93 | // Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service. 94 | // It's only intended for direct use with grpc.RegisterService, 95 | // and not to be introspected or modified (even as a copy) 96 | var Greeter_ServiceDesc = grpc.ServiceDesc{ 97 | ServiceName: "helloworld.Greeter", 98 | HandlerType: (*GreeterServer)(nil), 99 | Methods: []grpc.MethodDesc{ 100 | { 101 | MethodName: "SayHello", 102 | Handler: _Greeter_SayHello_Handler, 103 | }, 104 | }, 105 | Streams: []grpc.StreamDesc{}, 106 | Metadata: "example/helloworld/helloworld.proto", 107 | } 108 | -------------------------------------------------------------------------------- /api/example/optionsserver/options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package optionsserver; 3 | option go_package = "github.com/go-kod/grpc-gateway/gateway/optionsserver/pb;optionsserver"; 4 | 5 | import "graphql/v1/extend.proto"; 6 | 7 | service Service { 8 | rpc Mutate1 (Data) returns (Data); // must be a mutation 9 | rpc Mutate2 (Data) returns (Data); // must be a mutation 10 | rpc Query1 (Data) returns (Data) { 11 | option (graphql.v1.rpc) = {query: ""}; 12 | } // must be a query 13 | rpc Query2 (Data) returns (Data) { 14 | option (graphql.v1.rpc) = {query: ""}; 15 | } // must be a query 16 | 17 | rpc Publish (stream Data) returns (Data); // must be a mutation 18 | rpc Subscribe (Data) returns (stream Data); // must be a subscription 19 | rpc PubSub1 (stream Data) returns (stream Data); // must be a mutation and a subscription 20 | 21 | rpc InvalidSubscribe1 (stream Data) returns (Data) { 22 | option (graphql.v1.rpc) = {query: ""}; 23 | }; // must ignore 24 | rpc InvalidSubscribe2 (Data) returns (stream Data) { 25 | option (graphql.v1.rpc) = {mutation: ""}; 26 | }; // must ignore 27 | rpc InvalidSubscribe3 (stream Data) returns (stream Data) { 28 | option (graphql.v1.rpc) = {query: ""}; 29 | }; // must ignore 30 | rpc PubSub2 (stream Data) returns (stream Data) { 31 | option (graphql.v1.rpc) = {query: ""}; 32 | }; // must ignore 33 | 34 | rpc Name (Data) returns (Data) { 35 | option (graphql.v1.rpc) = {mutation: "newName"}; 36 | }; // method name should be "newName" 37 | } 38 | 39 | service Test { 40 | rpc Name (Data) returns (Data); // expect service name to be "name" 41 | rpc NewName (Data) returns (Data); // expect service name to be "newName1" since it collides with a name from a different service 42 | } 43 | 44 | service Query { 45 | rpc Query1 (Data) returns (Data); // must be a query 46 | rpc Query2 (Data) returns (Data); // must be a query 47 | rpc Mutate1 (Data) returns (Data) { 48 | option (graphql.v1.rpc) = {mutation: ""}; 49 | } // must be a mutation 50 | rpc Subscribe (Data) returns (stream Data); // must be a subscription 51 | } 52 | 53 | message Data { 54 | string string = 1 [(graphql.v1.field) = {required: true}]; // must be required 55 | Foo2 foo = 2 [(graphql.v1.field) = {required: true}]; // must be required 56 | repeated float float = 3 [(graphql.v1.field) = {required: true}]; // must be required because its greater than 0 57 | string string2 = 4; // simple 58 | Foo2 foo2 = 5; // simple 59 | repeated float float2 = 6; // simple 60 | } 61 | 62 | message Foo2 { 63 | string param1 = 1; 64 | } 65 | -------------------------------------------------------------------------------- /api/graphql/v1/extend.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package graphql.v1; 4 | 5 | option go_package = "github.com/go-kod/grpc-gateway/api/graphql/v1;graphqlv1"; 6 | 7 | import "google/protobuf/descriptor.proto"; 8 | 9 | // The options for the method 10 | extend google.protobuf.MethodOptions { 11 | // rpc defines the graphql information for the method 12 | Rpc rpc = 95279528; 13 | } 14 | 15 | // The options for the field 16 | extend google.protobuf.FieldOptions { 17 | // field defines the graphql information for the field 18 | Field field = 95279528; 19 | } 20 | 21 | extend google.protobuf.OneofOptions { 22 | // oneof defines the graphql information for the oneof 23 | Oneof oneof = 95279528; 24 | } 25 | 26 | // The options for the oneof 27 | message Oneof { 28 | // ignore the oneof 29 | bool ignore = 1; 30 | } 31 | 32 | // The options for the field 33 | message Field { 34 | // required the field is required 35 | bool required = 1; 36 | // ignore the field 37 | bool ignore = 2; 38 | } 39 | 40 | // The options for the rpc 41 | message Rpc { 42 | // pattern 43 | oneof pattern { 44 | // query is the graphql query 45 | string query = 1; 46 | // mutation is the graphql mutation 47 | string mutation = 2; 48 | } 49 | // ignore the rpc 50 | bool ignore = 3; 51 | } 52 | -------------------------------------------------------------------------------- /api/test/constructs-input.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package constructs; 3 | option go_package = "github.com/go-kod/grpc-gateway/example;pb"; 4 | 5 | import "google/protobuf/any.proto"; 6 | import "google/protobuf/empty.proto"; 7 | import "google/protobuf/timestamp.proto"; 8 | import "google/protobuf/field_mask.proto"; 9 | 10 | service Constructs { 11 | rpc Scalars_ (Scalars) returns (Scalars); // all possible scalars and same message as input and output 12 | rpc Repeated_ (Repeated) returns (Repeated); // all scalars messages and enums as repeated 13 | rpc Maps_ (Maps) returns (Maps); // all possible maps and different messages as input and output 14 | rpc Any_ (google.protobuf.Any) returns (google.protobuf.Any); // same name different types 15 | rpc Empty_ (google.protobuf.Empty) returns (Empty); // empty input and empty output 16 | rpc Empty2_ (EmptyRecursive) returns (EmptyNested); // messages with all empty fields 17 | rpc Empty3_ (Empty3) returns (Empty3); // messages with all empty fields 18 | rpc Ref_ (Ref) returns (Ref); 19 | rpc Oneof_ (Oneof) returns (Oneof); 20 | rpc CallWithId (Empty) returns (Empty); 21 | rpc Anyway_ (Any) returns (AnyInput); // some fake any types 22 | } 23 | 24 | message AnyInput { 25 | google.protobuf.Any any = 1; 26 | } 27 | 28 | message Empty3 { 29 | message Int { 30 | Empty3 e = 1; 31 | } 32 | Int i = 1; 33 | } 34 | 35 | message Foo { 36 | message Foo2 { 37 | string param1 = 1; 38 | } 39 | string param1 = 1; 40 | string param2 = 2; 41 | } 42 | 43 | enum Bar { 44 | BAR1 = 0; 45 | BAR2 = 1; 46 | BAR3 = 2; 47 | } 48 | 49 | message Baz { 50 | string param1 = 1; 51 | } 52 | 53 | message Scalars { 54 | double double = 1; 55 | float float = 2; 56 | int32 int32 = 3; 57 | int64 int64 = 4; 58 | uint32 uint32 = 5; 59 | uint64 uint64 = 6; 60 | sint32 sint32 = 7; 61 | sint64 sint64 = 8; 62 | fixed32 fixed32 = 9; 63 | fixed64 fixed64 = 10; 64 | sfixed32 sfixed32 = 11; 65 | sfixed64 sfixed64 = 12; 66 | bool bool = 13; 67 | string string_x = 14; // x for collisions with go method String 68 | bytes bytes = 15; 69 | google.protobuf.FieldMask paths = 16; 70 | } 71 | 72 | message Repeated { 73 | repeated double double = 1; 74 | repeated float float = 2; 75 | repeated int32 int32 = 3; 76 | repeated int64 int64 = 4; 77 | repeated uint32 uint32 = 5; 78 | repeated uint64 uint64 = 6; 79 | repeated sint32 sint32 = 7; 80 | repeated sint64 sint64 = 8; 81 | repeated fixed32 fixed32 = 9; 82 | repeated fixed64 fixed64 = 10; 83 | repeated sfixed32 sfixed32 = 11; 84 | repeated sfixed64 sfixed64 = 12; 85 | repeated bool bool = 13; 86 | repeated string string_x = 14; 87 | repeated bytes bytes = 15; 88 | repeated Foo foo = 16; 89 | repeated Bar bar = 17; 90 | } 91 | 92 | message Maps { 93 | map int32_int32 = 1; 94 | map int64_int64 = 2; 95 | map uint32_uint32 = 3; 96 | map uint64_uint64 = 4; 97 | map sint32_sint32 = 5; 98 | map sint64_sint64 = 6; 99 | map fixed32_fixed32 = 7; 100 | map fixed64_fixed64 = 8; 101 | map sfixed32_sfixed32 = 9; 102 | map sfixed64_sfixed64 = 10; 103 | map bool_bool = 11; 104 | map string_string = 12; 105 | map string_bytes = 15; 106 | map string_float = 16; 107 | map string_double = 17; 108 | map string_foo = 13; 109 | map string_bar = 14; 110 | } 111 | 112 | message Any { 113 | string param1 = 1; 114 | } 115 | 116 | message Empty { 117 | } 118 | 119 | message EmptyRecursive { 120 | google.protobuf.Empty nested1 = 1; 121 | EmptyRecursive empty = 2; 122 | } 123 | 124 | message EmptyNested { 125 | message EmptyNested1 { 126 | message EmptyNested2 { 127 | } 128 | EmptyNested2 nested2 = 1; 129 | } 130 | EmptyNested1 nested1 = 1; 131 | } 132 | 133 | message Timestamp { 134 | string time = 1; 135 | } 136 | 137 | message Ref { 138 | google.protobuf.Empty empty = 10; // must disappear as part of is empty validation 139 | message Bar { 140 | string param1 = 1; 141 | } 142 | message Foo { 143 | enum En { 144 | A0 = 0; 145 | A1 = 1; 146 | } 147 | message Baz { 148 | message Gz { 149 | string param1 = 1; 150 | } 151 | } 152 | message Bar { 153 | enum En { 154 | A0 = 0; 155 | A1 = 1; 156 | } 157 | string param1 = 1; 158 | } 159 | Bar bar1 = 1; 160 | Timestamp local_time2 = 12; 161 | google.protobuf.Timestamp external_time1 = 13; 162 | Ref.Bar bar2 = 2; 163 | En en1 = 3; 164 | Bar.En en2 = 4; 165 | } 166 | Timestamp local_time2 = 12; 167 | google.protobuf.Timestamp external = 1; 168 | Timestamp local_time = 11; 169 | Baz file = 2; 170 | constructs.Foo file_msg = 8; 171 | .constructs.Bar file_enum = 9; 172 | Foo local = 3; 173 | .constructs.Foo.Foo2 foreign = 4; 174 | Foo.En en1 = 5; 175 | Foo.Bar.En en2 = 6; 176 | Foo.Baz.Gz gz = 7; 177 | } 178 | 179 | message Oneof { 180 | string param1 = 1; 181 | oneof Oneof1 { 182 | string param2 = 2; 183 | string param3 = 3; 184 | } 185 | oneof Oneof2 { 186 | string param4 = 4; 187 | string param5 = 5; 188 | } 189 | oneof Oneof3 { 190 | string param6 = 6; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /api/test/options-input.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package options; 3 | option go_package = "github.com/go-kod/grpc-gateway/example;pb"; 4 | 5 | import "graphql/v1/extend.proto"; 6 | 7 | service Service { 8 | rpc Mutate1 (Data) returns (Data); // must be a mutation 9 | rpc Mutate2 (Data) returns (Data); // must be a mutation 10 | rpc Query1 (Data) returns (Data) { 11 | option (graphql.v1.rpc) = {query: ""}; 12 | } // must be a query 13 | 14 | rpc Publish (stream Data) returns (Data); // must be a mutation 15 | rpc Subscribe (Data) returns (stream Data); // must be a subscription 16 | rpc PubSub1 (stream Data) returns (stream Data); // must be a mutation and a subscription 17 | 18 | rpc InvalidSubscribe1 (stream Data) returns (Data) { 19 | option (graphql.v1.rpc) = {query: ""}; 20 | }; // must ignore 21 | rpc InvalidSubscribe2 (Data) returns (stream Data) { 22 | option (graphql.v1.rpc) = {mutation: ""}; 23 | }; // must ignore 24 | rpc InvalidSubscribe3 (stream Data) returns (stream Data) { 25 | option (graphql.v1.rpc) = {query: ""}; 26 | }; // must ignore 27 | rpc PubSub2 (stream Data) returns (stream Data) { 28 | option (graphql.v1.rpc) = {mutation: ""}; 29 | }; // must ignore 30 | 31 | rpc Ignore (Data) returns (Data) { 32 | option (graphql.v1.rpc) = {ignore: true}; 33 | }; // must ignore 34 | 35 | rpc Name (Data) returns (Data) { 36 | option (graphql.v1.rpc) = {mutation: "newName"}; 37 | }; // method name should be "newName" 38 | } 39 | 40 | service Test { 41 | rpc Name (Data) returns (Data); // expect service name to be "name" 42 | rpc NewName (Data) returns (Data); // expect service name to be "newName1" since it collides with a name from a different service 43 | } 44 | 45 | service Query { 46 | rpc Query1 (Data) returns (Data) { 47 | option (graphql.v1.rpc) = {query: "queryQuery1"}; 48 | }; // must be a query 49 | rpc Query2 (Data) returns (Data) { 50 | option (graphql.v1.rpc) = {query: "queryQuery2"}; 51 | }; // must be a query 52 | rpc Mutate1 (Data) returns (Data) { 53 | option (graphql.v1.rpc) = {mutation: ""}; 54 | } // must be a mutation 55 | rpc Subscribe (Data) returns (stream Data); // must be a subscription 56 | } 57 | 58 | message Data { 59 | string string_x = 1 [(graphql.v1.field) = {required: true}]; // must be required 60 | Foo2 foo = 2 [(graphql.v1.field) = {required: true}]; // must be required 61 | repeated double double = 3 [(graphql.v1.field) = {required: true}]; // must be required because its greater than 0 62 | string string2 = 4; // simple 63 | Foo2 foo2 = 5; // simple 64 | repeated double double2 = 6; // simple 65 | string bar = 7 [(graphql.v1.field) = {ignore: false}]; 66 | string string = 8 [(graphql.v1.field) = {ignore: false}]; 67 | } 68 | 69 | message Foo2 { 70 | string param1 = 1; 71 | } 72 | -------------------------------------------------------------------------------- /assets/arch.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-kod/grpc-gateway/d3dae30358e42fee0d4e3d250b8ea8cbf56e0c4b/assets/arch.excalidraw.png -------------------------------------------------------------------------------- /assets/callgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-kod/grpc-gateway/d3dae30358e42fee0d4e3d250b8ea8cbf56e0c4b/assets/callgraph.png -------------------------------------------------------------------------------- /assets/pyroscope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-kod/grpc-gateway/d3dae30358e42fee0d4e3d250b8ea8cbf56e0c4b/assets/pyroscope.png -------------------------------------------------------------------------------- /assets/uptrace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-kod/grpc-gateway/d3dae30358e42fee0d4e3d250b8ea8cbf56e0c4b/assets/uptrace.png -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | managed: 3 | enabled: true 4 | go_package_prefix: 5 | default: github.com/go-kod/grpc-gateway/api 6 | except: 7 | - buf.build/googleapis/googleapis 8 | plugins: 9 | - plugin: buf.build/protocolbuffers/go:v1.33.0 10 | out: api 11 | opt: 12 | - paths=source_relative 13 | 14 | - plugin: buf.build/grpc/go:v1.3.0 15 | out: api 16 | opt: 17 | - require_unimplemented_servers=false 18 | - paths=source_relative 19 | -------------------------------------------------------------------------------- /buf.work.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | directories: 3 | - api 4 | -------------------------------------------------------------------------------- /cmd/gateway/kod_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate". DO NOT EDIT. 2 | //go:build !ignoreKodGen 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/go-kod/kod" 8 | "reflect" 9 | ) 10 | 11 | // Full method names for components. 12 | const () 13 | 14 | func init() { 15 | kod.Register(&kod.Registration{ 16 | Name: "github.com/go-kod/kod/Main", 17 | Interface: reflect.TypeOf((*kod.Main)(nil)).Elem(), 18 | Impl: reflect.TypeOf(app{}), 19 | Refs: `⟦c03c7479:KoDeDgE:github.com/go-kod/kod/Main→github.com/go-kod/grpc-gateway/internal/config/Config⟧, 20 | ⟦f4859030:KoDeDgE:github.com/go-kod/kod/Main→github.com/go-kod/grpc-gateway/internal/server/Gateway⟧`, 21 | LocalStubFn: nil, 22 | }) 23 | } 24 | 25 | // CodeGen version check. 26 | var _ kod.CodeGenLatestVersion = kod.CodeGenVersion[[0][1]struct{}](` 27 | ERROR: You generated this file with 'kod generate' (codegen 28 | version v0.1.0). The generated code is incompatible with the version of the 29 | github.com/go-kod/kod module that you're using. The kod module 30 | version can be found in your go.mod file or by running the following command. 31 | 32 | go list -m github.com/go-kod/kod 33 | 34 | We recommend updating the kod module and the 'kod generate' command by 35 | running the following. 36 | 37 | go get github.com/go-kod/kod@latest 38 | go install github.com/go-kod/kod/cmd/kod@latest 39 | 40 | Then, re-run 'kod generate' and re-build your code. If the problem persists, 41 | please file an issue at https://github.com/go-kod/kod/issues. 42 | `) 43 | 44 | // kod.InstanceOf checks. 45 | var _ kod.InstanceOf[kod.Main] = (*app)(nil) 46 | 47 | // Local stub implementations. 48 | -------------------------------------------------------------------------------- /cmd/gateway/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "net/http" 8 | 9 | "github.com/go-kod/grpc-gateway/internal/config" 10 | "github.com/go-kod/grpc-gateway/internal/server" 11 | "github.com/go-kod/kod" 12 | "github.com/go-kod/kod/interceptor/kmetric" 13 | "github.com/go-kod/kod/interceptor/krecovery" 14 | "github.com/go-kod/kod/interceptor/ktrace" 15 | "github.com/samber/lo" 16 | ) 17 | 18 | type app struct { 19 | kod.Implements[kod.Main] 20 | 21 | config kod.Ref[config.Config] 22 | server kod.Ref[server.Gateway] 23 | } 24 | 25 | func run(ctx context.Context, app *app) error { 26 | cfg := app.config.Get().Config() 27 | 28 | l := lo.Must(net.Listen("tcp", cfg.Server.GraphQL.Address)) 29 | log.Printf("[INFO] Gateway listening on address: %s\n", l.Addr()) 30 | handler := lo.Must(app.server.Get().BuildServer()) 31 | go func() { lo.Must0(http.Serve(l, handler)) }() 32 | 33 | l = lo.Must(net.Listen("tcp", cfg.Server.HTTP.Address)) 34 | log.Printf("[INFO] Gateway listening on address: %s\n", l.Addr()) 35 | handler = lo.Must(app.server.Get().BuildHTTPServer()) 36 | lo.Must0(http.Serve(l, handler)) 37 | 38 | return nil 39 | } 40 | 41 | func main() { 42 | kod.MustRun(context.Background(), run, 43 | kod.WithInterceptors(krecovery.Interceptor(), ktrace.Interceptor(), kmetric.Interceptor()), 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /deployment/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redis: 5 | image: redis:6.2-alpine 6 | restart: always 7 | ports: 8 | - 6379:6379 9 | command: redis-server 10 | # volumes: 11 | # - ./data/cache:/data 12 | mysql: 13 | image: mysql:5.7 14 | restart: always 15 | # volumes: 16 | # - ./data/mysql:/var/run/mysqld/ 17 | command: --default-authentication-plugin=mysql_native_password 18 | environment: 19 | MYSQL_ROOT_PASSWORD: root 20 | MYSQL_ROOT_HOST: "%" 21 | 22 | ports: 23 | - 3306:3306 24 | volumes: 25 | - ./data/config/initdb:/docker-entrypoint-initdb.d 26 | # - ./data/mysql/data:/var/lib/mysql 27 | healthcheck: 28 | test: 29 | [ 30 | "CMD", 31 | "mysqladmin", 32 | "ping", 33 | "-h", 34 | "localhost", 35 | "-u", 36 | "root", 37 | "-proot" 38 | ] 39 | timeout: 20s 40 | retries: 10 41 | 42 | etcd: 43 | image: "quay.io/coreos/etcd:v3.5.14" 44 | restart: always 45 | environment: 46 | ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379" 47 | ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379" 48 | ETCDCTL_API: "3" 49 | ALLOW_NONE_AUTHENTICATION: "yes" 50 | ports: 51 | - 2379:2379 52 | - 2380:2380 53 | - 4001:4001 54 | 55 | clickhouse: 56 | image: clickhouse/clickhouse-server:23.7 57 | restart: on-failure 58 | environment: 59 | CLICKHOUSE_DB: uptrace 60 | healthcheck: 61 | test: ['CMD', 'wget', '--spider', '-q', 'localhost:8123/ping'] 62 | interval: 1s 63 | timeout: 1s 64 | retries: 30 65 | volumes: 66 | - ch_data2:/var/lib/clickhouse 67 | ports: 68 | - '8123:8123' 69 | - '9000:9000' 70 | 71 | postgres: 72 | image: postgres:15-alpine 73 | restart: on-failure 74 | environment: 75 | PGDATA: /var/lib/postgresql/data/pgdata 76 | POSTGRES_USER: uptrace 77 | POSTGRES_PASSWORD: uptrace 78 | POSTGRES_DB: uptrace 79 | healthcheck: 80 | test: ['CMD-SHELL', 'pg_isready -U uptrace -d uptrace'] 81 | interval: 1s 82 | timeout: 1s 83 | retries: 30 84 | volumes: 85 | - 'pg_data3:/var/lib/postgresql/data/pgdata' 86 | ports: 87 | - '5432:5432' 88 | 89 | uptrace: 90 | image: 'uptrace/uptrace:1.7.2' 91 | #image: 'uptrace/uptrace-dev:latest' 92 | restart: on-failure 93 | volumes: 94 | - ./uptrace.yml:/etc/uptrace/uptrace.yml 95 | #environment: 96 | # - DEBUG=2 97 | ports: 98 | - '14317:14317' 99 | - '14318:14318' 100 | 101 | otelcol: 102 | image: otel/opentelemetry-collector-contrib:0.88.0 103 | restart: on-failure 104 | volumes: 105 | - ./otel-collector.yaml:/etc/otelcol-contrib/config.yaml 106 | ports: 107 | - '4317:4317' 108 | - '4318:4318' 109 | 110 | mailpit: 111 | image: axllent/mailpit 112 | restart: always 113 | ports: 114 | - 1025:1025 115 | - 8025:8025 116 | environment: 117 | MP_MAX_MESSAGES: 5000 118 | MP_DATA_FILE: /data/mailpit.db 119 | MP_SMTP_AUTH_ACCEPT_ANY: 1 120 | MP_SMTP_AUTH_ALLOW_INSECURE: 1 121 | volumes: 122 | - mailpit_data:/data 123 | 124 | vector: 125 | image: timberio/vector:0.28.X-alpine 126 | volumes: 127 | - ./vector.toml:/etc/vector/vector.toml:ro 128 | 129 | grafana: 130 | image: grafana/grafana:9.5.3 131 | restart: on-failure 132 | volumes: 133 | - ./grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml 134 | - ./grafana/custom.ini:/etc/grafana/grafana.ini 135 | ports: 136 | - '3000:3000' 137 | 138 | prometheus: 139 | image: prom/prometheus:v2.36.2 140 | restart: always 141 | volumes: 142 | - ./prometheus/:/etc/prometheus/ 143 | - prometheus_data:/prometheus 144 | command: 145 | - '--config.file=/etc/prometheus/prometheus.yml' 146 | - '--storage.tsdb.path=/prometheus' 147 | - '--web.console.libraries=/usr/share/prometheus/console_libraries' 148 | - '--web.console.templates=/usr/share/prometheus/consoles' 149 | 150 | node_exporter: 151 | image: quay.io/prometheus/node-exporter:latest 152 | container_name: node_exporter 153 | command: 154 | - '--path.rootfs=/host' 155 | network_mode: host 156 | pid: host 157 | restart: unless-stopped 158 | volumes: 159 | - '/:/host:ro,rslave' 160 | 161 | 162 | pyroscope: 163 | image: grafana/pyroscope:latest 164 | restart: on-failure 165 | ports: 166 | - '4040:4040' 167 | 168 | volumes: 169 | ch_data2: 170 | pg_data3: 171 | prometheus_data: 172 | mailpit_data: -------------------------------------------------------------------------------- /deployment/grafana/custom.ini: -------------------------------------------------------------------------------- 1 | [feature_toggles] 2 | enable = tempoSearch tempoBackendSearch 3 | 4 | [security] 5 | admin_user = admin 6 | admin_password = admin 7 | 8 | [users] 9 | default_theme = light 10 | -------------------------------------------------------------------------------- /deployment/grafana/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Uptrace Tempo Project 1 5 | type: tempo 6 | access: proxy 7 | url: http://host.docker.internal:14318/api/tempo 8 | editable: true 9 | jsonData: 10 | httpHeaderName1: 'uptrace-dsn' 11 | secureJsonData: 12 | httpHeaderValue1: 'http://project1_secret_token@localhost:14317/1' 13 | -------------------------------------------------------------------------------- /deployment/otel-collector.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | health_check: 3 | pprof: 4 | endpoint: 0.0.0.0:1777 5 | zpages: 6 | endpoint: 0.0.0.0:55679 7 | 8 | receivers: 9 | otlp: 10 | protocols: 11 | grpc: 12 | http: 13 | hostmetrics: 14 | collection_interval: 10s 15 | scrapers: 16 | cpu: 17 | disk: 18 | load: 19 | filesystem: 20 | memory: 21 | network: 22 | paging: 23 | httpcheck: 24 | targets: 25 | - endpoint: 'http://localhost:13133/health/status' 26 | method: GET 27 | - endpoint: 'http://localhost:13134/health/status' 28 | method: GET 29 | collection_interval: 15s 30 | jaeger: 31 | protocols: 32 | grpc: 33 | postgresql: 34 | endpoint: postgres:5432 35 | transport: tcp 36 | username: uptrace 37 | password: uptrace 38 | databases: 39 | - uptrace 40 | tls: 41 | insecure: true 42 | prometheus/otelcol: 43 | config: 44 | scrape_configs: 45 | - job_name: 'otelcol' 46 | scrape_interval: 10s 47 | static_configs: 48 | - targets: ['0.0.0.0:8888'] 49 | 50 | processors: 51 | resourcedetection: 52 | detectors: ['system'] 53 | batch: 54 | send_batch_size: 10000 55 | timeout: 10s 56 | 57 | exporters: 58 | otlp/uptrace: 59 | endpoint: http://uptrace:14317 60 | tls: { insecure: true } 61 | headers: { 'uptrace-dsn': 'http://project1_secret_token@localhost:14318?grpc=14317' } 62 | 63 | service: 64 | telemetry: 65 | metrics: 66 | address: ':8888' 67 | # logs: 68 | # level: DEBUG 69 | pipelines: 70 | traces: 71 | receivers: [otlp, jaeger] 72 | processors: [batch] 73 | exporters: [otlp/uptrace] 74 | metrics: 75 | receivers: [otlp] 76 | processors: [batch] 77 | exporters: [otlp/uptrace] 78 | metrics/hostmetrics: 79 | receivers: [hostmetrics, postgresql, prometheus/otelcol, httpcheck] 80 | processors: [batch, resourcedetection] 81 | exporters: [otlp/uptrace] 82 | logs: 83 | receivers: [otlp] 84 | processors: [batch] 85 | exporters: [otlp/uptrace] 86 | 87 | extensions: [health_check, pprof, zpages] -------------------------------------------------------------------------------- /deployment/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 4 | evaluation_interval: 15s # By default, scrape targets every 15 seconds. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Attach these labels to any time series or alerts when communicating with 8 | # external systems (federation, remote storage, Alertmanager). 9 | external_labels: 10 | monitor: 'my-project' 11 | 12 | scrape_configs: 13 | - job_name: 'prometheus' 14 | scrape_interval: 15s 15 | static_configs: 16 | - targets: ['localhost:9090'] 17 | - job_name: 'node-exporter' 18 | scrape_interval: 15s 19 | static_configs: 20 | - targets: ['host.docker.internal:9100'] 21 | - job_name: docker 22 | static_configs: 23 | - targets: ['host.docker.internal:9323'] 24 | 25 | remote_write: 26 | - url: 'http://host.docker.internal:14318/api/v1/prometheus/write' 27 | headers: 28 | 'uptrace-dsn': 'http://project1_secret_token@localhost:14317/1' -------------------------------------------------------------------------------- /deployment/uptrace.yml: -------------------------------------------------------------------------------- 1 | ## 2 | ## Uptrace configuration file. 3 | ## See https://uptrace.dev/get/config.html for details. 4 | ## 5 | ## You can use environment variables anywhere in this file, for example: 6 | ## 7 | ## foo: $FOO 8 | ## bar: ${BAR} 9 | ## baz: ${BAZ:default} 10 | ## 11 | ## To escape `$`, use `$$`, for example: 12 | ## 13 | ## foo: $$FOO_BAR 14 | ## 15 | 16 | ## 17 | ## ClickHouse database credentials. 18 | ## 19 | ch: 20 | addr: clickhouse:9000 21 | user: default 22 | password: 23 | database: uptrace 24 | 25 | # TLS configuration. Uncomment to enable. 26 | # tls: 27 | # insecure_skip_verify: true 28 | 29 | # Maximum query execution time. 30 | max_execution_time: 30s 31 | 32 | ## 33 | ## PostgreSQL db that is used to store metadata such us metric names, dashboards, alerts, 34 | ## and so on. 35 | ## 36 | pg: 37 | addr: postgres:5432 38 | user: uptrace 39 | password: uptrace 40 | database: uptrace 41 | 42 | # TLS configuration. Uncomment to enable. 43 | # tls: 44 | # insecure_skip_verify: true # only for self-signed certificates 45 | 46 | ## 47 | ## A list of pre-configured projects. Each project is fully isolated. 48 | ## 49 | projects: 50 | # Conventionally, the first project is used to monitor Uptrace itself. 51 | - id: 1 52 | name: Uptrace 53 | # Token grants write access to the project. Keep a secret. 54 | token: project1_secret_token 55 | pinned_attrs: 56 | - service 57 | - host_name 58 | - deployment_environment 59 | # Group spans by deployment.environment attribute. 60 | group_by_env: false 61 | # Group funcs spans by service.name attribute. 62 | group_funcs_by_service: false 63 | # Enable prom_compat if you want to use the project as a Prometheus datasource in Grafana. 64 | prom_compat: true 65 | 66 | # Other projects can be used to monitor your applications. 67 | # To monitor micro-services or multiple related services, use a single project. 68 | - id: 2 69 | name: My project 70 | token: project2_secret_token 71 | pinned_attrs: 72 | - service 73 | - component 74 | - host_name 75 | - deployment_environment 76 | # Group spans by deployment.environment attribute. 77 | group_by_env: false 78 | # Group funcs spans by service.name attribute. 79 | group_funcs_by_service: false 80 | prom_compat: true 81 | 82 | ## 83 | ## To require authentication, uncomment one of the following sections. 84 | ## 85 | auth: 86 | users: 87 | - name: Anonymous 88 | email: uptrace@localhost 89 | password: uptrace 90 | notify_by_email: true 91 | 92 | # Cloudflare Zero Trust Access (Identity) 93 | # See https://developers.cloudflare.com/cloudflare-one/identity/ for more info. 94 | # cloudflare: 95 | # # The base URL of the Cloudflare Zero Trust team. 96 | # - team_url: https://myteam.cloudflareaccess.com 97 | # # The Application Audience (AUD) Tag for this application. 98 | # # You can retrieve this from the Cloudflare Zero Trust 'Access' Dashboard. 99 | # audience: bea6df23b944e4a0cd178609ba1bb64dc98dfe1f66ae7b918e563f6cf28b37e0 100 | 101 | # OpenID Connect (Single Sign-On) 102 | oidc: 103 | # # The ID is used in API endpoints, for example, in redirect URL 104 | # # `http:///api/v1/sso//callback`. 105 | # - id: keycloak 106 | # # Display name for the button in the login form. 107 | # # Default to 'OpenID Connect' 108 | # display_name: Keycloak 109 | # # The base URL for the OIDC provider. 110 | # issuer_url: http://localhost:8080/realms/uptrace 111 | # # The OAuth 2.0 Client ID 112 | # client_id: uptrace 113 | # # The OAuth 2.0 Client Secret 114 | # client_secret: ogbhd8Q0X0e5AZFGSG3m9oirPvnetqkA 115 | # # Additional OAuth 2.0 scopes to request from the OIDC provider. 116 | # # Defaults to 'profile'. 'openid' is requested by default and need not be specified. 117 | # scopes: 118 | # - profile 119 | 120 | ## 121 | ## Various options to tweak ClickHouse schema. 122 | ## For changes to take effect, you need reset the ClickHouse database with `ch reset`. 123 | ## 124 | ch_schema: 125 | # Compression codec, for example, LZ4, ZSTD(3), or Default. 126 | compression: ZSTD(3) 127 | 128 | # Whether to use ReplicatedMergeTree instead of MergeTree. 129 | replicated: false 130 | 131 | # Cluster name for Distributed tables and ON CLUSTER clause. 132 | #cluster: uptrace1 133 | 134 | spans: 135 | # Delete spans data after 30 days. 136 | ttl_delete: 7 DAY 137 | storage_policy: 'default' 138 | 139 | metrics: 140 | # Delete metrics data after 90 days. 141 | ttl_delete: 30 DAY 142 | storage_policy: 'default' 143 | 144 | ## 145 | ## Addresses on which Uptrace receives gRPC and HTTP requests. 146 | ## 147 | listen: 148 | # OTLP/gRPC API. 149 | grpc: 150 | addr: ':14317' 151 | 152 | # OTLP/HTTP API and Uptrace API with UI. 153 | http: 154 | addr: ':14318' 155 | 156 | # tls: 157 | # cert_file: config/tls/uptrace.crt 158 | # key_file: config/tls/uptrace.key 159 | 160 | ## 161 | ## Various options for Uptrace UI. 162 | ## 163 | site: 164 | # Overrides public URL for Vue-powered UI in case you put Uptrace behind a proxy. 165 | #addr: 'https://uptrace.mydomain.com/' 166 | 167 | ## 168 | ## Spans processing options. 169 | ## 170 | spans: 171 | # The size of the Go chan used to buffer incoming spans. 172 | # If the buffer is full, Uptrace starts to drop spans. 173 | #buffer_size: 100000 174 | 175 | # The number of spans to insert in a single query. 176 | #batch_size: 10000 177 | 178 | ## 179 | ## Metrics processing options. 180 | ## 181 | metrics: 182 | # List of attributes to drop for being noisy. 183 | drop_attrs: 184 | - telemetry.sdk.language 185 | - telemetry.sdk.name 186 | - telemetry.sdk.version 187 | 188 | # The size of the Go chan used to buffer incoming measures. 189 | # If the buffer is full, Uptrace starts to drop measures. 190 | #buffer_size: 100000 191 | 192 | # The number of measures to insert in a single query. 193 | #batch_size: 10000 194 | 195 | ## 196 | ## uptrace-go client configuration. 197 | ## Uptrace sends internal telemetry here. Defaults to listen.grpc.addr. 198 | ## 199 | uptrace_go: 200 | # Enabled by default. 201 | #disabled: true 202 | 203 | # dsn: http://project1_secret_token@localhost:14317/1 204 | # tls: 205 | # cert_file: config/tls/uptrace.crt 206 | # key_file: config/tls/uptrace.key 207 | # insecure_skip_verify: true 208 | 209 | ## 210 | ## SMTP settings to send emails. 211 | ## https://uptrace.dev/get/alerting.html 212 | ## 213 | smtp_mailer: 214 | # Whether to use this mailer for sending emails. 215 | enabled: true 216 | # SMTP server host. 217 | host: mailpit 218 | # SMTP server port. 219 | port: 1025 220 | # Username for authentication. 221 | username: mailpit 222 | # Password for authentication. 223 | password: mailpit 224 | # Disable TLS. Opportunistic TLS is used by default. 225 | tls: { disabled: true } 226 | # Emails will be send from this address. 227 | from: 'uptrace@localhost' 228 | 229 | ## 230 | ## Logging configuration. 231 | ## 232 | logs: 233 | # Zap minimal logging level. 234 | # Valid values: DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL. 235 | level: INFO 236 | 237 | # Secret key that is used to sign JWT tokens etc. 238 | secret_key: 102c1a557c314fc28198acd017960843 239 | 240 | # Enable to log HTTP requests and database queries. 241 | debug: false -------------------------------------------------------------------------------- /deployment/vector.toml: -------------------------------------------------------------------------------- 1 | [sources.syslog_logs] 2 | type = "demo_logs" 3 | format = "syslog" 4 | 5 | [sources.apache_common_logs] 6 | type = "demo_logs" 7 | format = "apache_common" 8 | 9 | [sources.apache_error_logs] 10 | type = "demo_logs" 11 | format = "apache_error" 12 | 13 | [sources.json_logs] 14 | type = "demo_logs" 15 | format = "json" 16 | 17 | [sources.my_docker_logs_source] 18 | type = "docker_logs" 19 | 20 | # Parse Syslog logs 21 | # See the Vector Remap Language reference for more info: https://vrl.dev 22 | [transforms.parse_logs] 23 | type = "remap" 24 | inputs = ["syslog_logs"] 25 | source = ''' 26 | . = parse_syslog!(string!(.message)) 27 | ''' 28 | 29 | # Export data to Uptrace. 30 | [sinks.uptrace] 31 | type = "http" 32 | method = "post" 33 | inputs = ["parse_logs", "apache_common_logs", "apache_error_logs", "json_logs", "my_docker_logs_source"] 34 | encoding.codec = "json" 35 | framing.method = "newline_delimited" 36 | compression = "gzip" 37 | uri = "http://localhost:14318/api/v1/vector/logs" 38 | #uri = "https://api.uptrace.dev/api/v1/vector/logs" 39 | request.headers.uptrace-dsn = "http://project1_secret_token@localhost:14318?grpc=14317" 40 | -------------------------------------------------------------------------------- /example/gateway/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | http: 3 | address: ":9090" 4 | singleFlight: true 5 | 6 | graphql: 7 | address: ":8080" 8 | disable: false 9 | playground: true 10 | generateUnboundMethods: true 11 | queryCache: true 12 | singleFlight: true 13 | 14 | engine: 15 | rateLimit: true 16 | circuitBreaker: true 17 | pyroscope: 18 | enable: true 19 | serverAddress: "http://localhost:4040" 20 | 21 | grpc: 22 | etcd: 23 | endpoints: ["localhost:2379"] 24 | timeout: "3s" 25 | 26 | services: 27 | - target: "etcd:///local/optionsserver/grpc" 28 | timeout: "1s" 29 | - target: "etcd:///local/constructsserver/grpc" 30 | timeout: "1s" 31 | - target: "etcd:///local/helloworld/grpc" 32 | timeout: "1s" 33 | -------------------------------------------------------------------------------- /example/gateway/constructsserver/kod_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate". DO NOT EDIT. 2 | //go:build !ignoreKodGen 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/go-kod/kod" 8 | "reflect" 9 | ) 10 | 11 | // Full method names for components. 12 | const () 13 | 14 | func init() { 15 | kod.Register(&kod.Registration{ 16 | Name: "github.com/go-kod/kod/Main", 17 | Interface: reflect.TypeOf((*kod.Main)(nil)).Elem(), 18 | Impl: reflect.TypeOf(app{}), 19 | Refs: ``, 20 | LocalStubFn: nil, 21 | }) 22 | } 23 | 24 | // CodeGen version check. 25 | var _ kod.CodeGenLatestVersion = kod.CodeGenVersion[[0][1]struct{}](` 26 | ERROR: You generated this file with 'kod generate' (codegen 27 | version v0.1.0). The generated code is incompatible with the version of the 28 | github.com/go-kod/kod module that you're using. The kod module 29 | version can be found in your go.mod file or by running the following command. 30 | 31 | go list -m github.com/go-kod/kod 32 | 33 | We recommend updating the kod module and the 'kod generate' command by 34 | running the following. 35 | 36 | go get github.com/go-kod/kod@latest 37 | go install github.com/go-kod/kod/cmd/kod@latest 38 | 39 | Then, re-run 'kod generate' and re-build your code. If the problem persists, 40 | please file an issue at https://github.com/go-kod/kod/issues. 41 | `) 42 | 43 | // kod.InstanceOf checks. 44 | var _ kod.InstanceOf[kod.Main] = (*app)(nil) 45 | 46 | // Local stub implementations. 47 | -------------------------------------------------------------------------------- /example/gateway/constructsserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | any "google.golang.org/protobuf/types/known/anypb" 7 | empty "google.golang.org/protobuf/types/known/emptypb" 8 | 9 | pb "github.com/go-kod/grpc-gateway/api/example/constructsserver" 10 | "github.com/go-kod/kod" 11 | "github.com/go-kod/kod-ext/core/otel" 12 | "github.com/go-kod/kod-ext/registry/etcdv3" 13 | "github.com/go-kod/kod-ext/server/kgrpc" 14 | "github.com/samber/lo" 15 | ) 16 | 17 | type app struct { 18 | kod.Implements[kod.Main] 19 | } 20 | 21 | func main() { 22 | kod.MustRun(context.Background(), func(ctx context.Context, app *app) error { 23 | otel := otel.Config{} 24 | err := otel.Init(ctx) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | etcd := lo.Must(etcdv3.Config{Endpoints: []string{"localhost:2379"}}.Build(ctx)) 30 | 31 | s := kgrpc.Config{ 32 | Address: ":8081", 33 | }.Build().WithRegistry(etcd) 34 | pb.RegisterConstructsServer(s, &service{}) 35 | 36 | return s.Run(ctx) 37 | }) 38 | } 39 | 40 | type service struct { 41 | pb.UnimplementedConstructsServer 42 | } 43 | 44 | func (s *service) Anyway_(ctx context.Context, a *pb.Any) (*pb.AnyInput, error) { 45 | return &pb.AnyInput{ 46 | Any: a.Any, 47 | }, nil 48 | } 49 | 50 | func (s *service) Scalars_(ctx context.Context, scalars *pb.Scalars) (*pb.Scalars, error) { 51 | return scalars, nil 52 | } 53 | 54 | func (s *service) Repeated_(ctx context.Context, repeated *pb.Repeated) (*pb.Repeated, error) { 55 | return repeated, nil 56 | } 57 | 58 | func (s *service) Maps_(ctx context.Context, maps *pb.Maps) (*pb.Maps, error) { 59 | return maps, nil 60 | } 61 | 62 | func (s *service) Any_(ctx context.Context, a *any.Any) (*any.Any, error) { 63 | println(a.String()) 64 | //_ = json.NewEncoder(os.Stdout).Encode(a) 65 | //var b pb.Bar 66 | //_ = ptypes.UnmarshalAny(a, &b) 67 | //println(b.String()) 68 | return a, nil 69 | } 70 | 71 | func (s *service) Empty_(ctx context.Context, empty *empty.Empty) (*pb.Empty, error) { 72 | return &pb.Empty{}, nil 73 | } 74 | 75 | func (s *service) Empty2_(ctx context.Context, recursive *pb.EmptyRecursive) (*pb.EmptyNested, error) { 76 | panic("implement me") 77 | } 78 | 79 | func (s *service) Empty3_(ctx context.Context, empty3 *pb.Empty3) (*pb.Empty3, error) { 80 | return &pb.Empty3{}, nil 81 | } 82 | 83 | func (s *service) Ref_(ctx context.Context, ref *pb.Ref) (*pb.Ref, error) { 84 | return ref, nil 85 | } 86 | 87 | func (s *service) Oneof_(ctx context.Context, oneof *pb.Oneof) (*pb.Oneof, error) { 88 | return oneof, nil 89 | } 90 | 91 | func (s *service) CallWithId(ctx context.Context, empty *pb.Empty) (*pb.Empty, error) { 92 | return &pb.Empty{}, nil 93 | } 94 | -------------------------------------------------------------------------------- /example/gateway/helloworld/kod_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate". DO NOT EDIT. 2 | //go:build !ignoreKodGen 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/go-kod/kod" 8 | "reflect" 9 | ) 10 | 11 | // Full method names for components. 12 | const () 13 | 14 | func init() { 15 | kod.Register(&kod.Registration{ 16 | Name: "github.com/go-kod/kod/Main", 17 | Interface: reflect.TypeOf((*kod.Main)(nil)).Elem(), 18 | Impl: reflect.TypeOf(app{}), 19 | Refs: ``, 20 | LocalStubFn: nil, 21 | }) 22 | } 23 | 24 | // CodeGen version check. 25 | var _ kod.CodeGenLatestVersion = kod.CodeGenVersion[[0][1]struct{}](` 26 | ERROR: You generated this file with 'kod generate' (codegen 27 | version v0.1.0). The generated code is incompatible with the version of the 28 | github.com/go-kod/kod module that you're using. The kod module 29 | version can be found in your go.mod file or by running the following command. 30 | 31 | go list -m github.com/go-kod/kod 32 | 33 | We recommend updating the kod module and the 'kod generate' command by 34 | running the following. 35 | 36 | go get github.com/go-kod/kod@latest 37 | go install github.com/go-kod/kod/cmd/kod@latest 38 | 39 | Then, re-run 'kod generate' and re-build your code. If the problem persists, 40 | please file an issue at https://github.com/go-kod/kod/issues. 41 | `) 42 | 43 | // kod.InstanceOf checks. 44 | var _ kod.InstanceOf[kod.Main] = (*app)(nil) 45 | 46 | // Local stub implementations. 47 | -------------------------------------------------------------------------------- /example/gateway/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-kod/grpc-gateway/api/example/helloworld" 7 | "github.com/go-kod/kod" 8 | "github.com/go-kod/kod-ext/core/otel" 9 | "github.com/go-kod/kod-ext/registry/etcdv3" 10 | "github.com/go-kod/kod-ext/server/kgrpc" 11 | "github.com/samber/lo" 12 | ) 13 | 14 | type app struct { 15 | kod.Implements[kod.Main] 16 | } 17 | 18 | func main() { 19 | kod.MustRun(context.Background(), func(ctx context.Context, app *app) error { 20 | otel := otel.Config{} 21 | err := otel.Init(ctx) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | etcd := lo.Must(etcdv3.Config{Endpoints: []string{"localhost:2379"}}.Build(ctx)) 27 | 28 | s := kgrpc.Config{ 29 | Address: ":8083", 30 | }.Build().WithRegistry(etcd) 31 | helloworld.RegisterGreeterServer(s, &service{}) 32 | 33 | return s.Run(ctx) 34 | }) 35 | } 36 | 37 | type service struct { 38 | helloworld.UnimplementedGreeterServer 39 | } 40 | 41 | func (s *service) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) { 42 | return &helloworld.HelloReply{ 43 | Message: "Hello " + req.Name, 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /example/gateway/optionsserver/kod_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate". DO NOT EDIT. 2 | //go:build !ignoreKodGen 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/go-kod/kod" 8 | "reflect" 9 | ) 10 | 11 | // Full method names for components. 12 | const () 13 | 14 | func init() { 15 | kod.Register(&kod.Registration{ 16 | Name: "github.com/go-kod/kod/Main", 17 | Interface: reflect.TypeOf((*kod.Main)(nil)).Elem(), 18 | Impl: reflect.TypeOf(app{}), 19 | Refs: ``, 20 | LocalStubFn: nil, 21 | }) 22 | } 23 | 24 | // CodeGen version check. 25 | var _ kod.CodeGenLatestVersion = kod.CodeGenVersion[[0][1]struct{}](` 26 | ERROR: You generated this file with 'kod generate' (codegen 27 | version v0.1.0). The generated code is incompatible with the version of the 28 | github.com/go-kod/kod module that you're using. The kod module 29 | version can be found in your go.mod file or by running the following command. 30 | 31 | go list -m github.com/go-kod/kod 32 | 33 | We recommend updating the kod module and the 'kod generate' command by 34 | running the following. 35 | 36 | go get github.com/go-kod/kod@latest 37 | go install github.com/go-kod/kod/cmd/kod@latest 38 | 39 | Then, re-run 'kod generate' and re-build your code. If the problem persists, 40 | please file an issue at https://github.com/go-kod/kod/issues. 41 | `) 42 | 43 | // kod.InstanceOf checks. 44 | var _ kod.InstanceOf[kod.Main] = (*app)(nil) 45 | 46 | // Local stub implementations. 47 | -------------------------------------------------------------------------------- /example/gateway/optionsserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | pb "github.com/go-kod/grpc-gateway/api/example/optionsserver" 7 | "github.com/go-kod/kod" 8 | "github.com/go-kod/kod-ext/core/otel" 9 | "github.com/go-kod/kod-ext/registry/etcdv3" 10 | "github.com/go-kod/kod-ext/server/kgrpc" 11 | "github.com/samber/lo" 12 | ) 13 | 14 | type app struct { 15 | kod.Implements[kod.Main] 16 | } 17 | 18 | func main() { 19 | kod.MustRun(context.Background(), func(ctx context.Context, app *app) error { 20 | otel := otel.Config{} 21 | err := otel.Init(ctx) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | etcd := lo.Must(etcdv3.Config{Endpoints: []string{"localhost:2379"}}.Build(ctx)) 27 | 28 | s := kgrpc.Config{ 29 | Address: ":8082", 30 | }.Build().WithRegistry(etcd) 31 | pb.RegisterServiceServer(s, &service{}) 32 | 33 | return s.Run(ctx) 34 | }) 35 | } 36 | 37 | type service struct{ pb.UnimplementedServiceServer } 38 | 39 | func (s service) Mutate1(ctx context.Context, data *pb.Data) (*pb.Data, error) { 40 | return data, nil 41 | } 42 | 43 | func (s service) Query2(ctx context.Context, data *pb.Data) (*pb.Data, error) { 44 | return data, nil 45 | } 46 | 47 | func (s service) Mutate2(ctx context.Context, data *pb.Data) (*pb.Data, error) { 48 | return data, nil 49 | } 50 | 51 | func (s service) Query1(ctx context.Context, data *pb.Data) (*pb.Data, error) { 52 | return data, nil 53 | } 54 | 55 | func (s service) Publish(server pb.Service_PublishServer) error { 56 | panic("implement me") 57 | } 58 | 59 | func (s service) Subscribe(data *pb.Data, server pb.Service_SubscribeServer) error { 60 | panic("implement me") 61 | } 62 | 63 | func (s service) PubSub1(server pb.Service_PubSub1Server) error { 64 | panic("implement me") 65 | } 66 | 67 | func (s service) InvalidSubscribe1(server pb.Service_InvalidSubscribe1Server) error { 68 | panic("implement me") 69 | } 70 | 71 | func (s service) InvalidSubscribe2(data *pb.Data, server pb.Service_InvalidSubscribe2Server) error { 72 | panic("implement me") 73 | } 74 | 75 | func (s service) InvalidSubscribe3(server pb.Service_InvalidSubscribe3Server) error { 76 | panic("implement me") 77 | } 78 | 79 | func (s service) PubSub2(server pb.Service_PubSub2Server) error { 80 | panic("implement me") 81 | } 82 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-kod/grpc-gateway 2 | 3 | go 1.22.7 4 | 5 | toolchain go1.23.0 6 | 7 | require ( 8 | github.com/cespare/xxhash/v2 v2.3.0 9 | github.com/fullstorydev/grpcurl v1.9.2 10 | github.com/go-kod/kod v0.17.0 11 | github.com/go-kod/kod-ext v0.4.0 12 | github.com/golang-jwt/jwt/v5 v5.2.1 13 | github.com/golang/protobuf v1.5.4 14 | github.com/hashicorp/golang-lru/v2 v2.0.7 15 | github.com/jhump/protoreflect v1.17.1-0.20240913204751-8f5fd1dcb3c5 16 | github.com/jhump/protoreflect/v2 v2.0.0-beta.2 17 | github.com/nautilus/gateway v0.4.0 18 | github.com/nautilus/graphql v0.0.26 19 | github.com/samber/lo v1.47.0 20 | github.com/stretchr/testify v1.10.0 21 | github.com/vektah/gqlparser/v2 v2.5.21 22 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 23 | go.uber.org/mock v0.5.0 24 | golang.org/x/sync v0.10.0 25 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 26 | google.golang.org/grpc v1.69.4 27 | google.golang.org/protobuf v1.36.2 28 | ) 29 | 30 | require ( 31 | github.com/99designs/gqlgen v0.17.56 // indirect 32 | github.com/agnivade/levenshtein v1.2.0 // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/bufbuild/protocompile v0.14.1 // indirect 35 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 36 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect 37 | github.com/coreos/go-semver v0.3.0 // indirect 38 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect 39 | github.com/creasty/defaults v1.8.0 // indirect 40 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 41 | github.com/dominikbraun/graph v0.23.0 // indirect 42 | github.com/ebitengine/purego v0.8.1 // indirect 43 | github.com/envoyproxy/go-control-plane v0.13.1 // indirect 44 | github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect 45 | github.com/fatih/color v1.18.0 // indirect 46 | github.com/felixge/httpsnoop v1.0.4 // indirect 47 | github.com/fsnotify/fsnotify v1.8.0 // indirect 48 | github.com/go-logr/logr v1.4.2 // indirect 49 | github.com/go-logr/stdr v1.2.2 // indirect 50 | github.com/go-ole/go-ole v1.3.0 // indirect 51 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 52 | github.com/gogo/protobuf v1.3.2 // indirect 53 | github.com/google/uuid v1.6.0 // indirect 54 | github.com/grafana/pyroscope-go v1.2.0 // indirect 55 | github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect 56 | github.com/graph-gophers/dataloader v5.0.0+incompatible // indirect 57 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect 58 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 59 | github.com/klauspost/compress v1.17.11 // indirect 60 | github.com/knadh/koanf/maps v0.1.1 // indirect 61 | github.com/knadh/koanf/parsers/json v0.1.0 // indirect 62 | github.com/knadh/koanf/parsers/toml/v2 v2.1.0 // indirect 63 | github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect 64 | github.com/knadh/koanf/providers/env v1.0.0 // indirect 65 | github.com/knadh/koanf/providers/file v1.1.2 // indirect 66 | github.com/knadh/koanf/v2 v2.1.2 // indirect 67 | github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect 68 | github.com/mattn/go-colorable v0.1.13 // indirect 69 | github.com/mattn/go-isatty v0.0.20 // indirect 70 | github.com/mitchellh/copystructure v1.2.0 // indirect 71 | github.com/mitchellh/mapstructure v1.5.0 // indirect 72 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 73 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 74 | github.com/opentracing/opentracing-go v1.2.0 // indirect 75 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 76 | github.com/pkg/errors v0.9.1 // indirect 77 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 78 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 79 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 80 | github.com/prometheus/client_golang v1.20.5 // indirect 81 | github.com/prometheus/client_model v0.6.1 // indirect 82 | github.com/prometheus/common v0.61.0 // indirect 83 | github.com/prometheus/procfs v0.15.1 // indirect 84 | github.com/shirou/gopsutil/v3 v3.24.5 // indirect 85 | github.com/shirou/gopsutil/v4 v4.24.12 // indirect 86 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 87 | github.com/sirupsen/logrus v1.9.3 // indirect 88 | github.com/sony/gobreaker v1.0.0 // indirect 89 | github.com/spf13/cobra v1.8.1 // indirect 90 | github.com/spf13/pflag v1.0.5 // indirect 91 | github.com/tklauser/go-sysconf v0.3.14 // indirect 92 | github.com/tklauser/numcpus v0.9.0 // indirect 93 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 94 | go.etcd.io/etcd/api/v3 v3.5.17 // indirect 95 | go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect 96 | go.etcd.io/etcd/client/v3 v3.5.17 // indirect 97 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 98 | go.opentelemetry.io/contrib/bridges/prometheus v0.58.0 // indirect 99 | go.opentelemetry.io/contrib/exporters/autoexport v0.58.0 // indirect 100 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect 101 | go.opentelemetry.io/contrib/instrumentation/host v0.58.0 // indirect 102 | go.opentelemetry.io/contrib/instrumentation/runtime v0.58.0 // indirect 103 | go.opentelemetry.io/otel v1.33.0 // indirect 104 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.9.0 // indirect 105 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.9.0 // indirect 106 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect 107 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 // indirect 108 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect 109 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect 110 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect 111 | go.opentelemetry.io/otel/exporters/prometheus v0.55.0 // indirect 112 | go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.9.0 // indirect 113 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 // indirect 114 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 // indirect 115 | go.opentelemetry.io/otel/log v0.9.0 // indirect 116 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 117 | go.opentelemetry.io/otel/sdk v1.33.0 // indirect 118 | go.opentelemetry.io/otel/sdk/log v0.9.0 // indirect 119 | go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect 120 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 121 | go.opentelemetry.io/proto/otlp v1.4.0 // indirect 122 | go.uber.org/multierr v1.11.0 // indirect 123 | go.uber.org/zap v1.26.0 // indirect 124 | golang.org/x/mod v0.22.0 // indirect 125 | golang.org/x/net v0.33.0 // indirect 126 | golang.org/x/sys v0.28.0 // indirect 127 | golang.org/x/text v0.21.0 // indirect 128 | golang.org/x/tools v0.28.0 // indirect 129 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 130 | gopkg.in/yaml.v3 v3.0.1 // indirect 131 | ) 132 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/go-kod/kod" 5 | "github.com/go-kod/kod-ext/client/kgrpc" 6 | "github.com/go-kod/kod-ext/core/otel" 7 | "github.com/go-kod/kod-ext/core/pyroscope" 8 | "github.com/go-kod/kod-ext/registry/etcdv3" 9 | ) 10 | 11 | type config struct { 12 | kod.Implements[Config] 13 | kod.WithGlobalConfig[ConfigInfo] 14 | } 15 | 16 | type ConfigInfo struct { 17 | Server ServerConfig 18 | Engine EngineConfig 19 | Grpc Grpc 20 | } 21 | 22 | type Pyroscope struct { 23 | pyroscope.Config `koanf:",squash"` 24 | Enable bool 25 | } 26 | 27 | type Jwt struct { 28 | Enable bool 29 | LocalJwks string 30 | ForwardPayloadHeader string 31 | } 32 | 33 | type JwtClaimToHeader struct { 34 | HeaderName string 35 | ClaimName string 36 | } 37 | 38 | type EngineConfig struct { 39 | Otel otel.Config 40 | Pyroscope Pyroscope 41 | RateLimit bool 42 | CircuitBreaker bool 43 | } 44 | 45 | type ServerConfig struct { 46 | GraphQL GraphQLConfig 47 | HTTP HTTPConfig 48 | } 49 | 50 | type HTTPConfig struct { 51 | Address string 52 | Disable bool 53 | Jwt Jwt 54 | SingleFlight bool 55 | } 56 | 57 | type GraphQLConfig struct { 58 | Address string 59 | Disable bool 60 | Playground bool 61 | Jwt Jwt 62 | GenerateUnboundMethods bool 63 | QueryCache bool 64 | SingleFlight bool 65 | } 66 | 67 | type Grpc struct { 68 | Etcd etcdv3.Config 69 | Services []kgrpc.Config 70 | } 71 | 72 | func (ins *config) Config() *ConfigInfo { 73 | return ins.WithGlobalConfig.Config() 74 | } 75 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/go-kod/kod" 9 | "github.com/go-kod/kod-ext/client/kgrpc" 10 | "github.com/go-kod/kod-ext/core/pyroscope" 11 | "github.com/go-kod/kod-ext/registry/etcdv3" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestConfig(t *testing.T) { 16 | kod.RunTest(t, func(ctx context.Context, c Config) { 17 | require.Equal(t, &ConfigInfo{ 18 | Engine: EngineConfig{ 19 | RateLimit: true, 20 | CircuitBreaker: true, 21 | 22 | Pyroscope: Pyroscope{ 23 | Enable: true, 24 | Config: pyroscope.Config{ServerAddress: "http://localhost:4040"}, 25 | }, 26 | }, 27 | Server: ServerConfig{ 28 | GraphQL: GraphQLConfig{ 29 | Address: ":8080", 30 | Disable: false, 31 | Playground: true, 32 | GenerateUnboundMethods: true, 33 | QueryCache: true, 34 | SingleFlight: true, 35 | }, 36 | HTTP: HTTPConfig{ 37 | Address: ":9090", 38 | SingleFlight: true, 39 | }, 40 | }, 41 | Grpc: Grpc{ 42 | Etcd: etcdv3.Config{ 43 | Endpoints: []string{"localhost:2379"}, 44 | Timeout: 3 * time.Second, 45 | TTL: 60, 46 | }, 47 | Services: []kgrpc.Config{ 48 | { 49 | Target: "etcd:///local/optionsserver/grpc", 50 | Timeout: time.Second, 51 | }, 52 | { 53 | Target: "etcd:///local/constructsserver/grpc", 54 | Timeout: time.Second, 55 | }, 56 | { 57 | Target: "etcd:///local/helloworld/grpc", 58 | Timeout: time.Second, 59 | }, 60 | }, 61 | }, 62 | }, c.Config()) 63 | }, kod.WithConfigFile("../../example/gateway/config.yaml")) 64 | } 65 | -------------------------------------------------------------------------------- /internal/config/kod_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate". DO NOT EDIT. 2 | //go:build !ignoreKodGen 3 | 4 | package config 5 | 6 | import ( 7 | "context" 8 | "github.com/go-kod/kod" 9 | "github.com/go-kod/kod/interceptor" 10 | "reflect" 11 | ) 12 | 13 | // Full method names for components. 14 | const () 15 | 16 | func init() { 17 | kod.Register(&kod.Registration{ 18 | Name: "github.com/go-kod/grpc-gateway/internal/config/Config", 19 | Interface: reflect.TypeOf((*Config)(nil)).Elem(), 20 | Impl: reflect.TypeOf(config{}), 21 | Refs: ``, 22 | LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { 23 | return config_local_stub{ 24 | impl: info.Impl.(Config), 25 | interceptor: info.Interceptor, 26 | } 27 | }, 28 | }) 29 | } 30 | 31 | // CodeGen version check. 32 | var _ kod.CodeGenLatestVersion = kod.CodeGenVersion[[0][1]struct{}](` 33 | ERROR: You generated this file with 'kod generate' (codegen 34 | version v0.1.0). The generated code is incompatible with the version of the 35 | github.com/go-kod/kod module that you're using. The kod module 36 | version can be found in your go.mod file or by running the following command. 37 | 38 | go list -m github.com/go-kod/kod 39 | 40 | We recommend updating the kod module and the 'kod generate' command by 41 | running the following. 42 | 43 | go get github.com/go-kod/kod@latest 44 | go install github.com/go-kod/kod/cmd/kod@latest 45 | 46 | Then, re-run 'kod generate' and re-build your code. If the problem persists, 47 | please file an issue at https://github.com/go-kod/kod/issues. 48 | `) 49 | 50 | // kod.InstanceOf checks. 51 | var _ kod.InstanceOf[Config] = (*config)(nil) 52 | 53 | // Local stub implementations. 54 | // config_local_stub is a local stub implementation of [Config]. 55 | type config_local_stub struct { 56 | impl Config 57 | interceptor interceptor.Interceptor 58 | } 59 | 60 | // Check that [config_local_stub] implements the [Config] interface. 61 | var _ Config = (*config_local_stub)(nil) 62 | 63 | // Config wraps the method [config.Config]. 64 | func (s config_local_stub) Config() (r0 *ConfigInfo) { 65 | // Because the first argument is not context.Context, so interceptors are not supported. 66 | r0 = s.impl.Config() 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /internal/config/kod_gen_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate"; DO NOT EDIT. 2 | 3 | package config 4 | 5 | // Config is implemented by [config], 6 | // which can be mocked with [NewMockConfig]. 7 | type Config interface { 8 | // Config is implemented by [config.Config] 9 | Config() *ConfigInfo 10 | } 11 | -------------------------------------------------------------------------------- /internal/config/kod_gen_mock.go: -------------------------------------------------------------------------------- 1 | //go:build !ignoreKodGen 2 | 3 | // Code generated by MockGen. DO NOT EDIT. 4 | // Source: internal/config/kod_gen_interface.go 5 | // 6 | // Generated by this command: 7 | // 8 | // mockgen -source internal/config/kod_gen_interface.go -destination internal/config/kod_gen_mock.go -package config -typed -build_constraint !ignoreKodGen 9 | // 10 | 11 | // Package config is a generated GoMock package. 12 | package config 13 | 14 | import ( 15 | reflect "reflect" 16 | 17 | gomock "go.uber.org/mock/gomock" 18 | ) 19 | 20 | // MockConfig is a mock of Config interface. 21 | type MockConfig struct { 22 | ctrl *gomock.Controller 23 | recorder *MockConfigMockRecorder 24 | isgomock struct{} 25 | } 26 | 27 | // MockConfigMockRecorder is the mock recorder for MockConfig. 28 | type MockConfigMockRecorder struct { 29 | mock *MockConfig 30 | } 31 | 32 | // NewMockConfig creates a new mock instance. 33 | func NewMockConfig(ctrl *gomock.Controller) *MockConfig { 34 | mock := &MockConfig{ctrl: ctrl} 35 | mock.recorder = &MockConfigMockRecorder{mock} 36 | return mock 37 | } 38 | 39 | // EXPECT returns an object that allows the caller to indicate expected use. 40 | func (m *MockConfig) EXPECT() *MockConfigMockRecorder { 41 | return m.recorder 42 | } 43 | 44 | // Config mocks base method. 45 | func (m *MockConfig) Config() *ConfigInfo { 46 | m.ctrl.T.Helper() 47 | ret := m.ctrl.Call(m, "Config") 48 | ret0, _ := ret[0].(*ConfigInfo) 49 | return ret0 50 | } 51 | 52 | // Config indicates an expected call of Config. 53 | func (mr *MockConfigMockRecorder) Config() *MockConfigConfigCall { 54 | mr.mock.ctrl.T.Helper() 55 | call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Config", reflect.TypeOf((*MockConfig)(nil).Config)) 56 | return &MockConfigConfigCall{Call: call} 57 | } 58 | 59 | // MockConfigConfigCall wrap *gomock.Call 60 | type MockConfigConfigCall struct { 61 | *gomock.Call 62 | } 63 | 64 | // Return rewrite *gomock.Call.Return 65 | func (c *MockConfigConfigCall) Return(arg0 *ConfigInfo) *MockConfigConfigCall { 66 | c.Call = c.Call.Return(arg0) 67 | return c 68 | } 69 | 70 | // Do rewrite *gomock.Call.Do 71 | func (c *MockConfigConfigCall) Do(f func() *ConfigInfo) *MockConfigConfigCall { 72 | c.Call = c.Call.Do(f) 73 | return c 74 | } 75 | 76 | // DoAndReturn rewrite *gomock.Call.DoAndReturn 77 | func (c *MockConfigConfigCall) DoAndReturn(f func() *ConfigInfo) *MockConfigConfigCall { 78 | c.Call = c.Call.DoAndReturn(f) 79 | return c 80 | } 81 | -------------------------------------------------------------------------------- /internal/server/gateway.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/go-kod/grpc-gateway/internal/config" 11 | "github.com/go-kod/kod" 12 | "github.com/hashicorp/golang-lru/v2/expirable" 13 | "github.com/nautilus/gateway" 14 | "github.com/nautilus/graphql" 15 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 16 | ) 17 | 18 | type server struct { 19 | kod.Implements[Gateway] 20 | 21 | config kod.Ref[config.Config] 22 | _ kod.Ref[GraphqlCaller] 23 | queryer kod.Ref[GraphqlQueryer] 24 | registry kod.Ref[GraphqlCallerRegistry] 25 | httpUpstream kod.Ref[HttpUpstream] 26 | } 27 | 28 | func (ins *server) Init(ctx context.Context) error { 29 | cfg := ins.config.Get().Config() 30 | if cfg.Engine.Pyroscope.Enable { 31 | err := ins.config.Get().Config().Engine.Pyroscope.Init(ctx) 32 | if err != nil { 33 | return err 34 | } 35 | } 36 | 37 | err := cfg.Engine.Otel.Init(ctx) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func (ins *server) Shutdown(ctx context.Context) error { 46 | return nil 47 | } 48 | 49 | func (s *server) BuildServer() (http.Handler, error) { 50 | queryFactory := gateway.QueryerFactory(func(ctx *gateway.PlanningContext, url string) graphql.Queryer { 51 | return s.queryer.Get() 52 | }) 53 | 54 | sources := []*graphql.RemoteSchema{{URL: "url1"}} 55 | sources[0].Schema = s.registry.Get().GraphQLSchema() 56 | 57 | // formatter.NewFormatter(os.Stdout).FormatSchema(sources[0].Schema) 58 | 59 | opts := []gateway.Option{ 60 | gateway.WithLogger(&noopLogger{}), 61 | gateway.WithQueryerFactory(&queryFactory), 62 | } 63 | if s.config.Get().Config().Server.GraphQL.QueryCache { 64 | opts = append(opts, gateway.WithQueryPlanCache(NewQueryPlanCacher())) 65 | } 66 | 67 | g, err := gateway.New(sources, opts...) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | mux := http.NewServeMux() 73 | cfg := s.config.Get().Config() 74 | 75 | if !cfg.Server.GraphQL.Disable { 76 | mux.HandleFunc("/query", g.GraphQLHandler) 77 | if cfg.Server.GraphQL.Playground { 78 | mux.HandleFunc("/playground", g.PlaygroundHandler) 79 | } 80 | } 81 | 82 | var handler http.Handler = addHeader(mux) 83 | handler = otelhttp.NewMiddleware("graphql-gateway")(handler) 84 | 85 | if cfg.Server.GraphQL.Jwt.Enable { 86 | handler = s.jwtAuthHandler(handler) 87 | } 88 | 89 | return handler, nil 90 | } 91 | 92 | func (s *server) BuildHTTPServer() (http.Handler, error) { 93 | mux := http.NewServeMux() 94 | cfg := s.config.Get().Config() 95 | 96 | if !cfg.Server.HTTP.Disable { 97 | s.httpUpstream.Get().Register(context.Background(), mux) 98 | } 99 | 100 | var handler http.Handler = addHeader(mux) 101 | handler = otelhttp.NewMiddleware("graphql-gateway")(handler) 102 | 103 | if cfg.Server.HTTP.Jwt.Enable { 104 | handler = s.jwtAuthHandler(handler) 105 | } 106 | 107 | return handler, nil 108 | } 109 | 110 | type noopLogger struct { 111 | gateway.Logger 112 | } 113 | 114 | func (noopLogger) Debug(args ...interface{}) {} 115 | func (noopLogger) Info(args ...interface{}) {} 116 | func (noopLogger) Warn(args ...interface{}) {} 117 | func (l noopLogger) WithFields(fields gateway.LoggerFields) gateway.Logger { return l } 118 | func (noopLogger) QueryPlanStep(step *gateway.QueryPlanStep) {} 119 | 120 | type queryPlanCacher struct { 121 | cache *expirable.LRU[string, gateway.QueryPlanList] 122 | } 123 | 124 | func NewQueryPlanCacher() *queryPlanCacher { 125 | cache := expirable.NewLRU[string, gateway.QueryPlanList](1024, nil, time.Hour) 126 | return &queryPlanCacher{cache: cache} 127 | } 128 | 129 | func (c *queryPlanCacher) Retrieve(ctx *gateway.PlanningContext, hash *string, planner gateway.QueryPlanner) (gateway.QueryPlanList, error) { 130 | // if there is no hash 131 | if *hash == "" { 132 | hashString := sha256.Sum256([]byte(ctx.Query)) 133 | // generate a hash that will identify the query for later use 134 | *hash = hex.EncodeToString(hashString[:]) 135 | } 136 | 137 | if plan, ok := c.cache.Get(*hash); ok { 138 | return plan, nil 139 | } 140 | 141 | // compute the plan 142 | plan, err := planner.Plan(ctx) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | c.cache.Add(*hash, plan) 148 | 149 | return plan, nil 150 | } 151 | -------------------------------------------------------------------------------- /internal/server/gateway_middleware.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/go-kod/grpc-gateway/pkg/header" 9 | "github.com/golang-jwt/jwt/v5" 10 | "google.golang.org/grpc/metadata" 11 | ) 12 | 13 | func addHeader(handler http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | md, _ := metadata.FromOutgoingContext(r.Context()) 16 | md = metadata.Join(md, header.HttpHeadersToGRPCMetadata(r.Header)) 17 | 18 | ctx := metadata.NewOutgoingContext(r.Context(), md) 19 | handler.ServeHTTP(w, r.WithContext(ctx)) 20 | }) 21 | } 22 | 23 | func (ins *server) jwtAuthHandler(handler http.Handler) http.Handler { 24 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | tokenString := r.Header.Get("Authorization") 26 | if tokenString == "" { 27 | w.WriteHeader(http.StatusUnauthorized) 28 | fmt.Fprint(w, "Missing authorization header") 29 | return 30 | } 31 | tokenString = tokenString[len("Bearer "):] 32 | 33 | token, err := ins.verifyToken(tokenString) 34 | if err != nil { 35 | w.WriteHeader(http.StatusUnauthorized) 36 | fmt.Fprint(w, "Invalid token") 37 | return 38 | } 39 | 40 | md, ok := metadata.FromOutgoingContext(r.Context()) 41 | if !ok { 42 | md = metadata.New(nil) 43 | } 44 | 45 | forwardPayloadHeader := ins.config.Get().Config().Server.GraphQL.Jwt.ForwardPayloadHeader 46 | if len(forwardPayloadHeader) > 0 { 47 | md.Set(forwardPayloadHeader, strings.Split(token.Raw, ".")[1]) 48 | } 49 | 50 | ctx := metadata.NewOutgoingContext(r.Context(), md) 51 | 52 | handler.ServeHTTP(w, r.WithContext(ctx)) 53 | }) 54 | } 55 | 56 | func (ins *server) verifyToken(tokenString string) (*jwt.Token, error) { 57 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 58 | return []byte(ins.config.Get().Config().Server.GraphQL.Jwt.LocalJwks), nil 59 | }) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | if !token.Valid { 65 | return nil, fmt.Errorf("invalid token") 66 | } 67 | 68 | return token, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/server/gateway_pool.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/cespare/xxhash/v2" 7 | ) 8 | 9 | var Hash64 = hash64Pool{ 10 | pool: sync.Pool{ 11 | New: func() interface{} { 12 | return xxhash.New() 13 | }, 14 | }, 15 | } 16 | 17 | type hash64Pool struct { 18 | pool sync.Pool 19 | } 20 | 21 | func (b *hash64Pool) Get() *xxhash.Digest { 22 | xxh := b.pool.Get().(*xxhash.Digest) 23 | xxh.Reset() 24 | return xxh 25 | } 26 | 27 | func (b *hash64Pool) Put(xxh *xxhash.Digest) { 28 | b.pool.Put(xxh) 29 | } 30 | -------------------------------------------------------------------------------- /internal/server/graphql_caller.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/go-kod/grpc-gateway/internal/config" 10 | "github.com/go-kod/grpc-gateway/pkg/header" 11 | "github.com/go-kod/kod" 12 | "github.com/go-kod/kod/interceptor" 13 | "github.com/go-kod/kod/interceptor/kcircuitbreaker" 14 | "golang.org/x/sync/singleflight" 15 | "google.golang.org/grpc/metadata" 16 | "google.golang.org/protobuf/proto" 17 | "google.golang.org/protobuf/reflect/protoreflect" 18 | ) 19 | 20 | type graphqlCaller struct { 21 | kod.Implements[GraphqlCaller] 22 | 23 | config kod.Ref[config.Config] 24 | registry kod.Ref[GraphqlCallerRegistry] 25 | 26 | singleflight singleflight.Group 27 | } 28 | 29 | func (c *graphqlCaller) Init(ctx context.Context) error { 30 | return nil 31 | } 32 | 33 | func (c *graphqlCaller) Call(ctx context.Context, rpc protoreflect.MethodDescriptor, message proto.Message) (proto.Message, error) { 34 | if c.config.Get().Config().Server.GraphQL.SingleFlight { 35 | if enable, ok := ctx.Value(allowSingleFlightKey).(bool); ok && enable { 36 | hash := Hash64.Get() 37 | defer Hash64.Put(hash) 38 | 39 | md, ok := metadata.FromOutgoingContext(ctx) 40 | if ok { 41 | hd := make([]string, 0, len(md)) 42 | for k, v := range md { 43 | // skip grpc gateway prefixed metadata 44 | if strings.Contains(k, header.MetadataPrefix) { 45 | continue 46 | } 47 | hd = append(hd, k+strings.Join(v, ",")) 48 | } 49 | slices.Sort(hd) 50 | for _, v := range hd { 51 | _, err := hash.Write([]byte(v)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | } 56 | } 57 | 58 | msg, err := proto.Marshal(message) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // generate hash based on rpc pointer 64 | _, err = hash.Write([]byte(rpc.FullName())) 65 | if err != nil { 66 | return nil, err 67 | } 68 | _, err = hash.Write(msg) 69 | if err != nil { 70 | return nil, err 71 | } 72 | sum := hash.Sum64() 73 | key := strconv.FormatUint(sum, 10) 74 | 75 | res, err, _ := c.singleflight.Do(key, func() (interface{}, error) { 76 | return c.registry.Get().GetCallerStub(string(rpc.Parent().FullName())).InvokeRpc(ctx, rpc, message) 77 | }) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | return res.(proto.Message), nil 83 | } 84 | } 85 | 86 | res, err := c.registry.Get().GetCallerStub(string(rpc.Parent().FullName())).InvokeRpc(ctx, rpc, message) 87 | return res, err 88 | } 89 | 90 | func (c *graphqlCaller) Interceptors() []interceptor.Interceptor { 91 | if c.config.Get().Config().Engine.CircuitBreaker { 92 | return []interceptor.Interceptor{ 93 | kcircuitbreaker.Interceptor(), 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | type allowSingleFlightType struct{} 100 | 101 | var allowSingleFlightKey allowSingleFlightType 102 | -------------------------------------------------------------------------------- /internal/server/graphql_caller_registry.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-kod/grpc-gateway/internal/config" 7 | "github.com/go-kod/grpc-gateway/pkg/protographql" 8 | "github.com/go-kod/kod" 9 | "github.com/go-kod/kod-ext/registry" 10 | "github.com/jhump/protoreflect/v2/grpcdynamic" 11 | "github.com/samber/lo" 12 | "github.com/vektah/gqlparser/v2/ast" 13 | "google.golang.org/grpc" 14 | "google.golang.org/protobuf/proto" 15 | "google.golang.org/protobuf/reflect/protoreflect" 16 | ) 17 | 18 | type graphqlCallerRegistry struct { 19 | kod.Implements[GraphqlCallerRegistry] 20 | config kod.Ref[config.Config] 21 | reflection kod.Ref[GraphqlReflection] 22 | 23 | serviceStub map[string]*grpcdynamic.Stub 24 | schema *protographql.SchemaDescriptor 25 | } 26 | 27 | func (c *graphqlCallerRegistry) Init(ctx context.Context) error { 28 | config := c.config.Get().Config().Grpc 29 | 30 | serviceStub := map[string]*grpcdynamic.Stub{} 31 | descs := make([]protoreflect.FileDescriptor, 0) 32 | descsconn := map[string]*grpc.ClientConn{} 33 | var etcd registry.Registry 34 | 35 | if len(c.config.Get().Config().Grpc.Etcd.Endpoints) > 0 { 36 | etcd = lo.Must(c.config.Get().Config().Grpc.Etcd.Build(ctx)) 37 | } 38 | 39 | for _, service := range config.Services { 40 | 41 | if etcd != nil { 42 | service = service.WithRegistry(etcd) 43 | } 44 | 45 | conn := service.Build() 46 | 47 | newDescs, err := c.reflection.Get().ListPackages(ctx, conn) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | for _, d := range newDescs { 53 | descsconn[string(d.FullName())] = conn 54 | } 55 | descs = append(descs, newDescs...) 56 | } 57 | 58 | for _, d := range descs { 59 | for i := 0; i < d.Services().Len(); i++ { 60 | svc := d.Services().Get(i) 61 | serviceStub[string(svc.FullName())] = grpcdynamic.NewStub(descsconn[string(d.FullName())]) 62 | } 63 | } 64 | 65 | descs = lo.UniqBy(descs, func(item protoreflect.FileDescriptor) string { 66 | return string(item.FullName()) 67 | }) 68 | 69 | c.serviceStub = serviceStub 70 | 71 | return c.setFileDescriptors(descs) 72 | } 73 | 74 | func (r *graphqlCallerRegistry) setFileDescriptors(files []protoreflect.FileDescriptor) error { 75 | schema := protographql.New() 76 | for _, file := range files { 77 | err := schema.RegisterFileDescriptor(r.config.Get().Config().Server.GraphQL.GenerateUnboundMethods, file) 78 | if err != nil { 79 | return err 80 | } 81 | } 82 | r.schema = schema 83 | 84 | return nil 85 | } 86 | 87 | func (r *graphqlCallerRegistry) FindMethodByName(op ast.Operation, name string) protoreflect.MethodDescriptor { 88 | return r.schema.MethodsByName[op][name] 89 | } 90 | 91 | func (r *graphqlCallerRegistry) GraphQLSchema() *ast.Schema { 92 | return r.schema.AsGraphQL() 93 | } 94 | 95 | func (r *graphqlCallerRegistry) Marshal(proto proto.Message, field *ast.Field) (interface{}, error) { 96 | return r.schema.Marshal(proto, field) 97 | } 98 | 99 | func (r *graphqlCallerRegistry) Unmarshal(desc protoreflect.MessageDescriptor, field *ast.Field, vars map[string]interface{}) (proto.Message, error) { 100 | return r.schema.Unmarshal(desc, field, vars) 101 | } 102 | 103 | func (r *graphqlCallerRegistry) GetCallerStub(service string) *grpcdynamic.Stub { 104 | return r.serviceStub[service] 105 | } 106 | -------------------------------------------------------------------------------- /internal/server/graphql_fetch.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/go-kod/kod" 10 | "github.com/jhump/protoreflect/v2/grpcreflect" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/reflection/grpc_reflection_v1" 13 | "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" 14 | "google.golang.org/grpc/status" 15 | "google.golang.org/protobuf/reflect/protoreflect" 16 | ) 17 | 18 | func isReflectionServiceName(name string) bool { 19 | return name == grpc_reflection_v1.ServerReflection_ServiceDesc.ServiceName || 20 | name == grpc_reflection_v1alpha.ServerReflection_ServiceDesc.ServiceName 21 | } 22 | 23 | var ErrTLSHandshakeFailed = errors.New("TLS handshake failed") 24 | 25 | type graphqlReflection struct { 26 | kod.Implements[GraphqlReflection] 27 | } 28 | 29 | func (ins *graphqlReflection) ListPackages(ctx context.Context, cc grpc.ClientConnInterface) ([]protoreflect.FileDescriptor, error) { 30 | client := grpcreflect.NewClientAuto(ctx, cc) 31 | ssvcs, err := client.ListServices() 32 | if err != nil { 33 | msg := status.Convert(err).Message() 34 | // Check whether the error message contains TLS related error. 35 | // If the server didn't enable TLS, the error message contains the first string. 36 | // If Evans didn't enable TLS against to the TLS enabled server, the error message contains 37 | // the second string. 38 | if strings.Contains(msg, "tls: first record does not look like a TLS handshake") || 39 | strings.Contains(msg, "latest connection error: ") { 40 | return nil, ErrTLSHandshakeFailed 41 | } 42 | return nil, fmt.Errorf("failed to list services from reflecton enabled gRPC server: %w", err) 43 | } 44 | 45 | var fds []protoreflect.FileDescriptor 46 | for _, s := range ssvcs { 47 | if isReflectionServiceName(string(s)) { 48 | continue 49 | } 50 | svc, err := client.AsResolver().FindDescriptorByName(s) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | fd := svc.ParentFile() //.AsFileDescriptorProto() 56 | fds = append(fds, fd) 57 | } 58 | return fds, nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/server/graphql_query.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | 8 | "github.com/go-kod/kod" 9 | "github.com/go-kod/kod/interceptor" 10 | "github.com/go-kod/kod/interceptor/kratelimit" 11 | "github.com/nautilus/graphql" 12 | "github.com/vektah/gqlparser/v2/ast" 13 | 14 | "github.com/go-kod/grpc-gateway/internal/config" 15 | ) 16 | 17 | type anyMap = map[string]interface{} 18 | 19 | type graphqlQueryer struct { 20 | kod.Implements[GraphqlQueryer] 21 | 22 | config kod.Ref[config.Config] 23 | caller kod.Ref[GraphqlCaller] 24 | registry kod.Ref[GraphqlCallerRegistry] 25 | } 26 | 27 | func (q *graphqlQueryer) Interceptors() []interceptor.Interceptor { 28 | if q.config.Get().Config().Engine.RateLimit { 29 | return []interceptor.Interceptor{ 30 | kratelimit.Interceptor(), 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func (q *graphqlQueryer) Query(ctx context.Context, input *graphql.QueryInput, result interface{}) error { 38 | res := map[string]interface{}{} 39 | var err error 40 | var selection ast.SelectionSet 41 | for _, op := range input.QueryDocument.Operations { 42 | selection, err = graphql.ApplyFragments(op.SelectionSet, input.QueryDocument.Fragments) 43 | if err != nil { 44 | return err 45 | } 46 | switch op.Operation { 47 | case ast.Query: 48 | // we allow single flight for queries right now 49 | ctx = context.WithValue(ctx, allowSingleFlightKey, true) 50 | err = q.resolveQuery(ctx, selection, res, input.Variables) 51 | 52 | case ast.Mutation: 53 | err = q.resolveMutation(ctx, selection, res, input.Variables) 54 | 55 | case ast.Subscription: 56 | return &graphql.Error{ 57 | Extensions: map[string]interface{}{"code": "UNIMPLEMENTED"}, 58 | Message: "subscription is not supported", 59 | } 60 | } 61 | } 62 | if err != nil { 63 | return &graphql.Error{ 64 | Extensions: map[string]interface{}{"code": "UNKNOWN_ERROR"}, 65 | Message: err.Error(), 66 | } 67 | } 68 | 69 | val := reflect.ValueOf(result) 70 | if val.Kind() != reflect.Ptr { 71 | return errors.New("result must be a pointer") 72 | } 73 | val = val.Elem() 74 | if !val.CanAddr() { 75 | return errors.New("result must be addressable (a pointer)") 76 | } 77 | val.Set(reflect.ValueOf(res)) 78 | return nil 79 | } 80 | 81 | func (q *graphqlQueryer) resolveMutation(ctx context.Context, selection ast.SelectionSet, res anyMap, vars map[string]interface{}) (err error) { 82 | for _, ss := range selection { 83 | field, ok := ss.(*ast.Field) 84 | if !ok { 85 | continue 86 | } 87 | if field.Name == "__typename" { 88 | res[nameOrAlias(field)] = field.ObjectDefinition.Name 89 | continue 90 | } 91 | res[nameOrAlias(field)], err = q.resolveCall(ctx, ast.Mutation, field, vars) 92 | if err != nil { 93 | return err 94 | } 95 | } 96 | return 97 | } 98 | 99 | func (q *graphqlQueryer) resolveQuery(ctx context.Context, selection ast.SelectionSet, res anyMap, vars map[string]interface{}) (err error) { 100 | type mapEntry struct { 101 | key string 102 | val interface{} 103 | } 104 | errCh := make(chan error, 4) 105 | resCh := make(chan mapEntry, 4) 106 | for _, ss := range selection { 107 | field, ok := ss.(*ast.Field) 108 | if !ok { 109 | continue 110 | } 111 | go func(field *ast.Field) { 112 | if field.Name == "__typename" { 113 | resCh <- mapEntry{ 114 | key: nameOrAlias(field), 115 | val: field.ObjectDefinition.Name, 116 | } 117 | return 118 | } 119 | resolvedValue, err := q.resolveCall(ctx, ast.Query, field, vars) 120 | if err != nil { 121 | errCh <- err 122 | return 123 | } 124 | resCh <- mapEntry{ 125 | key: nameOrAlias(field), 126 | val: resolvedValue, 127 | } 128 | }(field) 129 | } 130 | var errs graphql.ErrorList 131 | for i := 0; i < len(selection); i++ { 132 | select { 133 | case r := <-resCh: 134 | res[r.key] = r.val 135 | case err := <-errCh: 136 | errs = append(errs, err) 137 | case <-ctx.Done(): 138 | return ctx.Err() 139 | } 140 | } 141 | if len(errs) == 0 { 142 | return nil 143 | } 144 | return 145 | } 146 | 147 | func (q *graphqlQueryer) resolveCall(ctx context.Context, op ast.Operation, field *ast.Field, vars map[string]interface{}) (interface{}, error) { 148 | method := q.registry.Get().FindMethodByName(op, field.Name) 149 | if method == nil { 150 | return nil, errors.New("method not found") 151 | } 152 | 153 | inputMsg, err := q.registry.Get().Unmarshal(method.Input(), field, vars) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | msg, err := q.caller.Get().Call(ctx, method, inputMsg) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | return q.registry.Get().Marshal(msg, field) 164 | } 165 | 166 | func nameOrAlias(field *ast.Field) string { 167 | if field.Alias != "" { 168 | return field.Alias 169 | } 170 | 171 | return field.Name 172 | } 173 | -------------------------------------------------------------------------------- /internal/server/http_upstream_invoker.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/fullstorydev/grpcurl" 10 | "github.com/go-kod/grpc-gateway/internal/config" 11 | "github.com/go-kod/grpc-gateway/pkg/header" 12 | "github.com/go-kod/grpc-gateway/pkg/protojson" 13 | "github.com/go-kod/kod" 14 | "github.com/go-kod/kod/interceptor" 15 | "github.com/go-kod/kod/interceptor/kaccesslog" 16 | "github.com/go-kod/kod/interceptor/kmetric" 17 | "github.com/go-kod/kod/interceptor/ktrace" 18 | "golang.org/x/sync/singleflight" 19 | "google.golang.org/grpc/codes" 20 | "google.golang.org/grpc/status" 21 | ) 22 | 23 | type httpUpstreamInvoker struct { 24 | kod.Implements[HttpUpstreamInvoker] 25 | 26 | singleflight singleflight.Group 27 | config kod.Ref[config.Config] 28 | } 29 | 30 | func (i *httpUpstreamInvoker) Invoke(ctx context.Context, rw http.ResponseWriter, r *http.Request, upstream upstreamInfo, rpcPath string, pathNames []string) { 31 | eh := &protojson.EventHandler{} 32 | err := error(nil) 33 | 34 | if i.config.Get().Config().Server.HTTP.SingleFlight { 35 | if r.Method == http.MethodGet { 36 | hash := Hash64.Get() 37 | defer Hash64.Put(hash) 38 | 39 | _, _ = hash.WriteString(r.URL.String()) 40 | 41 | for k, v := range r.Header { 42 | _, _ = hash.WriteString(k) 43 | _, _ = hash.WriteString(strings.Join(v, ",")) 44 | } 45 | 46 | key := strconv.FormatUint(hash.Sum64(), 10) 47 | 48 | handler, err1, _ := i.singleflight.Do(key, func() (interface{}, error) { 49 | return i.invoke(ctx, rw, r, upstream, rpcPath, pathNames) 50 | }) 51 | 52 | eh = handler.(*protojson.EventHandler) 53 | err = err1 54 | } 55 | } else { 56 | eh, err = i.invoke(ctx, rw, r, upstream, rpcPath, pathNames) 57 | } 58 | 59 | if err != nil { 60 | http.Error(rw, err.Error(), codeFromGrpcError(err)) 61 | return 62 | } 63 | 64 | rw.Header().Set("Content-Type", "application/json; charset=utf-8") 65 | if eh.Status != nil { 66 | err = eh.Marshaler.Marshal(rw, eh.Status.Proto()) 67 | } else { 68 | err = eh.Marshaler.Marshal(rw, eh.Message) 69 | } 70 | 71 | if err != nil { 72 | i.L(ctx).Error("marshal response", "error", err) 73 | http.Error(rw, err.Error(), codeFromGrpcError(err)) 74 | } 75 | } 76 | 77 | func (i *httpUpstreamInvoker) invoke(ctx context.Context, 78 | rw http.ResponseWriter, r *http.Request, upstream upstreamInfo, rpcPath string, pathNames []string, 79 | ) (*protojson.EventHandler, error) { 80 | parser, err := protojson.NewRequestParser(r, pathNames, upstream.resovler) 81 | if err != nil { 82 | i.L(ctx).Error("parse request", "error", err) 83 | return nil, status.Error(codes.InvalidArgument, err.Error()) 84 | } 85 | 86 | handler := protojson.NewEventHandler(rw, upstream.resovler) 87 | 88 | err = grpcurl.InvokeRPC(ctx, upstream.source, upstream.conn, 89 | rpcPath, 90 | header.ProcessHeaders(r.Header), 91 | handler, parser.Next) 92 | if err != nil { 93 | i.L(ctx).Error("invoke rpc", "error", err) 94 | if handler.Status == nil { 95 | return nil, status.Error(codes.Internal, err.Error()) 96 | } 97 | } 98 | 99 | return handler, nil 100 | } 101 | 102 | func (*httpUpstreamInvoker) Interceptors() []interceptor.Interceptor { 103 | return []interceptor.Interceptor{ 104 | kaccesslog.Interceptor(), 105 | kmetric.Interceptor(), 106 | ktrace.Interceptor(), 107 | } 108 | } 109 | 110 | func codeFromGrpcError(err error) int { 111 | code := status.Code(err) 112 | switch code { 113 | case codes.OK: 114 | return http.StatusOK 115 | case codes.InvalidArgument, codes.FailedPrecondition, codes.OutOfRange: 116 | return http.StatusBadRequest 117 | case codes.Unauthenticated: 118 | return http.StatusUnauthorized 119 | case codes.PermissionDenied: 120 | return http.StatusForbidden 121 | case codes.NotFound: 122 | return http.StatusNotFound 123 | case codes.Canceled: 124 | return http.StatusRequestTimeout 125 | case codes.AlreadyExists, codes.Aborted: 126 | return http.StatusConflict 127 | case codes.ResourceExhausted: 128 | return http.StatusTooManyRequests 129 | case codes.Internal, codes.DataLoss, codes.Unknown: 130 | return http.StatusInternalServerError 131 | case codes.Unimplemented: 132 | return http.StatusNotImplemented 133 | case codes.Unavailable: 134 | return http.StatusServiceUnavailable 135 | case codes.DeadlineExceeded: 136 | return http.StatusGatewayTimeout 137 | } 138 | 139 | return http.StatusInternalServerError 140 | } 141 | -------------------------------------------------------------------------------- /internal/server/http_uptream.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/fullstorydev/grpcurl" 10 | "github.com/go-kod/grpc-gateway/internal/config" 11 | "github.com/go-kod/grpc-gateway/pkg/protojson" 12 | "github.com/go-kod/kod" 13 | "github.com/go-kod/kod-ext/client/kgrpc" 14 | "github.com/jhump/protoreflect/grpcreflect" 15 | 16 | // nolint 17 | "github.com/golang/protobuf/jsonpb" 18 | "github.com/samber/lo" 19 | "google.golang.org/grpc" 20 | ) 21 | 22 | type httpUpstream struct { 23 | kod.Implements[HttpUpstream] 24 | 25 | invoker kod.Ref[HttpUpstreamInvoker] 26 | config kod.Ref[config.Config] 27 | 28 | upstreams map[string]upstreamInfo 29 | } 30 | 31 | type upstreamInfo struct { 32 | target string 33 | conn grpc.ClientConnInterface 34 | source grpcurl.DescriptorSource 35 | resovler jsonpb.AnyResolver 36 | methods []protojson.Method 37 | } 38 | 39 | func (u *httpUpstream) Init(ctx context.Context) error { 40 | u.upstreams = make(map[string]upstreamInfo) 41 | 42 | lo.ForEach(u.config.Get().Config().Grpc.Services, func(item kgrpc.Config, index int) { 43 | registry := lo.Must(u.config.Get().Config().Grpc.Etcd.Build(ctx)) 44 | 45 | conn := item.WithRegistry(registry).Build() 46 | 47 | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 48 | defer cancel() 49 | 50 | client := grpcreflect.NewClientAuto(ctx, conn) 51 | 52 | source := grpcurl.DescriptorSourceFromServer(ctx, client) 53 | 54 | methods := lo.Must(protojson.GetMethods(source)) 55 | 56 | u.upstreams[item.Target] = upstreamInfo{ 57 | target: item.Target, 58 | conn: conn, 59 | source: source, 60 | resovler: grpcurl.AnyResolverFromDescriptorSource(source), 61 | methods: methods, 62 | } 63 | }) 64 | 65 | return nil 66 | } 67 | 68 | func (u *httpUpstream) Register(ctx context.Context, router *http.ServeMux) { 69 | for _, upstream := range u.upstreams { 70 | for _, v := range upstream.methods { 71 | if v.HttpPath == "" { 72 | continue 73 | } 74 | 75 | u.L(ctx).Info("register upstream", "http", v.HttpPath, "rpc", v.RpcPath) 76 | router.Handle(fmt.Sprintf("%s %s", v.HttpMethod, v.HttpPath), u.buildHandler(ctx, upstream, v.RpcPath, v.PathNames)) 77 | } 78 | } 79 | } 80 | 81 | func (u *httpUpstream) buildHandler(_ context.Context, upstream upstreamInfo, rpcPath string, pathNames []string) http.HandlerFunc { 82 | return func(rw http.ResponseWriter, r *http.Request) { 83 | u.invoker.Get().Invoke(r.Context(), rw, r, upstream, rpcPath, pathNames) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /internal/server/kod_gen_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate"; DO NOT EDIT. 2 | 3 | package server 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | 9 | "github.com/jhump/protoreflect/v2/grpcdynamic" 10 | "github.com/nautilus/graphql" 11 | "github.com/vektah/gqlparser/v2/ast" 12 | "google.golang.org/grpc" 13 | "google.golang.org/protobuf/proto" 14 | "google.golang.org/protobuf/reflect/protoreflect" 15 | ) 16 | 17 | // Gateway is implemented by [server], 18 | // which can be mocked with [NewMockGateway]. 19 | type Gateway interface { 20 | // BuildServer is implemented by [server.BuildServer] 21 | BuildServer() (http.Handler, error) 22 | // BuildHTTPServer is implemented by [server.BuildHTTPServer] 23 | BuildHTTPServer() (http.Handler, error) 24 | } 25 | 26 | // GraphqlCaller is implemented by [graphqlCaller], 27 | // which can be mocked with [NewMockGraphqlCaller]. 28 | type GraphqlCaller interface { 29 | // Call is implemented by [graphqlCaller.Call] 30 | Call(ctx context.Context, rpc protoreflect.MethodDescriptor, message proto.Message) (proto.Message, error) 31 | } 32 | 33 | // GraphqlCallerRegistry is implemented by [graphqlCallerRegistry], 34 | // which can be mocked with [NewMockGraphqlCallerRegistry]. 35 | type GraphqlCallerRegistry interface { 36 | // FindMethodByName is implemented by [graphqlCallerRegistry.FindMethodByName] 37 | FindMethodByName(op ast.Operation, name string) protoreflect.MethodDescriptor 38 | // GraphQLSchema is implemented by [graphqlCallerRegistry.GraphQLSchema] 39 | GraphQLSchema() *ast.Schema 40 | // Marshal is implemented by [graphqlCallerRegistry.Marshal] 41 | Marshal(proto proto.Message, field *ast.Field) (interface{}, error) 42 | // Unmarshal is implemented by [graphqlCallerRegistry.Unmarshal] 43 | Unmarshal(desc protoreflect.MessageDescriptor, field *ast.Field, vars map[string]interface{}) (proto.Message, error) 44 | // GetCallerStub is implemented by [graphqlCallerRegistry.GetCallerStub] 45 | GetCallerStub(service string) *grpcdynamic.Stub 46 | } 47 | 48 | // GraphqlReflection is implemented by [graphqlReflection], 49 | // which can be mocked with [NewMockGraphqlReflection]. 50 | type GraphqlReflection interface { 51 | // ListPackages is implemented by [graphqlReflection.ListPackages] 52 | ListPackages(ctx context.Context, cc grpc.ClientConnInterface) ([]protoreflect.FileDescriptor, error) 53 | } 54 | 55 | // GraphqlQueryer is implemented by [graphqlQueryer], 56 | // which can be mocked with [NewMockGraphqlQueryer]. 57 | type GraphqlQueryer interface { 58 | // Query is implemented by [graphqlQueryer.Query] 59 | Query(ctx context.Context, input *graphql.QueryInput, result interface{}) error 60 | } 61 | 62 | // HttpUpstreamInvoker is implemented by [httpUpstreamInvoker], 63 | // which can be mocked with [NewMockHttpUpstreamInvoker]. 64 | type HttpUpstreamInvoker interface { 65 | // Invoke is implemented by [httpUpstreamInvoker.Invoke] 66 | Invoke(ctx context.Context, rw http.ResponseWriter, r *http.Request, upstream upstreamInfo, rpcPath string, pathNames []string) 67 | } 68 | 69 | // HttpUpstream is implemented by [httpUpstream], 70 | // which can be mocked with [NewMockHttpUpstream]. 71 | type HttpUpstream interface { 72 | // Register is implemented by [httpUpstream.Register] 73 | Register(ctx context.Context, router *http.ServeMux) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/header/headerprocessor.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "net/http" 5 | "net/textproto" 6 | "strings" 7 | 8 | "google.golang.org/grpc/metadata" 9 | ) 10 | 11 | // ProcessHeaders builds the headers for the gateway from HTTP headers. 12 | func ProcessHeaders(header http.Header) []string { 13 | var headers []string 14 | 15 | for key := range header { 16 | _, ok := DefaultHeaderMatcher(key) 17 | if ok { 18 | headers = append(headers, key) 19 | } 20 | } 21 | 22 | return headers 23 | } 24 | 25 | // HttpHeadersToGRPCMetadata converts HTTP headers to gRPC metadata. 26 | func HttpHeadersToGRPCMetadata(headers http.Header) metadata.MD { 27 | grpcMetadata := metadata.MD{} 28 | for key, values := range headers { 29 | grpcKey, ok := DefaultHeaderMatcher(key) 30 | if ok { 31 | for _, value := range values { 32 | grpcMetadata.Append(grpcKey, value) 33 | } 34 | } 35 | } 36 | return grpcMetadata 37 | } 38 | 39 | const ( 40 | MetadataHeaderPrefix = "Grpc-Metadata-" 41 | MetadataPrefix = "grpcgateway-" 42 | ) 43 | 44 | func DefaultHeaderMatcher(key string) (string, bool) { 45 | switch key = textproto.CanonicalMIMEHeaderKey(key); { 46 | case isPermanentHTTPHeader(key): 47 | return MetadataPrefix + key, true 48 | case strings.HasPrefix(key, MetadataHeaderPrefix): 49 | return key[len(MetadataHeaderPrefix):], true 50 | } 51 | return "", false 52 | } 53 | 54 | // isPermanentHTTPHeader checks whether hdr belongs to the list of 55 | // permanent request headers maintained by IANA. 56 | // http://www.iana.org/assignments/message-headers/message-headers.xml 57 | func isPermanentHTTPHeader(hdr string) bool { 58 | switch hdr { 59 | case 60 | "Accept", 61 | "Accept-Charset", 62 | "Accept-Language", 63 | "Accept-Ranges", 64 | "Authorization", 65 | "Cache-Control", 66 | "Content-Type", 67 | "Cookie", 68 | "Date", 69 | "Expect", 70 | "From", 71 | "Host", 72 | "If-Match", 73 | "If-Modified-Since", 74 | "If-None-Match", 75 | "If-Schedule-Tag-Match", 76 | "If-Unmodified-Since", 77 | "Max-Forwards", 78 | "Origin", 79 | "Pragma", 80 | "Referer", 81 | "User-Agent", 82 | "Via", 83 | "Warning": 84 | return true 85 | } 86 | return false 87 | } 88 | -------------------------------------------------------------------------------- /pkg/protographql/marshal.go: -------------------------------------------------------------------------------- 1 | package protographql 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "maps" 7 | 8 | "github.com/vektah/gqlparser/v2/ast" 9 | "google.golang.org/protobuf/proto" 10 | "google.golang.org/protobuf/reflect/protoreflect" 11 | "google.golang.org/protobuf/types/known/anypb" 12 | ) 13 | 14 | // Marshal 将 gRPC 返回的 proto.Message 转换为 GraphQL 数据 15 | func (ins *SchemaDescriptor) Marshal(msg proto.Message, field *ast.Field) (interface{}, error) { 16 | message := msg.ProtoReflect() 17 | result := make(map[string]interface{}, message.Descriptor().Fields().Len()+1) 18 | 19 | if IsAny(msg.ProtoReflect().Descriptor()) { 20 | anyMsg, ok := msg.(*anypb.Any) 21 | if !ok { 22 | return nil, fmt.Errorf("failed to cast to Any type") 23 | } 24 | 25 | msgDesc, err := ins.ProtoTypes.FindMessageByURL(anyMsg.GetTypeUrl()) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | msgObj := ins.newMessage(msgDesc.Descriptor()).Interface() 31 | err = proto.UnmarshalOptions{}.Unmarshal(anyMsg.Value, msgObj) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return ins.marshalMessage(msgObj.ProtoReflect(), field, true) 37 | } 38 | 39 | for _, selection := range field.SelectionSet { 40 | if f, ok := selection.(*ast.Field); ok { 41 | fieldName := f.Name 42 | if fieldName == "__typename" { 43 | result["__typename"] = string(message.Descriptor().Name()) 44 | continue 45 | } 46 | 47 | fieldDesc := message.Descriptor().Fields().ByJSONName(fieldName) 48 | if fieldDesc == nil { 49 | oneofMap, err := ins.marshalOneof(message, f, false) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | result[fieldName] = oneofMap 55 | continue 56 | } 57 | value := message.Get(fieldDesc) 58 | marshaldValue, err := ins.marshalValue(value, fieldDesc, f, false) 59 | if err != nil { 60 | return nil, err 61 | } 62 | result[fieldName] = marshaldValue 63 | } 64 | } 65 | 66 | return result, nil 67 | } 68 | 69 | // marshalValue 将 protoreflect.Value 转换为适合 GraphQL 的值 70 | func (ins *SchemaDescriptor) marshalValue(value protoreflect.Value, fieldDesc protoreflect.FieldDescriptor, field *ast.Field, allField bool) (interface{}, error) { 71 | if fieldDesc.IsList() { 72 | return ins.marshalList(value.List(), fieldDesc, field, allField) 73 | } else if fieldDesc.IsMap() { 74 | return ins.marshalMap(value.Map(), fieldDesc, field, allField) 75 | } 76 | 77 | return ins.marshalScalar(value, fieldDesc, field, allField) 78 | } 79 | 80 | // marshalOneof 将 oneof 类型字段转换为 map[string]interface{} 81 | func (ins *SchemaDescriptor) marshalOneof(message protoreflect.Message, field *ast.Field, allField bool) (map[string]interface{}, error) { 82 | result := make(map[string]interface{}, message.Descriptor().Oneofs().Len()+1) 83 | oneofDesc := message.Descriptor().Oneofs().ByName(protoreflect.Name(field.Name)) 84 | if oneofDesc == nil { 85 | return result, nil 86 | } 87 | 88 | fieldDesc := message.WhichOneof(oneofDesc) 89 | if fieldDesc == nil { 90 | return result, nil 91 | } 92 | 93 | value := message.Get(fieldDesc) 94 | 95 | for _, selects := range field.SelectionSet { 96 | if f, ok := selects.(*ast.Field); ok { 97 | if f.Name == "__typename" { 98 | result["__typename"] = field.Definition.Type.NamedType 99 | continue 100 | } 101 | 102 | if string(fieldDesc.Name()) != f.Name { 103 | continue 104 | } 105 | 106 | marshaldValue, err := ins.marshalValue(value, fieldDesc, f, allField) 107 | if err != nil { 108 | return nil, err 109 | } 110 | result[f.Name] = marshaldValue 111 | } 112 | } 113 | 114 | return result, nil 115 | } 116 | 117 | func (ins *SchemaDescriptor) marshalScalar(value protoreflect.Value, fieldDesc protoreflect.FieldDescriptor, field *ast.Field, allField bool) (interface{}, error) { 118 | switch fieldDesc.Kind() { 119 | case protoreflect.BoolKind: 120 | return value.Bool(), nil 121 | case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: 122 | return int32(value.Int()), nil 123 | case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: 124 | return uint32(value.Uint()), nil 125 | case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: 126 | return value.Int(), nil 127 | case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: 128 | return value.Uint(), nil 129 | case protoreflect.FloatKind: 130 | return float32(value.Float()), nil 131 | case protoreflect.DoubleKind: 132 | return value.Float(), nil 133 | case protoreflect.StringKind: 134 | return value.String(), nil 135 | case protoreflect.BytesKind: 136 | return base64.StdEncoding.EncodeToString(value.Bytes()), nil 137 | case protoreflect.EnumKind: 138 | return string(fieldDesc.Enum().Values().ByNumber(value.Enum()).Name()), nil 139 | case protoreflect.MessageKind, protoreflect.GroupKind: 140 | if fieldDesc.IsMap() { 141 | return ins.marshalMap(value.Map(), fieldDesc, field, allField) 142 | } 143 | 144 | return ins.marshalMessage(value.Message(), field, allField) 145 | default: 146 | return nil, fmt.Errorf("unsupported field type: %v", fieldDesc.Kind()) 147 | } 148 | } 149 | 150 | func (ins *SchemaDescriptor) getSelectionSetFromMessage(message protoreflect.Message) ast.SelectionSet { 151 | selectionSet := make(ast.SelectionSet, 0, message.Descriptor().Fields().Len()+1) 152 | 153 | for i := 0; i < message.Descriptor().Fields().Len(); i++ { 154 | field := message.Descriptor().Fields().Get(i) 155 | if field.Kind() == protoreflect.MessageKind && IsEmpty(field.Message()) { 156 | continue 157 | } 158 | 159 | selectionSet = append(selectionSet, &ast.Field{ 160 | Name: field.JSONName(), 161 | }) 162 | } 163 | 164 | if len(selectionSet) > 0 { 165 | selectionSet = append(selectionSet, &ast.Field{ 166 | Name: "__typename", 167 | }) 168 | } 169 | 170 | return selectionSet 171 | } 172 | 173 | func (ins *SchemaDescriptor) marshalMessage(message protoreflect.Message, field *ast.Field, allField bool) (interface{}, error) { 174 | if allField { 175 | field.SelectionSet = ins.getSelectionSetFromMessage(message) 176 | } 177 | 178 | result := make(map[string]interface{}, message.Descriptor().Fields().Len()+1) 179 | for _, selection := range field.SelectionSet { 180 | if f, ok := selection.(*ast.Field); ok { 181 | fieldName := f.Alias 182 | if fieldName == "" { 183 | fieldName = f.Name 184 | } 185 | if fieldName == "__typename" { 186 | result[fieldName] = string(message.Descriptor().Name()) 187 | continue 188 | } 189 | 190 | fieldDesc := message.Descriptor().Fields().ByJSONName(fieldName) 191 | if fieldDesc == nil { 192 | oneofField, err := ins.marshalOneof(message, f, allField) 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | maps.Copy(result, oneofField) 198 | continue 199 | } 200 | 201 | value := message.Get(fieldDesc) 202 | marshaldValue, err := ins.marshalValue(value, fieldDesc, f, allField) 203 | if err != nil { 204 | return nil, err 205 | } 206 | result[fieldName] = marshaldValue 207 | } 208 | } 209 | return result, nil 210 | } 211 | 212 | // marshalList 将 protoreflect.List 转换为 []interface{} 213 | func (ins *SchemaDescriptor) marshalList(list protoreflect.List, fieldDesc protoreflect.FieldDescriptor, field *ast.Field, allField bool) (interface{}, error) { 214 | result := make([]interface{}, list.Len()) 215 | for i := 0; i < list.Len(); i++ { 216 | value := list.Get(i) 217 | var marshaldValue interface{} 218 | var err error 219 | 220 | if fieldDesc.Message() != nil { 221 | marshaldValue, err = ins.marshalMessage(value.Message(), field, allField) 222 | } else { 223 | marshaldValue, err = ins.marshalScalar(value, fieldDesc, field, allField) 224 | } 225 | 226 | if err != nil { 227 | return nil, err 228 | } 229 | result[i] = marshaldValue 230 | } 231 | return result, nil 232 | } 233 | 234 | func (ins *SchemaDescriptor) marshalMap(m protoreflect.Map, fieldDesc protoreflect.FieldDescriptor, field *ast.Field, allField bool) (interface{}, error) { 235 | result := make([]map[string]interface{}, 0) 236 | m.Range(func(mapKey protoreflect.MapKey, mapValue protoreflect.Value) bool { 237 | var valueField *ast.Field 238 | for _, selection := range field.SelectionSet { 239 | if f, ok := selection.(*ast.Field); ok { 240 | if f.Name == "value" { 241 | valueField = f 242 | } 243 | } 244 | } 245 | 246 | marshaldValue, err := ins.marshalValue(mapValue, fieldDesc.MapValue(), valueField, allField) 247 | if err != nil { 248 | return false 249 | } 250 | result = append(result, map[string]interface{}{"key": mapKey.Interface(), "value": marshaldValue}) 251 | return true 252 | }) 253 | return result, nil 254 | } 255 | -------------------------------------------------------------------------------- /pkg/protographql/marshal_test.go: -------------------------------------------------------------------------------- 1 | package protographql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-kod/grpc-gateway/api/test" 7 | "github.com/stretchr/testify/require" 8 | "github.com/vektah/gqlparser/v2/ast" 9 | ) 10 | 11 | func TestDecodeMaps(t *testing.T) { 12 | ins := New() 13 | err := ins.RegisterFileDescriptor(true, test.File_test_constructs_input_proto) 14 | require.Nil(t, err) 15 | 16 | msg := &test.Maps{} 17 | 18 | generated, err := ins.Marshal(msg, &ast.Field{ 19 | Alias: "constructsMaps_", 20 | Name: "constructsMaps_", 21 | SelectionSet: ast.SelectionSet{}, 22 | }) 23 | 24 | require.Nil(t, err) 25 | require.NotNil(t, generated) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/protographql/query_test.go: -------------------------------------------------------------------------------- 1 | package protographql 2 | 3 | type ( 4 | j = map[string]interface{} 5 | jj = []map[string]interface{} 6 | l = []interface{} 7 | ) 8 | 9 | const constructsScalarsQuery = ` 10 | mutation { 11 | constructsScalars_(in: { 12 | double: 1.1, float: 2.2, int32: 3, int64: -4, 13 | uint32: 5, uint64: 6, sint32: 7, sint64: 8, 14 | fixed32: 9, fixed64: 10, sfixed32: 11, sfixed64: 12, 15 | bool: true, stringX: "test", bytes: "dGVzdA==" 16 | }) { 17 | double 18 | float 19 | int32 20 | int64 21 | uint32 22 | uint64 23 | sint32 24 | sint64 25 | fixed32 26 | fixed64 27 | sfixed32 28 | sfixed64 29 | bool 30 | stringX 31 | bytes 32 | __typename 33 | } 34 | } 35 | ` 36 | 37 | var constructsScalarsResponse = j{ 38 | "__typename": "Scalars", 39 | "bool": true, 40 | "bytes": "dGVzdA==", 41 | "stringX": "test", 42 | "double": 1.1, 43 | "fixed32": uint32(9), 44 | "fixed64": uint64(10), 45 | "float": float32(2.2), 46 | "int32": int32(3), 47 | "int64": int64(-4), 48 | "sfixed32": int32(11), 49 | "sfixed64": int64(12), 50 | "sint32": int32(7), 51 | "sint64": int64(8), 52 | "uint32": uint32(5), 53 | "uint64": uint64(6), 54 | } 55 | 56 | const constructsMapsQuery = ` 57 | mutation { 58 | constructsMaps_( 59 | in: { 60 | int32Int32: [{ key: 1, value: 1 }] 61 | int64Int64: [{ key: 2, value: 2 }] 62 | uint32Uint32: [{ key: 3, value: 3 }] 63 | uint64Uint64: [{ key: 4, value: 4 }] 64 | sint32Sint32: [{ key: 5, value: 5 }] 65 | sint64Sint64: [{ key: 6, value: 5 }] 66 | fixed32Fixed32: [{ key: 7, value: 7 }] 67 | fixed64Fixed64: [{ key: 8, value: 8 }] 68 | sfixed32Sfixed32: [{ key: 9, value: 9 }] 69 | sfixed64Sfixed64: [{ key: 10, value: 10 }] 70 | boolBool: [{ key: true, value: true }] 71 | stringString: [{ key: "test1", value: "test1" }] 72 | stringBytes: [{ key: "test2", value: "dGVzdA==" }] 73 | stringFloat: [{ key: "test1", value: 11.1 }] 74 | stringDouble: [{ key: "test1", value: 12.2 }] 75 | stringFoo: [{ key: "test1", value: { param1: "param1", param2: "param2" } }] 76 | stringBar: [{ key: "test1", value: BAR3 }] 77 | } 78 | ) { 79 | int32Int32 { key value } 80 | int64Int64 { key value} 81 | uint32Uint32 { key value } 82 | uint64Uint64 { key value} 83 | sint32Sint32 { key value} 84 | sint64Sint64 { key value} 85 | fixed32Fixed32 { key value} 86 | fixed64Fixed64 { key value} 87 | sfixed32Sfixed32 { key value} 88 | sfixed64Sfixed64 { key value} 89 | boolBool { key value} 90 | stringString { key value} 91 | stringBytes { key value} 92 | stringFloat { key value} 93 | stringDouble { key value} 94 | stringFoo { key value { param1 param2 }} 95 | stringBar { key value} 96 | } 97 | } 98 | ` 99 | 100 | var constructsMapsResponse = j{ 101 | "boolBool": jj{j{"key": true, "value": true}}, 102 | "stringBar": jj{j{"key": "test1", "value": "BAR3"}}, 103 | "stringBytes": jj{j{"key": "test2", "value": "dGVzdA=="}}, 104 | "stringDouble": jj{j{"key": "test1", "value": 12.2}}, 105 | "stringFloat": jj{j{"key": "test1", "value": float32(11.1)}}, 106 | "stringFoo": jj{j{"key": "test1", "value": j{"param1": "param1", "param2": "param2"}}}, 107 | "stringString": jj{j{"key": "test1", "value": "test1"}}, 108 | "fixed32Fixed32": jj{j{"key": uint32(7), "value": uint32(7)}}, 109 | "fixed64Fixed64": jj{j{"key": uint64(8), "value": uint64(8)}}, 110 | "int32Int32": jj{j{"key": int32(1), "value": int32(1)}}, 111 | "int64Int64": jj{j{"key": int64(2), "value": int64(2)}}, 112 | "sfixed32Sfixed32": jj{j{"key": int32(9), "value": int32(9)}}, 113 | "sfixed64Sfixed64": jj{j{"key": int64(10), "value": int64(10)}}, 114 | "sint32Sint32": jj{j{"key": int32(5), "value": int32(5)}}, 115 | "sint64Sint64": jj{j{"key": int64(6), "value": int64(5)}}, 116 | "uint32Uint32": jj{j{"key": uint32(3), "value": uint32(3)}}, 117 | "uint64Uint64": jj{j{"key": uint64(4), "value": uint64(4)}}, 118 | } 119 | 120 | const constructsRepeatedQuery = ` 121 | mutation { 122 | constructsRepeated_( 123 | in: { 124 | double: [1.1] 125 | float: [2.2] 126 | int32: [3] 127 | int64: [4] 128 | uint32: [7] 129 | uint64: [8] 130 | sint32: [9] 131 | sint64: [10] 132 | fixed32: [11] 133 | fixed64: [12] 134 | sfixed32: [13] 135 | sfixed64: [14] 136 | bool: [true] 137 | stringX: ["test"] 138 | bytes: ["dGVzdA=="] 139 | foo: [{ param1: "param1", param2: "param2" }] 140 | bar: [BAR3] 141 | } 142 | ) { 143 | double 144 | float 145 | int32 146 | int64 147 | uint32 148 | uint64 149 | sint32 150 | sint64 151 | fixed32 152 | fixed64 153 | sfixed32 154 | sfixed64 155 | bool 156 | stringX 157 | bytes 158 | foo {param1 param2} 159 | bar 160 | } 161 | } 162 | ` 163 | 164 | var constructsRepeatedResponse = j{ 165 | "bar": l{"BAR3"}, 166 | "bool": l{true}, 167 | "stringX": l{"test"}, 168 | "bytes": l{"dGVzdA=="}, 169 | "foo": l{j{"param1": "param1", "param2": "param2"}}, 170 | "double": l{1.1}, 171 | "float": l{float32(2.2)}, 172 | "fixed32": l{uint32(11)}, 173 | "fixed64": l{uint64(12)}, 174 | "int32": l{int32(3)}, 175 | "int64": l{int64(4)}, 176 | "sfixed32": l{int32(13)}, 177 | "sfixed64": l{int64(14)}, 178 | "sint32": l{int32(9)}, 179 | "sint64": l{int64(10)}, 180 | "uint32": l{uint32(7)}, 181 | "uint64": l{uint64(8)}, 182 | } 183 | 184 | var constructsOneofsResponse = j{ 185 | "Oneof1": j{ 186 | "__typename": "constructs_Oneof_Oneof1", 187 | "param3": "3", 188 | }, 189 | "Oneof2": j{ 190 | "__typename": "constructs_Oneof_Oneof2", 191 | "param5": "5", 192 | }, 193 | "Oneof3": j{ 194 | "__typename": "constructs_Oneof_Oneof3", 195 | "param6": "6", 196 | }, 197 | "param1": "2", 198 | } 199 | 200 | const constructsOneofsQuery = ` 201 | mutation { 202 | constructsOneof_( 203 | in: {param1: "2", param3:"3", param5: "5", param6: "6"} 204 | ){ 205 | param1 206 | Oneof1 { 207 | __typename 208 | ... on constructs_Oneof_param2 { 209 | param2 210 | } 211 | ... on constructs_Oneof_param3 { 212 | param3 213 | } 214 | } 215 | Oneof2 { 216 | __typename 217 | ... on constructs_Oneof_param4 { 218 | param4 219 | } 220 | ... on constructs_Oneof_param5 { 221 | param5 222 | } 223 | } 224 | Oneof3 { 225 | __typename 226 | ... on constructs_Oneof_param6 { 227 | param6 228 | } 229 | } 230 | } 231 | } 232 | ` 233 | 234 | const constructsAnyQuery = ` 235 | mutation { 236 | constructsAny_(in: {__typename: "Ref", localTime2: {time: "1234"}, fileEnum: BAR2, local: { 237 | bar1: {param1: "param1"}, en1: A1, externalTime1: {seconds:1123, nanos: 123} 238 | }}) 239 | } 240 | ` 241 | 242 | var constructsAnyResponse = j{ 243 | "__typename": "Ref", 244 | "en1": "A0", 245 | "en2": "A0", 246 | "external": j{ 247 | "__typename": "Timestamp", 248 | "nanos": int32(0), 249 | "seconds": int64(0), 250 | }, 251 | "file": j{ 252 | "__typename": "Baz", 253 | "param1": "", 254 | }, 255 | "fileEnum": "BAR2", 256 | "fileMsg": j{ 257 | "__typename": "Foo", 258 | "param1": "", 259 | "param2": "", 260 | }, 261 | "foreign": j{ 262 | "__typename": "Foo2", 263 | "param1": "", 264 | }, 265 | "gz": j{ 266 | "__typename": "Gz", 267 | "param1": "", 268 | }, 269 | "local": j{ 270 | "__typename": "Foo", 271 | "bar1": j{ 272 | "__typename": "Bar", 273 | "param1": "param1", 274 | }, 275 | "bar2": j{ 276 | "__typename": "Bar", 277 | "param1": "", 278 | }, 279 | "en1": "A1", 280 | "en2": "A0", 281 | "externalTime1": j{ 282 | "__typename": "Timestamp", 283 | "nanos": int32(123), 284 | "seconds": int64(1123), 285 | }, 286 | "localTime2": j{ 287 | "__typename": "Timestamp", 288 | "time": "", 289 | }, 290 | }, 291 | "localTime": j{ 292 | "__typename": "Timestamp", 293 | "time": "", 294 | }, 295 | "localTime2": j{ 296 | "__typename": "Timestamp", 297 | "time": "1234", 298 | }, 299 | } 300 | -------------------------------------------------------------------------------- /pkg/protographql/schema_test.go: -------------------------------------------------------------------------------- 1 | package protographql 2 | 3 | import ( 4 | _ "embed" 5 | "os" 6 | "testing" 7 | 8 | "github.com/go-kod/grpc-gateway/api/test" 9 | "github.com/stretchr/testify/require" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | "github.com/vektah/gqlparser/v2/formatter" 12 | "github.com/vektah/gqlparser/v2/validator" 13 | ) 14 | 15 | //go:embed schema.graphql 16 | var schemaGraphQL string 17 | 18 | func TestSchema(t *testing.T) { 19 | ins := New() 20 | var err error 21 | err = ins.RegisterFileDescriptor(true, test.File_test_constructs_input_proto) 22 | require.Nil(t, err) 23 | err = ins.RegisterFileDescriptor(true, test.File_test_options_input_proto) 24 | require.Nil(t, err) 25 | 26 | schema := ins.AsGraphQL() 27 | file, err := os.Create("schema.graphql") 28 | require.Nil(t, err) 29 | formatter.NewFormatter(file).FormatSchema(schema) 30 | 31 | _, err = validator.LoadSchema(validator.Prelude, &ast.Source{Input: schemaGraphQL}) 32 | require.Nil(t, err) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/protographql/unmarshal_test.go: -------------------------------------------------------------------------------- 1 | package protographql 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-kod/grpc-gateway/api/test" 7 | "github.com/nautilus/graphql" 8 | "github.com/stretchr/testify/require" 9 | "github.com/vektah/gqlparser/v2" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | "google.golang.org/protobuf/proto" 12 | "google.golang.org/protobuf/types/known/anypb" 13 | ) 14 | 15 | func TestEncodeMaps(t *testing.T) { 16 | schema, err := gqlparser.LoadSchema(&ast.Source{ 17 | Input: schemaGraphQL, 18 | }) 19 | require.Nil(t, err) 20 | var msg test.Maps 21 | 22 | queryDoc, err := gqlparser.LoadQuery(schema, constructsMapsQuery) 23 | require.Nil(t, err) 24 | 25 | ins := New() 26 | err = ins.RegisterFileDescriptor(true, test.File_test_constructs_input_proto) 27 | require.Nil(t, err) 28 | 29 | generated, err := ins.Unmarshal(msg.ProtoReflect().Descriptor(), queryDoc.Operations[0].SelectionSet[0].(*ast.Field), nil) 30 | 31 | require.Nil(t, err) 32 | 33 | data, err := ins.Marshal(generated, queryDoc.Operations[0].SelectionSet[0].(*ast.Field)) 34 | require.Nil(t, err) 35 | 36 | require.Equal(t, constructsMapsResponse, data) 37 | } 38 | 39 | func TestEncodeRepeated(t *testing.T) { 40 | schema, err := gqlparser.LoadSchema(&ast.Source{ 41 | Input: schemaGraphQL, 42 | }) 43 | require.Nil(t, err) 44 | var msg test.Repeated 45 | 46 | queryDoc, err := gqlparser.LoadQuery(schema, constructsRepeatedQuery) 47 | require.Nil(t, err, err.Error()) 48 | 49 | ins := New() 50 | err = ins.RegisterFileDescriptor(true, test.File_test_constructs_input_proto) 51 | require.Nil(t, err) 52 | generated, err := ins.Unmarshal(msg.ProtoReflect().Descriptor(), queryDoc.Operations[0].SelectionSet[0].(*ast.Field), nil) 53 | 54 | require.Nil(t, err) 55 | 56 | data, err := ins.Marshal(generated, queryDoc.Operations[0].SelectionSet[0].(*ast.Field)) 57 | require.Nil(t, err) 58 | 59 | require.Equal(t, constructsRepeatedResponse, data) 60 | } 61 | 62 | func TestEncodeDecode(t *testing.T) { 63 | _ = test.Ref{} 64 | cases := []struct { 65 | name string 66 | query string 67 | wantResponse interface{} 68 | msg proto.Message 69 | }{ 70 | { 71 | name: "encode decode any", 72 | query: constructsAnyQuery, 73 | wantResponse: constructsAnyResponse, 74 | msg: &anypb.Any{}, 75 | }, 76 | { 77 | name: "encode decode scalar", 78 | query: constructsScalarsQuery, 79 | wantResponse: constructsScalarsResponse, 80 | msg: &test.Scalars{}, 81 | }, 82 | { 83 | name: "encode decode maps", 84 | query: constructsMapsQuery, 85 | wantResponse: constructsMapsResponse, 86 | msg: &test.Maps{}, 87 | }, 88 | { 89 | name: "encode decode repeated", 90 | query: constructsRepeatedQuery, 91 | wantResponse: constructsRepeatedResponse, 92 | msg: &test.Repeated{}, 93 | }, 94 | { 95 | name: "encode decode oneof", 96 | query: constructsOneofsQuery, 97 | wantResponse: constructsOneofsResponse, 98 | msg: &test.Oneof{}, 99 | }, 100 | } 101 | 102 | for _, c := range cases { 103 | t.Run(c.name, func(t *testing.T) { 104 | schema, err := gqlparser.LoadSchema(&ast.Source{ 105 | Input: schemaGraphQL, 106 | }) 107 | require.Nil(t, err, err) 108 | 109 | queryDoc, err := gqlparser.LoadQuery(schema, c.query) 110 | require.Nil(t, err, err) 111 | 112 | ins := New() 113 | err = ins.RegisterFileDescriptor(true, test.File_test_constructs_input_proto) 114 | require.Nil(t, err) 115 | 116 | selection, err := graphql.ApplyFragments(queryDoc.Operations[0].SelectionSet, queryDoc.Fragments) 117 | require.Nil(t, err, err) 118 | 119 | generated, err := ins.Unmarshal(c.msg.ProtoReflect().Descriptor(), selection[0].(*ast.Field), nil) 120 | 121 | require.Nil(t, err) 122 | 123 | data, err := ins.Marshal(generated, selection[0].(*ast.Field)) 124 | require.Nil(t, err) 125 | 126 | require.Equal(t, c.wantResponse, data) 127 | }) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /pkg/protographql/util.go: -------------------------------------------------------------------------------- 1 | package protographql 2 | 3 | import ( 4 | "unicode" 5 | 6 | "google.golang.org/protobuf/proto" 7 | "google.golang.org/protobuf/reflect/protoreflect" 8 | "google.golang.org/protobuf/types/known/anypb" 9 | 10 | graphqlv1 "github.com/go-kod/grpc-gateway/api/graphql/v1" 11 | ) 12 | 13 | func GraphqlMethodOptions(opts proto.Message) *graphqlv1.Rpc { 14 | if opts != nil { 15 | v := proto.GetExtension(opts, graphqlv1.E_Rpc) 16 | if v != nil { 17 | return v.(*graphqlv1.Rpc) 18 | } 19 | } 20 | return nil 21 | } 22 | 23 | func GraphqlFieldOptions(opts proto.Message) *graphqlv1.Field { 24 | if opts != nil { 25 | v := proto.GetExtension(opts, graphqlv1.E_Field) 26 | if v != nil { 27 | return v.(*graphqlv1.Field) 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | func GraphqlOneofOptions(opts proto.Message) *graphqlv1.Oneof { 34 | if opts != nil { 35 | v := proto.GetExtension(opts, graphqlv1.E_Oneof) 36 | if v != nil && v.(*graphqlv1.Oneof) != nil { 37 | return v.(*graphqlv1.Oneof) 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func GetRequestOperation(rpcOpts *graphqlv1.Rpc) string { 44 | if rpcOpts != nil { 45 | switch pattern := rpcOpts.GetPattern().(type) { 46 | case *graphqlv1.Rpc_Query: 47 | return pattern.Query 48 | case *graphqlv1.Rpc_Mutation: 49 | return pattern.Mutation 50 | } 51 | } 52 | 53 | return "" 54 | } 55 | 56 | func ToLowerFirst(s string) string { 57 | if len(s) > 0 { 58 | return string(unicode.ToLower(rune(s[0]))) + s[1:] 59 | } 60 | return "" 61 | } 62 | 63 | // same isEmpty but for mortals 64 | func IsEmpty(o protoreflect.MessageDescriptor) bool { return isEmpty(o, NewCallstack()) } 65 | 66 | // make sure objects are fulled with all objects 67 | func isEmpty(o protoreflect.MessageDescriptor, callstack Callstack) bool { 68 | callstack.Push(o) 69 | defer callstack.Pop(o) 70 | 71 | if o.Fields().Len() == 0 { 72 | return true 73 | } 74 | for i := 0; i < o.Fields().Len(); i++ { 75 | f := o.Fields().Get(i) 76 | objType := f.Message() 77 | if objType == nil { 78 | return false 79 | } 80 | 81 | // check if the call stack already contains a reference to this type and prevent it from calling itself again 82 | if callstack.Has(objType) { 83 | return true 84 | } 85 | if !isEmpty(objType, callstack) { 86 | return false 87 | } 88 | } 89 | 90 | return true 91 | } 92 | 93 | type Callstack interface { 94 | Push(entry interface{}) 95 | Pop(entry interface{}) 96 | Has(entry interface{}) bool 97 | } 98 | 99 | func NewCallstack() Callstack { 100 | return &callstack{stack: make(map[interface{}]int), index: 0} 101 | } 102 | 103 | type callstack struct { 104 | stack map[interface{}]int 105 | index int 106 | } 107 | 108 | func (c *callstack) Pop(entry interface{}) { 109 | delete(c.stack, entry) 110 | c.index-- 111 | } 112 | 113 | func (c *callstack) Push(entry interface{}) { 114 | c.stack[entry] = c.index 115 | c.index++ 116 | } 117 | 118 | func (c *callstack) Has(entry interface{}) bool { 119 | _, ok := c.stack[entry] 120 | return ok 121 | } 122 | 123 | func IsAny(o protoreflect.MessageDescriptor) bool { 124 | return o.FullName() == "google.protobuf.Any" 125 | } 126 | 127 | func marshalAny(inputMsg proto.Message) (*anypb.Any, error) { 128 | b, err := proto.Marshal(inputMsg) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | return &anypb.Any{TypeUrl: "type.googleapis.com/" + string(inputMsg.ProtoReflect().Descriptor().FullName()), Value: b}, nil 134 | } 135 | -------------------------------------------------------------------------------- /pkg/protojson/descriptorsource.go: -------------------------------------------------------------------------------- 1 | package protojson 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "regexp" 7 | 8 | "github.com/fullstorydev/grpcurl" 9 | // nolint 10 | "github.com/jhump/protoreflect/desc" 11 | "google.golang.org/genproto/googleapis/api/annotations" 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | type Method struct { 16 | HttpMethod string 17 | PathNames []string 18 | HttpPath string 19 | RpcPath string 20 | } 21 | 22 | // GetMethods returns all methods of the given grpcurl.DescriptorSource. 23 | func GetMethods(source grpcurl.DescriptorSource) ([]Method, error) { 24 | svcs, err := source.ListServices() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | var methods []Method 30 | for _, svc := range svcs { 31 | d, err := source.FindSymbol(svc) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | switch val := d.(type) { 37 | case *desc.ServiceDescriptor: 38 | svcMethods := val.GetMethods() 39 | for _, method := range svcMethods { 40 | rpcPath := fmt.Sprintf("%s/%s", svc, method.GetName()) 41 | ext := proto.GetExtension(method.GetMethodOptions(), annotations.E_Http) 42 | switch rule := ext.(type) { 43 | case *annotations.HttpRule: 44 | if rule == nil { 45 | methods = append(methods, Method{ 46 | RpcPath: rpcPath, 47 | }) 48 | continue 49 | } 50 | 51 | switch httpRule := rule.GetPattern().(type) { 52 | case *annotations.HttpRule_Get: 53 | methods = append(methods, Method{ 54 | HttpMethod: http.MethodGet, 55 | PathNames: extractFieldNames(httpRule.Get), 56 | HttpPath: adjustHttpPath(httpRule.Get), 57 | RpcPath: rpcPath, 58 | }) 59 | case *annotations.HttpRule_Post: 60 | methods = append(methods, Method{ 61 | HttpMethod: http.MethodPost, 62 | PathNames: extractFieldNames(httpRule.Post), 63 | HttpPath: adjustHttpPath(httpRule.Post), 64 | RpcPath: rpcPath, 65 | }) 66 | case *annotations.HttpRule_Put: 67 | methods = append(methods, Method{ 68 | HttpMethod: http.MethodPut, 69 | PathNames: extractFieldNames(httpRule.Put), 70 | HttpPath: adjustHttpPath(httpRule.Put), 71 | RpcPath: rpcPath, 72 | }) 73 | case *annotations.HttpRule_Delete: 74 | methods = append(methods, Method{ 75 | HttpMethod: http.MethodDelete, 76 | PathNames: extractFieldNames(httpRule.Delete), 77 | HttpPath: adjustHttpPath(httpRule.Delete), 78 | RpcPath: rpcPath, 79 | }) 80 | case *annotations.HttpRule_Patch: 81 | methods = append(methods, Method{ 82 | HttpMethod: http.MethodPatch, 83 | PathNames: extractFieldNames(httpRule.Patch), 84 | HttpPath: adjustHttpPath(httpRule.Patch), 85 | RpcPath: rpcPath, 86 | }) 87 | default: 88 | methods = append(methods, Method{ 89 | RpcPath: rpcPath, 90 | }) 91 | } 92 | default: 93 | methods = append(methods, Method{ 94 | RpcPath: rpcPath, 95 | }) 96 | } 97 | } 98 | } 99 | } 100 | 101 | return methods, nil 102 | } 103 | 104 | func adjustHttpPath(path string) string { 105 | // path = strings.ReplaceAll(path, "{", ":") 106 | // path = strings.ReplaceAll(path, "}", "") 107 | return path 108 | } 109 | 110 | // extractFieldNames extracts all field names (e.g., "name", "id") from the given path template. 111 | func extractFieldNames(template string) []string { 112 | // Regular expression to match both {name=...} and {name} 113 | re := regexp.MustCompile(`\{([^=}]+)(=[^}]*)?\}`) 114 | 115 | // Find all matches 116 | matches := re.FindAllStringSubmatch(template, -1) 117 | if len(matches) == 0 { 118 | return nil 119 | } 120 | 121 | // Extract the field names 122 | var fieldNames []string 123 | for _, match := range matches { 124 | if len(match) > 1 { 125 | fieldNames = append(fieldNames, match[1]) 126 | } 127 | } 128 | 129 | return fieldNames 130 | } 131 | -------------------------------------------------------------------------------- /pkg/protojson/eventhandler.go: -------------------------------------------------------------------------------- 1 | package protojson 2 | 3 | import ( 4 | "io" 5 | 6 | // nolint 7 | "github.com/golang/protobuf/jsonpb" 8 | // nolint 9 | "github.com/golang/protobuf/proto" 10 | // nolint 11 | "github.com/jhump/protoreflect/desc" 12 | "google.golang.org/grpc/metadata" 13 | "google.golang.org/grpc/status" 14 | ) 15 | 16 | type EventHandler struct { 17 | Status *status.Status 18 | Message proto.Message 19 | Marshaler jsonpb.Marshaler 20 | } 21 | 22 | func NewEventHandler(writer io.Writer, resolver jsonpb.AnyResolver) *EventHandler { 23 | return &EventHandler{ 24 | Marshaler: jsonpb.Marshaler{ 25 | OrigName: false, 26 | EmitDefaults: true, 27 | EnumsAsInts: true, 28 | Indent: "", 29 | AnyResolver: resolver, 30 | }, 31 | } 32 | } 33 | 34 | func (h *EventHandler) OnReceiveResponse(message proto.Message) { 35 | h.Message = message 36 | } 37 | 38 | func (h *EventHandler) OnReceiveTrailers(status *status.Status, _ metadata.MD) { 39 | h.Status = status 40 | } 41 | 42 | func (h *EventHandler) OnResolveMethod(_ *desc.MethodDescriptor) { 43 | } 44 | 45 | func (h *EventHandler) OnSendHeaders(_ metadata.MD) { 46 | } 47 | 48 | func (h *EventHandler) OnReceiveHeaders(_ metadata.MD) { 49 | } 50 | -------------------------------------------------------------------------------- /pkg/protojson/requestparser.go: -------------------------------------------------------------------------------- 1 | package protojson 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/fullstorydev/grpcurl" 10 | 11 | // nolint 12 | "github.com/golang/protobuf/jsonpb" 13 | ) 14 | 15 | // NewRequestParser creates a new request parser from the given http.Request and resolver. 16 | func NewRequestParser(r *http.Request, pathName []string, resolver jsonpb.AnyResolver) (grpcurl.RequestParser, error) { 17 | params := make(map[string]any) 18 | 19 | for _, v := range pathName { 20 | params[v] = r.PathValue(v) 21 | } 22 | 23 | body, ok := getBody(r) 24 | if !ok { 25 | return buildJsonRequestParser(params, resolver) 26 | } 27 | 28 | if len(params) == 0 { 29 | return grpcurl.NewJSONRequestParser(body, resolver), nil 30 | } 31 | 32 | m := make(map[string]any) 33 | if err := json.NewDecoder(body).Decode(&m); err != nil && err != io.EOF { 34 | return nil, err 35 | } 36 | 37 | for k, v := range params { 38 | m[k] = v 39 | } 40 | 41 | return buildJsonRequestParser(m, resolver) 42 | } 43 | 44 | func buildJsonRequestParser(m map[string]any, resolver jsonpb.AnyResolver) ( 45 | grpcurl.RequestParser, error, 46 | ) { 47 | var buf bytes.Buffer 48 | if err := json.NewEncoder(&buf).Encode(m); err != nil { 49 | return nil, err 50 | } 51 | 52 | return grpcurl.NewJSONRequestParser(&buf, resolver), nil 53 | } 54 | 55 | func getBody(r *http.Request) (io.Reader, bool) { 56 | if r.Body == nil { 57 | return nil, false 58 | } 59 | 60 | if r.ContentLength == 0 { 61 | return nil, false 62 | } 63 | 64 | if r.ContentLength > 0 { 65 | return r.Body, true 66 | } 67 | 68 | var buf bytes.Buffer 69 | if _, err := io.Copy(&buf, r.Body); err != nil { 70 | return nil, false 71 | } 72 | 73 | if buf.Len() > 0 { 74 | return &buf, true 75 | } 76 | 77 | return nil, false 78 | } 79 | -------------------------------------------------------------------------------- /test/bench.sh: -------------------------------------------------------------------------------- 1 | ab -n 50000 -kc 100 -T 'application/json' -H 'Accept: application/json' -H 'DNT: 1' -H 'Origin: http://localhost:8080' -p post1.json http://localhost:8080/query 2 | 3 | ab -n 50000 -kc 100 -T 'application/json' -H 'Accept: application/json' -H 'DNT: 1' -H 'Origin: http://localhost:8080' -p post2.json http://localhost:8080/query 4 | -------------------------------------------------------------------------------- /test/integration/fieldmask_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | apitest "github.com/go-kod/grpc-gateway/api/test" 9 | "github.com/go-kod/grpc-gateway/internal/config" 10 | "github.com/go-kod/grpc-gateway/internal/server" 11 | "github.com/go-kod/grpc-gateway/test" 12 | "github.com/go-kod/kod" 13 | "github.com/go-kod/kod-ext/client/kgrpc" 14 | "github.com/go-kod/kod-ext/registry/etcdv3" 15 | "github.com/nautilus/graphql" 16 | "go.uber.org/mock/gomock" 17 | "google.golang.org/protobuf/encoding/protojson" 18 | "google.golang.org/protobuf/types/dynamicpb" 19 | "google.golang.org/protobuf/types/known/fieldmaskpb" 20 | ) 21 | 22 | func TestFieldMask(t *testing.T) { 23 | infos := test.SetupDeps(t) 24 | 25 | mockConfig := config.NewMockConfig(gomock.NewController(t)) 26 | mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ 27 | Engine: config.EngineConfig{ 28 | RateLimit: true, 29 | CircuitBreaker: true, 30 | }, 31 | Grpc: config.Grpc{ 32 | Etcd: etcdv3.Config{ 33 | Endpoints: []string{"localhost:2379"}, 34 | }, 35 | Services: []kgrpc.Config{ 36 | { 37 | Target: infos.ConstructsServerAddr.Addr().String(), 38 | }, 39 | { 40 | Target: infos.OptionsServerAddr.Addr().String(), 41 | }, 42 | }, 43 | }, 44 | Server: config.ServerConfig{ 45 | GraphQL: config.GraphQLConfig{ 46 | Playground: true, 47 | GenerateUnboundMethods: true, 48 | SingleFlight: true, 49 | QueryCache: true, 50 | }, 51 | }, 52 | }).AnyTimes() 53 | 54 | scalars := &apitest.Scalars{ 55 | Double: 1.1, Float: 2.2, Int32: 3, Int64: -4, Uint32: 5, Uint64: 6, Sint32: 7, Sint64: 8, Fixed32: 9, Fixed64: 10, Sfixed32: 11, 56 | Sfixed64: 12, Bool: true, StringX: "test", Bytes: []byte("test"), Paths: &fieldmaskpb.FieldMask{Paths: []string{"double", "float"}}, 57 | } 58 | mockCaller := server.NewMockGraphqlCaller(gomock.NewController(t)) 59 | mockCaller.EXPECT().Call(gomock.Any(), gomock.Any(), gomock.Cond(func(x any) bool { 60 | d1, _ := protojson.Marshal(x.(*dynamicpb.Message)) 61 | d2, _ := protojson.Marshal(scalars) 62 | return string(d1) == string(d2) 63 | })).Return(scalars, nil).Times(1) 64 | 65 | kod.RunTest(t, func(ctx context.Context, s server.Gateway) { 66 | gatewayUrl := test.SetupGateway(t, s) 67 | querier := graphql.NewSingleRequestQueryer(gatewayUrl) 68 | 69 | cases := []struct { 70 | name string 71 | query string 72 | wantResponse interface{} 73 | }{ 74 | { 75 | name: "Mutation constructs scalars", 76 | query: `mutation { 77 | constructsScalars_(in: { 78 | double: 1.1, float: 2.2, int32: 3, int64: -4, 79 | uint32: 5, uint64: 6, sint32: 7, sint64: 8, 80 | fixed32: 9, fixed64: 10, sfixed32: 11, sfixed64: 12, 81 | bool: true, stringX: "test", bytes: "dGVzdA==" 82 | }) { 83 | double 84 | float 85 | } 86 | }`, 87 | wantResponse: j{ 88 | "constructsScalars_": j{ 89 | "double": 1.1, 90 | "float": 2.2, 91 | }, 92 | }, 93 | }, 94 | } 95 | 96 | for _, tc := range cases { 97 | 98 | recv := map[string]interface{}{} 99 | if err := querier.Query(context.Background(), &graphql.QueryInput{ 100 | Query: tc.query, 101 | }, &recv); err != nil { 102 | t.Fatal(err) 103 | } 104 | if !reflect.DeepEqual(recv, tc.wantResponse) { 105 | t.Errorf("mutation failed: expected: %s got: %s", tc.wantResponse, recv) 106 | } 107 | } 108 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig), kod.Fake[server.GraphqlCaller](mockCaller))) 109 | } 110 | -------------------------------------------------------------------------------- /test/integration/graphql2grpc_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/go-kod/grpc-gateway/internal/config" 9 | "github.com/go-kod/grpc-gateway/internal/server" 10 | "github.com/go-kod/grpc-gateway/test" 11 | "github.com/go-kod/kod" 12 | "github.com/go-kod/kod-ext/client/kgrpc" 13 | "github.com/go-kod/kod-ext/registry/etcdv3" 14 | "github.com/nautilus/graphql" 15 | "go.uber.org/mock/gomock" 16 | ) 17 | 18 | func TestGraphql2Grpc(t *testing.T) { 19 | infos := test.SetupDeps(t) 20 | 21 | mockConfig := config.NewMockConfig(gomock.NewController(t)) 22 | mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ 23 | Engine: config.EngineConfig{ 24 | RateLimit: true, 25 | CircuitBreaker: true, 26 | }, 27 | Grpc: config.Grpc{ 28 | Etcd: etcdv3.Config{ 29 | Endpoints: []string{"localhost:2379"}, 30 | }, 31 | Services: []kgrpc.Config{ 32 | { 33 | Target: infos.ConstructsServerAddr.Addr().String(), 34 | }, 35 | { 36 | Target: infos.OptionsServerAddr.Addr().String(), 37 | }, 38 | }, 39 | }, 40 | Server: config.ServerConfig{ 41 | GraphQL: config.GraphQLConfig{ 42 | Playground: true, 43 | GenerateUnboundMethods: true, 44 | SingleFlight: true, 45 | QueryCache: true, 46 | }, 47 | }, 48 | }).AnyTimes() 49 | 50 | kod.RunTest(t, func(ctx context.Context, s server.Gateway) { 51 | gatewayUrl := test.SetupGateway(t, s) 52 | querier := graphql.NewSingleRequestQueryer(gatewayUrl) 53 | 54 | cases := []struct { 55 | name string 56 | query string 57 | wantResponse interface{} 58 | }{ 59 | { 60 | name: "Mutation constructs scalars", 61 | query: constructsScalarsQuery, 62 | wantResponse: constructsScalarsResponse, 63 | }, 64 | { 65 | name: "Mutation constructs any", 66 | query: constructsAnyQuery, 67 | wantResponse: constructsAnyResponse, 68 | }, 69 | { 70 | name: "Mutation constructs maps", 71 | query: constructsMapsQuery, 72 | wantResponse: constructsMapsResponse, 73 | }, 74 | { 75 | name: "Mutation constructs repeated", 76 | query: constructsRepeatedQuery, 77 | wantResponse: constructsRepeatedResponse, 78 | }, 79 | { 80 | name: "Mutation constructs oneofs", 81 | query: constructsOneofsQuery, 82 | wantResponse: constructsOneofsResponse, 83 | }, 84 | } 85 | 86 | for _, tc := range cases { 87 | 88 | recv := map[string]interface{}{} 89 | if err := querier.Query(context.Background(), &graphql.QueryInput{ 90 | Query: tc.query, 91 | }, &recv); err != nil { 92 | t.Fatal(err) 93 | } 94 | if !reflect.DeepEqual(recv, tc.wantResponse) { 95 | t.Errorf("mutation failed: expected: %s got: %s", tc.wantResponse, recv) 96 | } 97 | } 98 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 99 | } 100 | -------------------------------------------------------------------------------- /test/integration/graphql_schema_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "os" 8 | "testing" 9 | 10 | "github.com/go-kod/grpc-gateway/internal/config" 11 | "github.com/go-kod/grpc-gateway/internal/server" 12 | "github.com/go-kod/grpc-gateway/test" 13 | "github.com/go-kod/kod" 14 | "github.com/go-kod/kod-ext/client/kgrpc" 15 | "github.com/go-kod/kod-ext/registry/etcdv3" 16 | "github.com/nautilus/graphql" 17 | "github.com/stretchr/testify/require" 18 | "github.com/vektah/gqlparser/v2/ast" 19 | "github.com/vektah/gqlparser/v2/formatter" 20 | "github.com/vektah/gqlparser/v2/validator" 21 | "go.uber.org/mock/gomock" 22 | ) 23 | 24 | //go:embed testdata/gateway-expect.graphql 25 | var testGatewayExpectedSchema []byte 26 | 27 | //go:embed testdata/gateway-expect-without-unbound-method.graphql 28 | var testGatewayExpectedSchemaWithoutUnboundMethod []byte 29 | 30 | func TestGraphqlSchema(t *testing.T) { 31 | infos := test.SetupDeps(t) 32 | 33 | mockConfig := config.NewMockConfig(gomock.NewController(t)) 34 | mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ 35 | Grpc: config.Grpc{ 36 | Etcd: etcdv3.Config{ 37 | Endpoints: []string{"localhost:2379"}, 38 | }, 39 | Services: []kgrpc.Config{ 40 | { 41 | Target: infos.ConstructsServerAddr.Addr().String(), 42 | }, 43 | { 44 | Target: infos.OptionsServerAddr.Addr().String(), 45 | }, 46 | }, 47 | }, 48 | Server: config.ServerConfig{ 49 | GraphQL: config.GraphQLConfig{ 50 | GenerateUnboundMethods: true, 51 | Playground: true, 52 | }, 53 | }, 54 | }).AnyTimes() 55 | 56 | kod.RunTest(t, func(ctx context.Context, s server.Gateway) { 57 | gatewayUrl := test.SetupGateway(t, s) 58 | t.Run("schema is correct", func(t *testing.T) { 59 | schema, err := graphql.IntrospectRemoteSchema(gatewayUrl) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | file, err := os.Create("testdata/gateway-generate.graphql") 65 | require.Nil(t, err) 66 | formatter.NewFormatter(file).FormatSchema(schema.Schema) 67 | 68 | generated, err := os.ReadFile("testdata/gateway-generate.graphql") 69 | require.Nil(t, err) 70 | 71 | require.Equal(t, string(testGatewayExpectedSchema), string(generated)) 72 | }) 73 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 74 | } 75 | 76 | func TestGraphqlSchemaWithoutUnboundMethod(t *testing.T) { 77 | infos := test.SetupDeps(t) 78 | 79 | mockConfig := config.NewMockConfig(gomock.NewController(t)) 80 | mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ 81 | Grpc: config.Grpc{ 82 | Etcd: etcdv3.Config{ 83 | Endpoints: []string{"localhost:2379"}, 84 | }, 85 | Services: []kgrpc.Config{ 86 | { 87 | Target: infos.ConstructsServerAddr.Addr().String(), 88 | }, 89 | { 90 | Target: infos.OptionsServerAddr.Addr().String(), 91 | }, 92 | }, 93 | }, 94 | Server: config.ServerConfig{ 95 | GraphQL: config.GraphQLConfig{ 96 | Playground: true, 97 | GenerateUnboundMethods: false, 98 | }, 99 | }, 100 | }).AnyTimes() 101 | 102 | kod.RunTest(t, func(ctx context.Context, s server.Gateway) { 103 | t.Run("schema is correct", func(t *testing.T) { 104 | gatewayUrl := test.SetupGateway(t, s) 105 | schema, err := graphql.IntrospectRemoteSchema(gatewayUrl) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | file, err := os.Create("testdata/gateway-generate-without-unbound-method.graphql") 111 | require.Nil(t, err) 112 | formatter.NewFormatter(file).FormatSchema(schema.Schema) 113 | 114 | generated, err := os.ReadFile("testdata/gateway-generate-without-unbound-method.graphql") 115 | require.Nil(t, err) 116 | 117 | require.Equal(t, string(testGatewayExpectedSchemaWithoutUnboundMethod), string(generated)) 118 | }) 119 | 120 | t.Run("schema request", func(t *testing.T) { 121 | gatewayUrl := test.SetupGateway(t, s) 122 | schema, err := graphql.IntrospectRemoteSchema(gatewayUrl) 123 | require.Nil(t, err) 124 | 125 | schemaDefine := new(bytes.Buffer) 126 | formatter.NewFormatter(schemaDefine).FormatSchema(schema.Schema) 127 | _, err = validator.LoadSchema(validator.Prelude, &ast.Source{ 128 | Input: schemaDefine.String(), 129 | }) 130 | require.Nil(t, err) 131 | }) 132 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 133 | } 134 | -------------------------------------------------------------------------------- /test/integration/http_grpc_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/go-kod/grpc-gateway/internal/config" 11 | "github.com/go-kod/grpc-gateway/internal/server" 12 | "github.com/go-kod/grpc-gateway/test" 13 | "github.com/go-kod/kod" 14 | "github.com/go-kod/kod-ext/client/kgrpc" 15 | "github.com/go-kod/kod-ext/registry/etcdv3" 16 | "github.com/stretchr/testify/assert" 17 | "go.uber.org/mock/gomock" 18 | ) 19 | 20 | func TestHTTP2Grpc(t *testing.T) { 21 | infos := test.SetupDeps(t) 22 | 23 | mockConfig := config.NewMockConfig(gomock.NewController(t)) 24 | mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ 25 | Engine: config.EngineConfig{ 26 | RateLimit: true, 27 | CircuitBreaker: true, 28 | }, 29 | Grpc: config.Grpc{ 30 | Etcd: etcdv3.Config{ 31 | Endpoints: []string{"localhost:2379"}, 32 | }, 33 | Services: []kgrpc.Config{ 34 | { 35 | Target: infos.ConstructsServerAddr.Addr().String(), 36 | }, 37 | { 38 | Target: infos.OptionsServerAddr.Addr().String(), 39 | }, 40 | { 41 | Target: infos.HelloworldServerAddr.Addr().String(), 42 | }, 43 | }, 44 | }, 45 | Server: config.ServerConfig{ 46 | GraphQL: config.GraphQLConfig{ 47 | Playground: true, 48 | GenerateUnboundMethods: true, 49 | SingleFlight: true, 50 | QueryCache: true, 51 | }, 52 | }, 53 | }).AnyTimes() 54 | 55 | t.Run("http to grpc", func(t *testing.T) { 56 | kod.RunTest(t, func(ctx context.Context, up server.HttpUpstream) { 57 | router := http.NewServeMux() 58 | up.Register(ctx, router) 59 | rec := httptest.NewRecorder() 60 | req, _ := http.NewRequest(http.MethodGet, "/say/bob", nil) 61 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 62 | 63 | router.ServeHTTP(rec, req) 64 | assert.Equal(t, http.StatusOK, rec.Code) 65 | assert.Equal(t, "application/json; charset=utf-8", rec.Header().Get("Content-Type")) 66 | assert.Equal(t, "{\"message\":\"Hello bob\"}", rec.Body.String()) 67 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 68 | }) 69 | 70 | t.Run("not found", func(t *testing.T) { 71 | kod.RunTest(t, func(ctx context.Context, up server.HttpUpstream) { 72 | router := http.NewServeMux() 73 | up.Register(ctx, router) 74 | rec := httptest.NewRecorder() 75 | req, _ := http.NewRequest(http.MethodGet, "/say-notfound", nil) 76 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 77 | 78 | router.ServeHTTP(rec, req) 79 | assert.Equal(t, http.StatusNotFound, rec.Code) 80 | assert.Equal(t, "text/plain; charset=utf-8", rec.Header().Get("Content-Type")) 81 | assert.Equal(t, "404 page not found\n", rec.Body.String()) 82 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 83 | }) 84 | 85 | t.Run("error", func(t *testing.T) { 86 | kod.RunTest(t, func(ctx context.Context, up server.HttpUpstream) { 87 | router := http.NewServeMux() 88 | up.Register(ctx, router) 89 | rec := httptest.NewRecorder() 90 | req, _ := http.NewRequest(http.MethodGet, "/say/error", nil) 91 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 92 | 93 | router.ServeHTTP(rec, req) 94 | assert.Equal(t, http.StatusOK, rec.Code) 95 | assert.Equal(t, "application/json; charset=utf-8", rec.Header().Get("Content-Type")) 96 | assert.Equal(t, `{"code":2,"message":"error","details":[]}`, rec.Body.String()) 97 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 98 | }) 99 | 100 | t.Run("http body", func(t *testing.T) { 101 | kod.RunTest(t, func(ctx context.Context, up server.HttpUpstream) { 102 | router := http.NewServeMux() 103 | up.Register(ctx, router) 104 | rec := httptest.NewRecorder() 105 | req, _ := http.NewRequest(http.MethodGet, "/say/sam", bytes.NewBufferString("{\"name\":\"bob\"}")) 106 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 107 | 108 | router.ServeHTTP(rec, req) 109 | assert.Equal(t, http.StatusOK, rec.Code) 110 | assert.Equal(t, "application/json; charset=utf-8", rec.Header().Get("Content-Type")) 111 | assert.Equal(t, `{"message":"Hello sam"}`, rec.Body.String()) 112 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 113 | }) 114 | 115 | t.Run("invalid http body", func(t *testing.T) { 116 | kod.RunTest(t, func(ctx context.Context, up server.HttpUpstream) { 117 | router := http.NewServeMux() 118 | up.Register(ctx, router) 119 | rec := httptest.NewRecorder() 120 | req, _ := http.NewRequest(http.MethodGet, "/say/sam", bytes.NewBufferString("{invalid data}")) 121 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 122 | 123 | router.ServeHTTP(rec, req) 124 | assert.Equal(t, http.StatusBadRequest, rec.Code) 125 | assert.Equal(t, "text/plain; charset=utf-8", rec.Header().Get("Content-Type")) 126 | assert.Equal(t, "rpc error: code = InvalidArgument desc = invalid character 'i' looking for beginning of object key string\n", rec.Body.String()) 127 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 128 | }) 129 | } 130 | 131 | func TestHTTP2Grpc_Singleflight(t *testing.T) { 132 | infos := test.SetupDeps(t) 133 | 134 | t.Run("singleflight", func(t *testing.T) { 135 | mockConfig := config.NewMockConfig(gomock.NewController(t)) 136 | mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ 137 | Grpc: config.Grpc{ 138 | Etcd: etcdv3.Config{ 139 | Endpoints: []string{"localhost:2379"}, 140 | }, 141 | Services: []kgrpc.Config{ 142 | { 143 | Target: infos.ConstructsServerAddr.Addr().String(), 144 | }, 145 | { 146 | Target: infos.OptionsServerAddr.Addr().String(), 147 | }, 148 | { 149 | Target: infos.HelloworldServerAddr.Addr().String(), 150 | }, 151 | }, 152 | }, 153 | Server: config.ServerConfig{ 154 | HTTP: config.HTTPConfig{ 155 | SingleFlight: true, 156 | }, 157 | }, 158 | }).AnyTimes() 159 | 160 | kod.RunTest(t, func(ctx context.Context, up server.HttpUpstream) { 161 | router := http.NewServeMux() 162 | up.Register(ctx, router) 163 | rec := httptest.NewRecorder() 164 | req, _ := http.NewRequest(http.MethodGet, "/say/bob", nil) 165 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 166 | 167 | router.ServeHTTP(rec, req) 168 | assert.Equal(t, http.StatusOK, rec.Code) 169 | assert.Equal(t, "application/json; charset=utf-8", rec.Header().Get("Content-Type")) 170 | assert.Equal(t, "{\"message\":\"Hello bob\"}", rec.Body.String()) 171 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 172 | }) 173 | } 174 | -------------------------------------------------------------------------------- /test/integration/jwt_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/go-kod/grpc-gateway/internal/config" 10 | "github.com/go-kod/grpc-gateway/internal/server" 11 | "github.com/go-kod/grpc-gateway/test" 12 | "github.com/go-kod/kod" 13 | "github.com/go-kod/kod-ext/client/kgrpc" 14 | "github.com/go-kod/kod-ext/registry/etcdv3" 15 | "github.com/golang-jwt/jwt/v5" 16 | "github.com/nautilus/graphql" 17 | "github.com/stretchr/testify/require" 18 | "go.uber.org/mock/gomock" 19 | ) 20 | 21 | func TestJwt(t *testing.T) { 22 | infos := test.SetupDeps(t) 23 | 24 | mockConfig := config.NewMockConfig(gomock.NewController(t)) 25 | mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ 26 | Engine: config.EngineConfig{}, 27 | Grpc: config.Grpc{ 28 | Etcd: etcdv3.Config{ 29 | Endpoints: []string{"localhost:2379"}, 30 | }, 31 | Services: []kgrpc.Config{ 32 | { 33 | Target: infos.ConstructsServerAddr.Addr().String(), 34 | }, 35 | { 36 | Target: infos.OptionsServerAddr.Addr().String(), 37 | }, 38 | }, 39 | }, 40 | Server: config.ServerConfig{ 41 | GraphQL: config.GraphQLConfig{ 42 | GenerateUnboundMethods: true, 43 | Jwt: config.Jwt{ 44 | Enable: true, 45 | LocalJwks: "key", 46 | ForwardPayloadHeader: "x-jwt-payload", 47 | }, 48 | }, 49 | }, 50 | }).AnyTimes() 51 | 52 | kod.RunTest(t, func(ctx context.Context, s server.Gateway) { 53 | gatewayUrl := test.SetupGateway(t, s) 54 | 55 | t.Run("jwt auth", func(t *testing.T) { 56 | querier := graphql.NewSingleRequestQueryer(gatewayUrl) 57 | 58 | recv := map[string]interface{}{} 59 | querier.WithMiddlewares([]graphql.NetworkMiddleware{ 60 | func(r *http.Request) error { 61 | token, err := createToken("bob", mockConfig.Config().Server.GraphQL.Jwt.LocalJwks) 62 | require.Nil(t, err) 63 | 64 | r.Header.Set("Authorization", "Bearer "+token) 65 | return nil 66 | }, 67 | }) 68 | 69 | err := querier.Query(context.Background(), &graphql.QueryInput{ 70 | Query: contructsMultipleQuery, 71 | }, &recv) 72 | require.Nil(t, err) 73 | require.EqualValues(t, constructsMultipleResponse, recv) 74 | }) 75 | 76 | t.Run("jwt auth failed", func(t *testing.T) { 77 | querier := graphql.NewSingleRequestQueryer(gatewayUrl) 78 | recv := map[string]interface{}{} 79 | querier.WithMiddlewares([]graphql.NetworkMiddleware{ 80 | func(r *http.Request) error { 81 | token, err := createToken("bob", "wrong key") 82 | require.Nil(t, err) 83 | 84 | r.Header.Set("Authorization", "Bearer "+token) 85 | return nil 86 | }, 87 | }) 88 | 89 | err := querier.Query(context.Background(), &graphql.QueryInput{ 90 | Query: contructsMultipleQuery, 91 | }, &recv) 92 | require.NotNil(t, err) 93 | }) 94 | 95 | t.Run("not authorization", func(t *testing.T) { 96 | querier := graphql.NewSingleRequestQueryer(gatewayUrl) 97 | recv := map[string]interface{}{} 98 | 99 | err := querier.Query(context.Background(), &graphql.QueryInput{ 100 | Query: contructsMultipleQuery, 101 | }, &recv) 102 | require.NotNil(t, err) 103 | }) 104 | 105 | t.Run("expired authorization", func(t *testing.T) { 106 | querier := graphql.NewSingleRequestQueryer(gatewayUrl) 107 | recv := map[string]interface{}{} 108 | querier.WithMiddlewares([]graphql.NetworkMiddleware{ 109 | func(r *http.Request) error { 110 | token, err := createExpiredToken("bob", mockConfig.Config().Server.GraphQL.Jwt.LocalJwks) 111 | require.Nil(t, err) 112 | 113 | r.Header.Set("Authorization", "Bearer "+token) 114 | return nil 115 | }, 116 | }) 117 | 118 | err := querier.Query(context.Background(), &graphql.QueryInput{ 119 | Query: contructsMultipleQuery, 120 | }, &recv) 121 | require.NotNil(t, err) 122 | require.ErrorContains(t, err, "response was not successful with status code: 401") 123 | }) 124 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 125 | } 126 | 127 | func createToken(username string, secretKey string) (string, error) { 128 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, 129 | jwt.MapClaims{ 130 | "username": username, 131 | "exp": time.Now().Add(time.Hour * 24).Unix(), 132 | }) 133 | 134 | tokenString, err := token.SignedString([]byte(secretKey)) 135 | if err != nil { 136 | return "", err 137 | } 138 | 139 | return tokenString, nil 140 | } 141 | 142 | func createExpiredToken(username string, secretKey string) (string, error) { 143 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, 144 | jwt.MapClaims{ 145 | "username": username, 146 | "exp": time.Now().Add(time.Hour * -24).Unix(), 147 | }) 148 | 149 | tokenString, err := token.SignedString([]byte(secretKey)) 150 | if err != nil { 151 | return "", err 152 | } 153 | 154 | return tokenString, nil 155 | } 156 | -------------------------------------------------------------------------------- /test/integration/reflection_exit_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/go-kod/grpc-gateway/internal/config" 9 | "github.com/go-kod/grpc-gateway/internal/server" 10 | "github.com/go-kod/grpc-gateway/test" 11 | "github.com/go-kod/kod" 12 | "github.com/go-kod/kod-ext/client/kgrpc" 13 | "github.com/go-kod/kod-ext/registry/etcdv3" 14 | "github.com/nautilus/graphql" 15 | "go.uber.org/mock/gomock" 16 | ) 17 | 18 | func TestReflectionExit(t *testing.T) { 19 | infos := test.SetupDeps(t) 20 | 21 | mockConfig := config.NewMockConfig(gomock.NewController(t)) 22 | mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ 23 | Engine: config.EngineConfig{}, 24 | Grpc: config.Grpc{ 25 | Etcd: etcdv3.Config{ 26 | Endpoints: []string{"localhost:2379"}, 27 | }, 28 | Services: []kgrpc.Config{ 29 | { 30 | Target: infos.ConstructsServerAddr.Addr().String(), 31 | }, 32 | { 33 | Target: infos.OptionsServerAddr.Addr().String(), 34 | }, 35 | }, 36 | }, 37 | Server: config.ServerConfig{ 38 | GraphQL: config.GraphQLConfig{ 39 | GenerateUnboundMethods: true, 40 | }, 41 | }, 42 | }).AnyTimes() 43 | 44 | kod.RunTest(t, func(ctx context.Context, s server.Gateway) { 45 | gatewayUrl := test.SetupGateway(t, s) 46 | querier := graphql.NewSingleRequestQueryer(gatewayUrl) 47 | 48 | t.Run("stop do not panic", func(t *testing.T) { 49 | infos.OptionServer.Stop() 50 | 51 | recv := map[string]interface{}{} 52 | if err := querier.Query(context.Background(), &graphql.QueryInput{ 53 | Query: constructsScalarsQuery, 54 | }, &recv); err != nil { 55 | t.Fatal(err) 56 | } 57 | if !reflect.DeepEqual(recv, constructsScalarsResponse) { 58 | t.Errorf("mutation failed: expected: %s got: %s", constructsScalarsResponse, recv) 59 | } 60 | }) 61 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 62 | } 63 | -------------------------------------------------------------------------------- /test/integration/singleflight_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/go-kod/grpc-gateway/internal/config" 10 | "github.com/go-kod/grpc-gateway/internal/server" 11 | "github.com/go-kod/grpc-gateway/pkg/header" 12 | "github.com/go-kod/grpc-gateway/test" 13 | "github.com/go-kod/kod" 14 | "github.com/go-kod/kod-ext/client/kgrpc" 15 | "github.com/go-kod/kod-ext/registry/etcdv3" 16 | "github.com/nautilus/graphql" 17 | "github.com/stretchr/testify/assert" 18 | "go.uber.org/mock/gomock" 19 | ) 20 | 21 | func TestSingleFlight(t *testing.T) { 22 | infos := test.SetupDeps(t) 23 | 24 | mockConfig := config.NewMockConfig(gomock.NewController(t)) 25 | mockConfig.EXPECT().Config().Return(&config.ConfigInfo{ 26 | Engine: config.EngineConfig{}, 27 | Grpc: config.Grpc{ 28 | Etcd: etcdv3.Config{ 29 | Endpoints: []string{"localhost:2379"}, 30 | }, 31 | Services: []kgrpc.Config{ 32 | { 33 | Target: infos.ConstructsServerAddr.Addr().String(), 34 | }, 35 | { 36 | Target: infos.OptionsServerAddr.Addr().String(), 37 | }, 38 | }, 39 | }, 40 | Server: config.ServerConfig{ 41 | GraphQL: config.GraphQLConfig{ 42 | GenerateUnboundMethods: true, 43 | SingleFlight: true, 44 | }, 45 | }, 46 | }).AnyTimes() 47 | 48 | kod.RunTest(t, func(ctx context.Context, s server.Gateway) { 49 | gatewayUrl := test.SetupGateway(t, s) 50 | 51 | t.Run("multiple different query", func(t *testing.T) { 52 | recv := map[string]interface{}{} 53 | querier := graphql.NewSingleRequestQueryer(gatewayUrl) 54 | if err := querier.Query(context.Background(), &graphql.QueryInput{ 55 | Query: contructsMultipleQuery, 56 | }, &recv); err != nil { 57 | t.Fatal(err) 58 | } 59 | assert.EqualValues(t, constructsMultipleResponse, recv) 60 | }) 61 | 62 | t.Run("multiple same query", func(t *testing.T) { 63 | wg := sync.WaitGroup{} 64 | wg.Add(2) 65 | for i := 0; i < 2; i++ { 66 | go func(t *testing.T) { 67 | defer wg.Done() 68 | recv := map[string]interface{}{} 69 | querier := graphql.NewSingleRequestQueryer(gatewayUrl) 70 | querier.WithMiddlewares([]graphql.NetworkMiddleware{ 71 | func(r *http.Request) error { 72 | r.Header.Set("Authorization", "Bearer ") 73 | r.Header.Set(header.MetadataHeaderPrefix+"singleflight", "true") 74 | return nil 75 | }, 76 | }) 77 | err := querier.Query(context.Background(), &graphql.QueryInput{ 78 | Query: contructsMultipleSameQuery, 79 | }, &recv) 80 | assert.Nil(t, err) 81 | assert.EqualValues(t, constructsMultipleSameResponse, recv) 82 | }(t) 83 | } 84 | wg.Wait() 85 | }) 86 | }, kod.WithFakes(kod.Fake[config.Config](mockConfig))) 87 | } 88 | -------------------------------------------------------------------------------- /test/integration/testdata/constructs-expect.graphql: -------------------------------------------------------------------------------- 1 | directive @Constructs on FIELD_DEFINITION 2 | directive @Oneof_Oneof1 on INPUT_FIELD_DEFINITION 3 | directive @Oneof_Oneof2 on INPUT_FIELD_DEFINITION 4 | directive @Oneof_Oneof3 on INPUT_FIELD_DEFINITION 5 | """ 6 | Any is any json type 7 | """ 8 | scalar Any 9 | enum Bar { 10 | BAR1 11 | BAR2 12 | BAR3 13 | } 14 | type Baz { 15 | param1: String 16 | } 17 | input BazInput { 18 | param1: String 19 | } 20 | scalar Bytes 21 | type Foo { 22 | param1: String 23 | param2: String 24 | } 25 | input FooInput { 26 | param1: String 27 | param2: String 28 | } 29 | type Foo_Foo2 { 30 | param1: String 31 | } 32 | input Foo_Foo2Input { 33 | param1: String 34 | } 35 | type GoogleProtobuf_Timestamp { 36 | seconds: Int 37 | nanos: Int 38 | } 39 | 40 | input GoogleProtobuf_TimestampInput { 41 | seconds: Int 42 | nanos: Int 43 | } 44 | type Maps { 45 | int32Int32: [Maps_Int32Int32Entry!] 46 | int64Int64: [Maps_Int64Int64Entry!] 47 | uint32Uint32: [Maps_Uint32Uint32Entry!] 48 | uint64Uint64: [Maps_Uint64Uint64Entry!] 49 | sint32Sint32: [Maps_Sint32Sint32Entry!] 50 | sint64Sint64: [Maps_Sint64Sint64Entry!] 51 | fixed32Fixed32: [Maps_Fixed32Fixed32Entry!] 52 | fixed64Fixed64: [Maps_Fixed64Fixed64Entry!] 53 | sfixed32Sfixed32: [Maps_Sfixed32Sfixed32Entry!] 54 | sfixed64Sfixed64: [Maps_Sfixed64Sfixed64Entry!] 55 | boolBool: [Maps_BoolBoolEntry!] 56 | stringString: [Maps_StringStringEntry!] 57 | stringBytes: [Maps_StringBytesEntry!] 58 | stringFloat: [Maps_StringFloatEntry!] 59 | stringDouble: [Maps_StringDoubleEntry!] 60 | stringFoo: [Maps_StringFooEntry!] 61 | stringBar: [Maps_StringBarEntry!] 62 | } 63 | input MapsInput { 64 | int32Int32: [Maps_Int32Int32EntryInput!] 65 | int64Int64: [Maps_Int64Int64EntryInput!] 66 | uint32Uint32: [Maps_Uint32Uint32EntryInput!] 67 | uint64Uint64: [Maps_Uint64Uint64EntryInput!] 68 | sint32Sint32: [Maps_Sint32Sint32EntryInput!] 69 | sint64Sint64: [Maps_Sint64Sint64EntryInput!] 70 | fixed32Fixed32: [Maps_Fixed32Fixed32EntryInput!] 71 | fixed64Fixed64: [Maps_Fixed64Fixed64EntryInput!] 72 | sfixed32Sfixed32: [Maps_Sfixed32Sfixed32EntryInput!] 73 | sfixed64Sfixed64: [Maps_Sfixed64Sfixed64EntryInput!] 74 | boolBool: [Maps_BoolBoolEntryInput!] 75 | stringString: [Maps_StringStringEntryInput!] 76 | stringBytes: [Maps_StringBytesEntryInput!] 77 | stringFloat: [Maps_StringFloatEntryInput!] 78 | stringDouble: [Maps_StringDoubleEntryInput!] 79 | stringFoo: [Maps_StringFooEntryInput!] 80 | stringBar: [Maps_StringBarEntryInput!] 81 | } 82 | type Maps_BoolBoolEntry { 83 | key: Boolean 84 | value: Boolean 85 | } 86 | input Maps_BoolBoolEntryInput { 87 | key: Boolean 88 | value: Boolean 89 | } 90 | type Maps_Fixed32Fixed32Entry { 91 | key: Int 92 | value: Int 93 | } 94 | input Maps_Fixed32Fixed32EntryInput { 95 | key: Int 96 | value: Int 97 | } 98 | type Maps_Fixed64Fixed64Entry { 99 | key: Int 100 | value: Int 101 | } 102 | input Maps_Fixed64Fixed64EntryInput { 103 | key: Int 104 | value: Int 105 | } 106 | type Maps_Int32Int32Entry { 107 | key: Int 108 | value: Int 109 | } 110 | input Maps_Int32Int32EntryInput { 111 | key: Int 112 | value: Int 113 | } 114 | type Maps_Int64Int64Entry { 115 | key: Int 116 | value: Int 117 | } 118 | input Maps_Int64Int64EntryInput { 119 | key: Int 120 | value: Int 121 | } 122 | type Maps_Sfixed32Sfixed32Entry { 123 | key: Int 124 | value: Int 125 | } 126 | input Maps_Sfixed32Sfixed32EntryInput { 127 | key: Int 128 | value: Int 129 | } 130 | type Maps_Sfixed64Sfixed64Entry { 131 | key: Int 132 | value: Int 133 | } 134 | input Maps_Sfixed64Sfixed64EntryInput { 135 | key: Int 136 | value: Int 137 | } 138 | type Maps_Sint32Sint32Entry { 139 | key: Int 140 | value: Int 141 | } 142 | input Maps_Sint32Sint32EntryInput { 143 | key: Int 144 | value: Int 145 | } 146 | type Maps_Sint64Sint64Entry { 147 | key: Int 148 | value: Int 149 | } 150 | input Maps_Sint64Sint64EntryInput { 151 | key: Int 152 | value: Int 153 | } 154 | type Maps_StringBarEntry { 155 | key: String 156 | value: Bar 157 | } 158 | input Maps_StringBarEntryInput { 159 | key: String 160 | value: Bar 161 | } 162 | type Maps_StringBytesEntry { 163 | key: String 164 | value: Bytes 165 | } 166 | input Maps_StringBytesEntryInput { 167 | key: String 168 | value: Bytes 169 | } 170 | type Maps_StringDoubleEntry { 171 | key: String 172 | value: Float 173 | } 174 | input Maps_StringDoubleEntryInput { 175 | key: String 176 | value: Float 177 | } 178 | type Maps_StringFloatEntry { 179 | key: String 180 | value: Float 181 | } 182 | input Maps_StringFloatEntryInput { 183 | key: String 184 | value: Float 185 | } 186 | type Maps_StringFooEntry { 187 | key: String 188 | value: Foo 189 | } 190 | input Maps_StringFooEntryInput { 191 | key: String 192 | value: FooInput 193 | } 194 | type Maps_StringStringEntry { 195 | key: String 196 | value: String 197 | } 198 | input Maps_StringStringEntryInput { 199 | key: String 200 | value: String 201 | } 202 | type Maps_Uint32Uint32Entry { 203 | key: Int 204 | value: Int 205 | } 206 | input Maps_Uint32Uint32EntryInput { 207 | key: Int 208 | value: Int 209 | } 210 | type Maps_Uint64Uint64Entry { 211 | key: Int 212 | value: Int 213 | } 214 | input Maps_Uint64Uint64EntryInput { 215 | key: Int 216 | value: Int 217 | } 218 | type Mutation { 219 | constructsScalars_(in: ScalarsInput): Scalars @Constructs 220 | constructsRepeated_(in: RepeatedInput): Repeated @Constructs 221 | constructsMaps_(in: MapsInput): Maps @Constructs 222 | constructsAny_(in: Any): Any @Constructs 223 | constructsEmpty_: Boolean @Constructs 224 | constructsEmpty2_: Boolean @Constructs 225 | constructsEmpty3_: Boolean @Constructs 226 | constructsRef_(in: RefInput): Ref @Constructs 227 | constructsOneof_(in: OneofInput): Oneof @Constructs 228 | constructsCallWithId: Boolean @Constructs 229 | } 230 | type Oneof { 231 | param1: String 232 | oneof1: Oneof_Oneof1 233 | oneof2: Oneof_Oneof2 234 | oneof3: Oneof_Oneof3 235 | } 236 | input OneofInput { 237 | param1: String 238 | param2: String @Oneof_Oneof1 239 | param3: String @Oneof_Oneof1 240 | param4: String @Oneof_Oneof2 241 | param5: String @Oneof_Oneof2 242 | param6: String @Oneof_Oneof3 243 | } 244 | union Oneof_Oneof1 = Oneof_Param2 | Oneof_Param3 245 | union Oneof_Oneof2 = Oneof_Param4 | Oneof_Param5 246 | union Oneof_Oneof3 = Oneof_Param6 247 | type Oneof_Param2 { 248 | param2: String 249 | } 250 | type Oneof_Param3 { 251 | param3: String 252 | } 253 | type Oneof_Param4 { 254 | param4: String 255 | } 256 | type Oneof_Param5 { 257 | param5: String 258 | } 259 | type Oneof_Param6 { 260 | param6: String 261 | } 262 | type Query { 263 | dummy: Boolean 264 | } 265 | type Ref { 266 | localTime2: Timestamp 267 | external: GoogleProtobuf_Timestamp 268 | localTime: Timestamp 269 | file: Baz 270 | fileMsg: Foo 271 | fileEnum: Bar 272 | local: Ref_Foo 273 | foreign: Foo_Foo2 274 | en1: Ref_Foo_En 275 | en2: Ref_Foo_Bar_En 276 | gz: Ref_Foo_Baz_Gz 277 | } 278 | input RefInput { 279 | localTime2: TimestampInput 280 | external: GoogleProtobuf_TimestampInput 281 | localTime: TimestampInput 282 | file: BazInput 283 | fileMsg: FooInput 284 | fileEnum: Bar 285 | local: Ref_FooInput 286 | foreign: Foo_Foo2Input 287 | en1: Ref_Foo_En 288 | en2: Ref_Foo_Bar_En 289 | gz: Ref_Foo_Baz_GzInput 290 | } 291 | type Ref_Bar { 292 | param1: String 293 | } 294 | input Ref_BarInput { 295 | param1: String 296 | } 297 | type Ref_Foo { 298 | bar1: Ref_Foo_Bar 299 | localTime2: Timestamp 300 | externalTime1: GoogleProtobuf_Timestamp 301 | bar2: Ref_Bar 302 | en1: Ref_Foo_En 303 | en2: Ref_Foo_Bar_En 304 | } 305 | input Ref_FooInput { 306 | bar1: Ref_Foo_BarInput 307 | localTime2: TimestampInput 308 | externalTime1: GoogleProtobuf_TimestampInput 309 | bar2: Ref_BarInput 310 | en1: Ref_Foo_En 311 | en2: Ref_Foo_Bar_En 312 | } 313 | type Ref_Foo_Bar { 314 | param1: String 315 | } 316 | input Ref_Foo_BarInput { 317 | param1: String 318 | } 319 | enum Ref_Foo_Bar_En { 320 | A0 321 | A1 322 | } 323 | type Ref_Foo_Baz_Gz { 324 | param1: String 325 | } 326 | input Ref_Foo_Baz_GzInput { 327 | param1: String 328 | } 329 | enum Ref_Foo_En { 330 | A0 331 | A1 332 | } 333 | type Repeated { 334 | double: [Float!] 335 | float: [Float!] 336 | int32: [Int!] 337 | int64: [Int!] 338 | uint32: [Int!] 339 | uint64: [Int!] 340 | sint32: [Int!] 341 | sint64: [Int!] 342 | fixed32: [Int!] 343 | fixed64: [Int!] 344 | sfixed32: [Int!] 345 | sfixed64: [Int!] 346 | bool: [Boolean!] 347 | stringX: [String!] 348 | bytes: [Bytes!] 349 | foo: [Foo!] 350 | bar: [Bar!] 351 | } 352 | input RepeatedInput { 353 | double: [Float!] 354 | float: [Float!] 355 | int32: [Int!] 356 | int64: [Int!] 357 | uint32: [Int!] 358 | uint64: [Int!] 359 | sint32: [Int!] 360 | sint64: [Int!] 361 | fixed32: [Int!] 362 | fixed64: [Int!] 363 | sfixed32: [Int!] 364 | sfixed64: [Int!] 365 | bool: [Boolean!] 366 | stringX: [String!] 367 | bytes: [Bytes!] 368 | foo: [FooInput!] 369 | bar: [Bar!] 370 | } 371 | type Scalars { 372 | double: Float 373 | float: Float 374 | int32: Int 375 | int64: Int 376 | uint32: Int 377 | uint64: Int 378 | sint32: Int 379 | sint64: Int 380 | fixed32: Int 381 | fixed64: Int 382 | sfixed32: Int 383 | sfixed64: Int 384 | bool: Boolean 385 | stringX: String 386 | bytes: Bytes 387 | } 388 | input ScalarsInput { 389 | double: Float 390 | float: Float 391 | int32: Int 392 | int64: Int 393 | uint32: Int 394 | uint64: Int 395 | sint32: Int 396 | sint64: Int 397 | fixed32: Int 398 | fixed64: Int 399 | sfixed32: Int 400 | sfixed64: Int 401 | bool: Boolean 402 | stringX: String 403 | bytes: Bytes 404 | } 405 | type Timestamp { 406 | time: String 407 | } 408 | input TimestampInput { 409 | time: String 410 | } 411 | -------------------------------------------------------------------------------- /test/integration/testdata/gateway-expect-without-unbound-method.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | The @defer directive may be specified on a fragment spread to imply de-prioritization, that causes the fragment to be omitted in the initial response, and delivered as a subsequent response afterward. A query with @defer directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred delivered in a subsequent response. @include and @skip take precedence over @defer. 3 | """ 4 | directive @defer(if: Boolean = true, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT 5 | """ 6 | The @specifiedBy built-in directive is used within the type system definition language to provide a scalar specification URL for specifying the behavior of custom scalar types. 7 | """ 8 | directive @specifiedBy(url: String!) on SCALAR 9 | scalar Any 10 | input AnyInput { 11 | param1: String 12 | } 13 | type AnyInputOutput { 14 | any: Any 15 | } 16 | enum Bar { 17 | BAR1 18 | BAR2 19 | BAR3 20 | } 21 | type Baz { 22 | param1: String 23 | } 24 | input BazInput { 25 | param1: String 26 | } 27 | type Data { 28 | stringX: String! 29 | foo: Foo2! 30 | double: [Float!]! 31 | string2: String 32 | foo2: Foo2 33 | double2: [Float!] 34 | bar: String 35 | string: String 36 | } 37 | input DataInput { 38 | stringX: String! 39 | foo: Foo2Input! 40 | double: [Float!]! 41 | string2: String 42 | foo2: Foo2Input 43 | double2: [Float!] 44 | bar: String 45 | string: String 46 | } 47 | type Foo { 48 | param1: String 49 | param2: String 50 | } 51 | type Foo2 { 52 | param1: String 53 | } 54 | input Foo2Input { 55 | param1: String 56 | } 57 | type FooFoo2 { 58 | param1: String 59 | } 60 | input FooFoo2Input { 61 | param1: String 62 | } 63 | input FooInput { 64 | param1: String 65 | param2: String 66 | } 67 | type Maps { 68 | int32Int32: [MapsInt32Int32Entry!] 69 | int64Int64: [MapsInt64Int64Entry!] 70 | uint32Uint32: [MapsUint32Uint32Entry!] 71 | uint64Uint64: [MapsUint64Uint64Entry!] 72 | sint32Sint32: [MapsSint32Sint32Entry!] 73 | sint64Sint64: [MapsSint64Sint64Entry!] 74 | fixed32Fixed32: [MapsFixed32Fixed32Entry!] 75 | fixed64Fixed64: [MapsFixed64Fixed64Entry!] 76 | sfixed32Sfixed32: [MapsSfixed32Sfixed32Entry!] 77 | sfixed64Sfixed64: [MapsSfixed64Sfixed64Entry!] 78 | boolBool: [MapsBoolBoolEntry!] 79 | stringString: [MapsStringStringEntry!] 80 | stringBytes: [MapsStringBytesEntry!] 81 | stringFloat: [MapsStringFloatEntry!] 82 | stringDouble: [MapsStringDoubleEntry!] 83 | stringFoo: [MapsStringFooEntry!] 84 | stringBar: [MapsStringBarEntry!] 85 | } 86 | type MapsBoolBoolEntry { 87 | key: Boolean 88 | value: Boolean 89 | } 90 | input MapsBoolBoolEntryInput { 91 | key: Boolean 92 | value: Boolean 93 | } 94 | type MapsFixed32Fixed32Entry { 95 | key: Int 96 | value: Int 97 | } 98 | input MapsFixed32Fixed32EntryInput { 99 | key: Int 100 | value: Int 101 | } 102 | type MapsFixed64Fixed64Entry { 103 | key: Int 104 | value: Int 105 | } 106 | input MapsFixed64Fixed64EntryInput { 107 | key: Int 108 | value: Int 109 | } 110 | input MapsInput { 111 | int32Int32: [MapsInt32Int32EntryInput!] 112 | int64Int64: [MapsInt64Int64EntryInput!] 113 | uint32Uint32: [MapsUint32Uint32EntryInput!] 114 | uint64Uint64: [MapsUint64Uint64EntryInput!] 115 | sint32Sint32: [MapsSint32Sint32EntryInput!] 116 | sint64Sint64: [MapsSint64Sint64EntryInput!] 117 | fixed32Fixed32: [MapsFixed32Fixed32EntryInput!] 118 | fixed64Fixed64: [MapsFixed64Fixed64EntryInput!] 119 | sfixed32Sfixed32: [MapsSfixed32Sfixed32EntryInput!] 120 | sfixed64Sfixed64: [MapsSfixed64Sfixed64EntryInput!] 121 | boolBool: [MapsBoolBoolEntryInput!] 122 | stringString: [MapsStringStringEntryInput!] 123 | stringBytes: [MapsStringBytesEntryInput!] 124 | stringFloat: [MapsStringFloatEntryInput!] 125 | stringDouble: [MapsStringDoubleEntryInput!] 126 | stringFoo: [MapsStringFooEntryInput!] 127 | stringBar: [MapsStringBarEntryInput!] 128 | } 129 | type MapsInt32Int32Entry { 130 | key: Int 131 | value: Int 132 | } 133 | input MapsInt32Int32EntryInput { 134 | key: Int 135 | value: Int 136 | } 137 | type MapsInt64Int64Entry { 138 | key: Int 139 | value: Int 140 | } 141 | input MapsInt64Int64EntryInput { 142 | key: Int 143 | value: Int 144 | } 145 | type MapsSfixed32Sfixed32Entry { 146 | key: Int 147 | value: Int 148 | } 149 | input MapsSfixed32Sfixed32EntryInput { 150 | key: Int 151 | value: Int 152 | } 153 | type MapsSfixed64Sfixed64Entry { 154 | key: Int 155 | value: Int 156 | } 157 | input MapsSfixed64Sfixed64EntryInput { 158 | key: Int 159 | value: Int 160 | } 161 | type MapsSint32Sint32Entry { 162 | key: Int 163 | value: Int 164 | } 165 | input MapsSint32Sint32EntryInput { 166 | key: Int 167 | value: Int 168 | } 169 | type MapsSint64Sint64Entry { 170 | key: Int 171 | value: Int 172 | } 173 | input MapsSint64Sint64EntryInput { 174 | key: Int 175 | value: Int 176 | } 177 | type MapsStringBarEntry { 178 | key: String 179 | value: Bar 180 | } 181 | input MapsStringBarEntryInput { 182 | key: String 183 | value: Bar 184 | } 185 | type MapsStringBytesEntry { 186 | key: String 187 | value: String 188 | } 189 | input MapsStringBytesEntryInput { 190 | key: String 191 | value: String 192 | } 193 | type MapsStringDoubleEntry { 194 | key: String 195 | value: Float 196 | } 197 | input MapsStringDoubleEntryInput { 198 | key: String 199 | value: Float 200 | } 201 | type MapsStringFloatEntry { 202 | key: String 203 | value: Float 204 | } 205 | input MapsStringFloatEntryInput { 206 | key: String 207 | value: Float 208 | } 209 | type MapsStringFooEntry { 210 | key: String 211 | value: Foo 212 | } 213 | input MapsStringFooEntryInput { 214 | key: String 215 | value: FooInput 216 | } 217 | type MapsStringStringEntry { 218 | key: String 219 | value: String 220 | } 221 | input MapsStringStringEntryInput { 222 | key: String 223 | value: String 224 | } 225 | type MapsUint32Uint32Entry { 226 | key: Int 227 | value: Int 228 | } 229 | input MapsUint32Uint32EntryInput { 230 | key: Int 231 | value: Int 232 | } 233 | type MapsUint64Uint64Entry { 234 | key: Int 235 | value: Int 236 | } 237 | input MapsUint64Uint64EntryInput { 238 | key: Int 239 | value: Int 240 | } 241 | type Mutation { 242 | newName(in: DataInput): Data 243 | } 244 | interface Node { 245 | id: ID! 246 | } 247 | type Oneof { 248 | param1: String 249 | Oneof1: constructs_Oneof_Oneof1 250 | Oneof2: constructs_Oneof_Oneof2 251 | Oneof3: constructs_Oneof_Oneof3 252 | } 253 | input OneofInput { 254 | param1: String 255 | param2: String 256 | param3: String 257 | param4: String 258 | param5: String 259 | param6: String 260 | } 261 | type Query { 262 | queryQuery1(in: DataInput): Data 263 | queryQuery2(in: DataInput): Data 264 | node(id: ID!): Node 265 | } 266 | type Ref { 267 | localTime2: Timestamp 268 | external: googleprotobufTimestamp 269 | localTime: Timestamp 270 | file: Baz 271 | fileMsg: Foo 272 | fileEnum: Bar 273 | local: RefFoo 274 | foreign: FooFoo2 275 | en1: Ref_Foo_En 276 | en2: Ref_Foo_Bar_En 277 | gz: RefFooBazGz 278 | } 279 | type RefBar { 280 | param1: String 281 | } 282 | input RefBarInput { 283 | param1: String 284 | } 285 | type RefFoo { 286 | bar1: RefFooBar 287 | localTime2: Timestamp 288 | externalTime1: googleprotobufTimestamp 289 | bar2: RefBar 290 | en1: Ref_Foo_En 291 | en2: Ref_Foo_Bar_En 292 | } 293 | type RefFooBar { 294 | param1: String 295 | } 296 | input RefFooBarInput { 297 | param1: String 298 | } 299 | type RefFooBazGz { 300 | param1: String 301 | } 302 | input RefFooBazGzInput { 303 | param1: String 304 | } 305 | input RefFooInput { 306 | bar1: RefFooBarInput 307 | localTime2: TimestampInput 308 | externalTime1: googleprotobufTimestampInput 309 | bar2: RefBarInput 310 | en1: Ref_Foo_En 311 | en2: Ref_Foo_Bar_En 312 | } 313 | input RefInput { 314 | localTime2: TimestampInput 315 | external: googleprotobufTimestampInput 316 | localTime: TimestampInput 317 | file: BazInput 318 | fileMsg: FooInput 319 | fileEnum: Bar 320 | local: RefFooInput 321 | foreign: FooFoo2Input 322 | en1: Ref_Foo_En 323 | en2: Ref_Foo_Bar_En 324 | gz: RefFooBazGzInput 325 | } 326 | enum Ref_Foo_Bar_En { 327 | A0 328 | A1 329 | } 330 | enum Ref_Foo_En { 331 | A0 332 | A1 333 | } 334 | type Repeated { 335 | double: [Float!] 336 | float: [Float!] 337 | int32: [Int!] 338 | int64: [Int!] 339 | uint32: [Int!] 340 | uint64: [Int!] 341 | sint32: [Int!] 342 | sint64: [Int!] 343 | fixed32: [Int!] 344 | fixed64: [Int!] 345 | sfixed32: [Int!] 346 | sfixed64: [Int!] 347 | bool: [Boolean!] 348 | stringX: [String!] 349 | bytes: [String!] 350 | foo: [Foo!] 351 | bar: [Bar!] 352 | } 353 | input RepeatedInput { 354 | double: [Float!] 355 | float: [Float!] 356 | int32: [Int!] 357 | int64: [Int!] 358 | uint32: [Int!] 359 | uint64: [Int!] 360 | sint32: [Int!] 361 | sint64: [Int!] 362 | fixed32: [Int!] 363 | fixed64: [Int!] 364 | sfixed32: [Int!] 365 | sfixed64: [Int!] 366 | bool: [Boolean!] 367 | stringX: [String!] 368 | bytes: [String!] 369 | foo: [FooInput!] 370 | bar: [Bar!] 371 | } 372 | type Scalars { 373 | double: Float 374 | float: Float 375 | int32: Int 376 | int64: Int 377 | uint32: Int 378 | uint64: Int 379 | sint32: Int 380 | sint64: Int 381 | fixed32: Int 382 | fixed64: Int 383 | sfixed32: Int 384 | sfixed64: Int 385 | bool: Boolean 386 | stringX: String 387 | bytes: String 388 | paths: googleprotobufFieldMask 389 | } 390 | input ScalarsInput { 391 | double: Float 392 | float: Float 393 | int32: Int 394 | int64: Int 395 | uint32: Int 396 | uint64: Int 397 | sint32: Int 398 | sint64: Int 399 | fixed32: Int 400 | fixed64: Int 401 | sfixed32: Int 402 | sfixed64: Int 403 | bool: Boolean 404 | stringX: String 405 | bytes: String 406 | paths: googleprotobufFieldMaskInput 407 | } 408 | type Timestamp { 409 | time: String 410 | } 411 | input TimestampInput { 412 | time: String 413 | } 414 | union constructs_Oneof_Oneof1 = constructs_Oneof_param2 | constructs_Oneof_param3 415 | union constructs_Oneof_Oneof2 = constructs_Oneof_param4 | constructs_Oneof_param5 416 | union constructs_Oneof_Oneof3 = constructs_Oneof_param6 417 | type constructs_Oneof_param2 { 418 | param2: String 419 | } 420 | type constructs_Oneof_param3 { 421 | param3: String 422 | } 423 | type constructs_Oneof_param4 { 424 | param4: String 425 | } 426 | type constructs_Oneof_param5 { 427 | param5: String 428 | } 429 | type constructs_Oneof_param6 { 430 | param6: String 431 | } 432 | type googleprotobufFieldMask { 433 | paths: [String!] 434 | } 435 | input googleprotobufFieldMaskInput { 436 | paths: [String!] 437 | } 438 | type googleprotobufTimestamp { 439 | seconds: Int 440 | nanos: Int 441 | } 442 | input googleprotobufTimestampInput { 443 | seconds: Int 444 | nanos: Int 445 | } 446 | -------------------------------------------------------------------------------- /test/integration/testdata/gateway-generate-without-unbound-method.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | The @defer directive may be specified on a fragment spread to imply de-prioritization, that causes the fragment to be omitted in the initial response, and delivered as a subsequent response afterward. A query with @defer directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred delivered in a subsequent response. @include and @skip take precedence over @defer. 3 | """ 4 | directive @defer(if: Boolean = true, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT 5 | """ 6 | The @specifiedBy built-in directive is used within the type system definition language to provide a scalar specification URL for specifying the behavior of custom scalar types. 7 | """ 8 | directive @specifiedBy(url: String!) on SCALAR 9 | scalar Any 10 | input AnyInput { 11 | param1: String 12 | } 13 | type AnyInputOutput { 14 | any: Any 15 | } 16 | enum Bar { 17 | BAR1 18 | BAR2 19 | BAR3 20 | } 21 | type Baz { 22 | param1: String 23 | } 24 | input BazInput { 25 | param1: String 26 | } 27 | type Data { 28 | stringX: String! 29 | foo: Foo2! 30 | double: [Float!]! 31 | string2: String 32 | foo2: Foo2 33 | double2: [Float!] 34 | bar: String 35 | string: String 36 | } 37 | input DataInput { 38 | stringX: String! 39 | foo: Foo2Input! 40 | double: [Float!]! 41 | string2: String 42 | foo2: Foo2Input 43 | double2: [Float!] 44 | bar: String 45 | string: String 46 | } 47 | type Foo { 48 | param1: String 49 | param2: String 50 | } 51 | type Foo2 { 52 | param1: String 53 | } 54 | input Foo2Input { 55 | param1: String 56 | } 57 | type FooFoo2 { 58 | param1: String 59 | } 60 | input FooFoo2Input { 61 | param1: String 62 | } 63 | input FooInput { 64 | param1: String 65 | param2: String 66 | } 67 | type Maps { 68 | int32Int32: [MapsInt32Int32Entry!] 69 | int64Int64: [MapsInt64Int64Entry!] 70 | uint32Uint32: [MapsUint32Uint32Entry!] 71 | uint64Uint64: [MapsUint64Uint64Entry!] 72 | sint32Sint32: [MapsSint32Sint32Entry!] 73 | sint64Sint64: [MapsSint64Sint64Entry!] 74 | fixed32Fixed32: [MapsFixed32Fixed32Entry!] 75 | fixed64Fixed64: [MapsFixed64Fixed64Entry!] 76 | sfixed32Sfixed32: [MapsSfixed32Sfixed32Entry!] 77 | sfixed64Sfixed64: [MapsSfixed64Sfixed64Entry!] 78 | boolBool: [MapsBoolBoolEntry!] 79 | stringString: [MapsStringStringEntry!] 80 | stringBytes: [MapsStringBytesEntry!] 81 | stringFloat: [MapsStringFloatEntry!] 82 | stringDouble: [MapsStringDoubleEntry!] 83 | stringFoo: [MapsStringFooEntry!] 84 | stringBar: [MapsStringBarEntry!] 85 | } 86 | type MapsBoolBoolEntry { 87 | key: Boolean 88 | value: Boolean 89 | } 90 | input MapsBoolBoolEntryInput { 91 | key: Boolean 92 | value: Boolean 93 | } 94 | type MapsFixed32Fixed32Entry { 95 | key: Int 96 | value: Int 97 | } 98 | input MapsFixed32Fixed32EntryInput { 99 | key: Int 100 | value: Int 101 | } 102 | type MapsFixed64Fixed64Entry { 103 | key: Int 104 | value: Int 105 | } 106 | input MapsFixed64Fixed64EntryInput { 107 | key: Int 108 | value: Int 109 | } 110 | input MapsInput { 111 | int32Int32: [MapsInt32Int32EntryInput!] 112 | int64Int64: [MapsInt64Int64EntryInput!] 113 | uint32Uint32: [MapsUint32Uint32EntryInput!] 114 | uint64Uint64: [MapsUint64Uint64EntryInput!] 115 | sint32Sint32: [MapsSint32Sint32EntryInput!] 116 | sint64Sint64: [MapsSint64Sint64EntryInput!] 117 | fixed32Fixed32: [MapsFixed32Fixed32EntryInput!] 118 | fixed64Fixed64: [MapsFixed64Fixed64EntryInput!] 119 | sfixed32Sfixed32: [MapsSfixed32Sfixed32EntryInput!] 120 | sfixed64Sfixed64: [MapsSfixed64Sfixed64EntryInput!] 121 | boolBool: [MapsBoolBoolEntryInput!] 122 | stringString: [MapsStringStringEntryInput!] 123 | stringBytes: [MapsStringBytesEntryInput!] 124 | stringFloat: [MapsStringFloatEntryInput!] 125 | stringDouble: [MapsStringDoubleEntryInput!] 126 | stringFoo: [MapsStringFooEntryInput!] 127 | stringBar: [MapsStringBarEntryInput!] 128 | } 129 | type MapsInt32Int32Entry { 130 | key: Int 131 | value: Int 132 | } 133 | input MapsInt32Int32EntryInput { 134 | key: Int 135 | value: Int 136 | } 137 | type MapsInt64Int64Entry { 138 | key: Int 139 | value: Int 140 | } 141 | input MapsInt64Int64EntryInput { 142 | key: Int 143 | value: Int 144 | } 145 | type MapsSfixed32Sfixed32Entry { 146 | key: Int 147 | value: Int 148 | } 149 | input MapsSfixed32Sfixed32EntryInput { 150 | key: Int 151 | value: Int 152 | } 153 | type MapsSfixed64Sfixed64Entry { 154 | key: Int 155 | value: Int 156 | } 157 | input MapsSfixed64Sfixed64EntryInput { 158 | key: Int 159 | value: Int 160 | } 161 | type MapsSint32Sint32Entry { 162 | key: Int 163 | value: Int 164 | } 165 | input MapsSint32Sint32EntryInput { 166 | key: Int 167 | value: Int 168 | } 169 | type MapsSint64Sint64Entry { 170 | key: Int 171 | value: Int 172 | } 173 | input MapsSint64Sint64EntryInput { 174 | key: Int 175 | value: Int 176 | } 177 | type MapsStringBarEntry { 178 | key: String 179 | value: Bar 180 | } 181 | input MapsStringBarEntryInput { 182 | key: String 183 | value: Bar 184 | } 185 | type MapsStringBytesEntry { 186 | key: String 187 | value: String 188 | } 189 | input MapsStringBytesEntryInput { 190 | key: String 191 | value: String 192 | } 193 | type MapsStringDoubleEntry { 194 | key: String 195 | value: Float 196 | } 197 | input MapsStringDoubleEntryInput { 198 | key: String 199 | value: Float 200 | } 201 | type MapsStringFloatEntry { 202 | key: String 203 | value: Float 204 | } 205 | input MapsStringFloatEntryInput { 206 | key: String 207 | value: Float 208 | } 209 | type MapsStringFooEntry { 210 | key: String 211 | value: Foo 212 | } 213 | input MapsStringFooEntryInput { 214 | key: String 215 | value: FooInput 216 | } 217 | type MapsStringStringEntry { 218 | key: String 219 | value: String 220 | } 221 | input MapsStringStringEntryInput { 222 | key: String 223 | value: String 224 | } 225 | type MapsUint32Uint32Entry { 226 | key: Int 227 | value: Int 228 | } 229 | input MapsUint32Uint32EntryInput { 230 | key: Int 231 | value: Int 232 | } 233 | type MapsUint64Uint64Entry { 234 | key: Int 235 | value: Int 236 | } 237 | input MapsUint64Uint64EntryInput { 238 | key: Int 239 | value: Int 240 | } 241 | type Mutation { 242 | newName(in: DataInput): Data 243 | } 244 | interface Node { 245 | id: ID! 246 | } 247 | type Oneof { 248 | param1: String 249 | Oneof1: constructs_Oneof_Oneof1 250 | Oneof2: constructs_Oneof_Oneof2 251 | Oneof3: constructs_Oneof_Oneof3 252 | } 253 | input OneofInput { 254 | param1: String 255 | param2: String 256 | param3: String 257 | param4: String 258 | param5: String 259 | param6: String 260 | } 261 | type Query { 262 | queryQuery1(in: DataInput): Data 263 | queryQuery2(in: DataInput): Data 264 | node(id: ID!): Node 265 | } 266 | type Ref { 267 | localTime2: Timestamp 268 | external: googleprotobufTimestamp 269 | localTime: Timestamp 270 | file: Baz 271 | fileMsg: Foo 272 | fileEnum: Bar 273 | local: RefFoo 274 | foreign: FooFoo2 275 | en1: Ref_Foo_En 276 | en2: Ref_Foo_Bar_En 277 | gz: RefFooBazGz 278 | } 279 | type RefBar { 280 | param1: String 281 | } 282 | input RefBarInput { 283 | param1: String 284 | } 285 | type RefFoo { 286 | bar1: RefFooBar 287 | localTime2: Timestamp 288 | externalTime1: googleprotobufTimestamp 289 | bar2: RefBar 290 | en1: Ref_Foo_En 291 | en2: Ref_Foo_Bar_En 292 | } 293 | type RefFooBar { 294 | param1: String 295 | } 296 | input RefFooBarInput { 297 | param1: String 298 | } 299 | type RefFooBazGz { 300 | param1: String 301 | } 302 | input RefFooBazGzInput { 303 | param1: String 304 | } 305 | input RefFooInput { 306 | bar1: RefFooBarInput 307 | localTime2: TimestampInput 308 | externalTime1: googleprotobufTimestampInput 309 | bar2: RefBarInput 310 | en1: Ref_Foo_En 311 | en2: Ref_Foo_Bar_En 312 | } 313 | input RefInput { 314 | localTime2: TimestampInput 315 | external: googleprotobufTimestampInput 316 | localTime: TimestampInput 317 | file: BazInput 318 | fileMsg: FooInput 319 | fileEnum: Bar 320 | local: RefFooInput 321 | foreign: FooFoo2Input 322 | en1: Ref_Foo_En 323 | en2: Ref_Foo_Bar_En 324 | gz: RefFooBazGzInput 325 | } 326 | enum Ref_Foo_Bar_En { 327 | A0 328 | A1 329 | } 330 | enum Ref_Foo_En { 331 | A0 332 | A1 333 | } 334 | type Repeated { 335 | double: [Float!] 336 | float: [Float!] 337 | int32: [Int!] 338 | int64: [Int!] 339 | uint32: [Int!] 340 | uint64: [Int!] 341 | sint32: [Int!] 342 | sint64: [Int!] 343 | fixed32: [Int!] 344 | fixed64: [Int!] 345 | sfixed32: [Int!] 346 | sfixed64: [Int!] 347 | bool: [Boolean!] 348 | stringX: [String!] 349 | bytes: [String!] 350 | foo: [Foo!] 351 | bar: [Bar!] 352 | } 353 | input RepeatedInput { 354 | double: [Float!] 355 | float: [Float!] 356 | int32: [Int!] 357 | int64: [Int!] 358 | uint32: [Int!] 359 | uint64: [Int!] 360 | sint32: [Int!] 361 | sint64: [Int!] 362 | fixed32: [Int!] 363 | fixed64: [Int!] 364 | sfixed32: [Int!] 365 | sfixed64: [Int!] 366 | bool: [Boolean!] 367 | stringX: [String!] 368 | bytes: [String!] 369 | foo: [FooInput!] 370 | bar: [Bar!] 371 | } 372 | type Scalars { 373 | double: Float 374 | float: Float 375 | int32: Int 376 | int64: Int 377 | uint32: Int 378 | uint64: Int 379 | sint32: Int 380 | sint64: Int 381 | fixed32: Int 382 | fixed64: Int 383 | sfixed32: Int 384 | sfixed64: Int 385 | bool: Boolean 386 | stringX: String 387 | bytes: String 388 | paths: googleprotobufFieldMask 389 | } 390 | input ScalarsInput { 391 | double: Float 392 | float: Float 393 | int32: Int 394 | int64: Int 395 | uint32: Int 396 | uint64: Int 397 | sint32: Int 398 | sint64: Int 399 | fixed32: Int 400 | fixed64: Int 401 | sfixed32: Int 402 | sfixed64: Int 403 | bool: Boolean 404 | stringX: String 405 | bytes: String 406 | paths: googleprotobufFieldMaskInput 407 | } 408 | type Timestamp { 409 | time: String 410 | } 411 | input TimestampInput { 412 | time: String 413 | } 414 | union constructs_Oneof_Oneof1 = constructs_Oneof_param2 | constructs_Oneof_param3 415 | union constructs_Oneof_Oneof2 = constructs_Oneof_param4 | constructs_Oneof_param5 416 | union constructs_Oneof_Oneof3 = constructs_Oneof_param6 417 | type constructs_Oneof_param2 { 418 | param2: String 419 | } 420 | type constructs_Oneof_param3 { 421 | param3: String 422 | } 423 | type constructs_Oneof_param4 { 424 | param4: String 425 | } 426 | type constructs_Oneof_param5 { 427 | param5: String 428 | } 429 | type constructs_Oneof_param6 { 430 | param6: String 431 | } 432 | type googleprotobufFieldMask { 433 | paths: [String!] 434 | } 435 | input googleprotobufFieldMaskInput { 436 | paths: [String!] 437 | } 438 | type googleprotobufTimestamp { 439 | seconds: Int 440 | nanos: Int 441 | } 442 | input googleprotobufTimestampInput { 443 | seconds: Int 444 | nanos: Int 445 | } 446 | -------------------------------------------------------------------------------- /test/integration/testdata/options-expect.graphql: -------------------------------------------------------------------------------- 1 | directive @Query on FIELD_DEFINITION 2 | directive @Service on FIELD_DEFINITION 3 | directive @Test on FIELD_DEFINITION 4 | directive @goField(forceResolver: Boolean, name: String) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION 5 | type Data { 6 | stringX: String! 7 | foo: Foo2! 8 | double: [Float!]! 9 | string2: String 10 | foo2: Foo2 11 | double2: [Float!] 12 | bars: String @goField(name: "Bar") 13 | str: String @goField(name: "String_") 14 | } 15 | input DataInput { 16 | stringX: String! 17 | foo: Foo2Input! 18 | double: [Float!]! 19 | string2: String 20 | foo2: Foo2Input 21 | double2: [Float!] 22 | bars: String @goField(name: "Bar") 23 | str: String @goField(name: "String_") 24 | } 25 | type Foo2 { 26 | param1: String 27 | } 28 | input Foo2Input { 29 | param1: String 30 | } 31 | type Mutation { 32 | serviceMutate1(in: DataInput): Data @Service 33 | serviceMutate2(in: DataInput): Data @Service 34 | servicePublish(in: DataInput): Data @Service 35 | servicePubSub1(in: DataInput): Data @Service 36 | serviceInvalidSubscribe3(in: DataInput): Data @Service 37 | servicePubSub2(in: DataInput): Data @Service 38 | newName(in: DataInput): Data @Service 39 | name(in: DataInput): Data @Test 40 | newName0(in: DataInput): Data @Test 41 | queryMutate1(in: DataInput): Data @Query 42 | } 43 | type Query { 44 | serviceQuery1(in: DataInput): Data @Service 45 | serviceInvalidSubscribe1(in: DataInput): Data @Service 46 | queryQuery1(in: DataInput): Data @Query 47 | queryQuery2(in: DataInput): Data @Query 48 | } 49 | type Subscription { 50 | serviceSubscribe(in: DataInput): Data @Service 51 | servicePubSub1(in: DataInput): Data @Service 52 | serviceInvalidSubscribe2(in: DataInput): Data @Service 53 | serviceInvalidSubscribe3(in: DataInput): Data @Service 54 | servicePubSub2(in: DataInput): Data @Service 55 | querySubscribe(in: DataInput): Data @Query 56 | } 57 | -------------------------------------------------------------------------------- /test/post1.json: -------------------------------------------------------------------------------- 1 | { 2 | "query": "{\n serviceQuery1(in: {\n string: \"sdfs\",\n foo: {\n param1: \"para\"\n },\n string2: \"ssdfsd\",\n float: [111]\n }) {\n string,string2,float , foo {\n param1\n }\n }\n}" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /test/post2.json: -------------------------------------------------------------------------------- 1 | { 2 | "query": "{\n serviceQuery1(in: {\n string: \"sdfs\",\n foo: {\n param1: \"para\"\n },\n string2: \"ssdfsd\",\n float: [111]\n }) {\n string,string2,float , foo {\n param1\n }\n }\n, serviceQuery2(in: {\n string: \"sdfs\",\n foo: {\n param1: \"para\"\n },\n string2: \"ssdfsd\",\n float: [111]\n }) {\n string,string2,float , foo {\n param1\n }\n }\n}" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /test/util.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | 12 | "github.com/go-kod/grpc-gateway/api/example/helloworld" 13 | pb "github.com/go-kod/grpc-gateway/api/test" 14 | "github.com/go-kod/grpc-gateway/internal/server" 15 | "github.com/stretchr/testify/require" 16 | "google.golang.org/grpc" 17 | "google.golang.org/grpc/metadata" 18 | "google.golang.org/grpc/reflection" 19 | "google.golang.org/protobuf/proto" 20 | "google.golang.org/protobuf/protoadapt" 21 | "google.golang.org/protobuf/reflect/protoreflect" 22 | "google.golang.org/protobuf/runtime/protoiface" 23 | "google.golang.org/protobuf/types/known/anypb" 24 | "google.golang.org/protobuf/types/known/emptypb" 25 | "google.golang.org/protobuf/types/known/fieldmaskpb" 26 | ) 27 | 28 | type DepsInfo struct { 29 | OptionsServerAddr net.Listener 30 | ConstructsServerAddr net.Listener 31 | HelloworldServerAddr net.Listener 32 | OptionServer *grpc.Server 33 | ConstructServer *grpc.Server 34 | HelloworldServer *grpc.Server 35 | } 36 | 37 | func SetupGateway(t testing.TB, s server.Gateway) string { 38 | serverCh := make(chan net.Addr) 39 | go func() { 40 | l, err := net.Listen("tcp", ":0") 41 | require.Nil(t, err) 42 | go func() { 43 | time.Sleep(10 * time.Millisecond) 44 | serverCh <- l.Addr() 45 | }() 46 | 47 | _, _ = s.BuildHTTPServer() 48 | handler, err := s.BuildServer() 49 | require.Nil(t, err) 50 | _ = http.Serve(l, handler) 51 | }() 52 | gatewayServer := <-serverCh 53 | 54 | gatewayUrl := fmt.Sprintf("http://127.0.0.1:%d/query", gatewayServer.(*net.TCPAddr).Port) 55 | return gatewayUrl 56 | } 57 | 58 | func SetupDeps(t testing.TB) DepsInfo { 59 | var ( 60 | optionsServerCh = make(chan net.Listener) 61 | constructsServerCh = make(chan net.Listener) 62 | HelloworldServerCh = make(chan net.Listener) 63 | 64 | optionsServer atomic.Pointer[*grpc.Server] 65 | constructsServer atomic.Pointer[*grpc.Server] 66 | helloworldServer atomic.Pointer[*grpc.Server] 67 | ) 68 | go func() { 69 | l, err := net.Listen("tcp", "localhost:0") 70 | require.Nil(t, err) 71 | go func() { 72 | time.Sleep(10 * time.Millisecond) 73 | optionsServerCh <- l 74 | }() 75 | s := grpc.NewServer() 76 | pb.RegisterServiceServer(s, &optionsServiceMock{}) 77 | pb.RegisterQueryServer(s, &optionsQueryMock{}) 78 | reflection.Register(s) 79 | optionsServer.Store(&s) 80 | _ = s.Serve(l) 81 | }() 82 | go func() { 83 | l, err := net.Listen("tcp", "localhost:0") 84 | require.Nil(t, err) 85 | go func() { 86 | time.Sleep(10 * time.Millisecond) 87 | constructsServerCh <- l 88 | }() 89 | s := grpc.NewServer() 90 | pb.RegisterConstructsServer(s, constructsServiceMock{}) 91 | reflection.Register(s) 92 | constructsServer.Store(&s) 93 | _ = s.Serve(l) 94 | }() 95 | go func() { 96 | l, err := net.Listen("tcp", "localhost:0") 97 | require.Nil(t, err) 98 | go func() { 99 | time.Sleep(10 * time.Millisecond) 100 | HelloworldServerCh <- l 101 | }() 102 | s := grpc.NewServer() 103 | helloworld.RegisterGreeterServer(s, &helloworldService{}) 104 | reflection.Register(s) 105 | helloworldServer.Store(&s) 106 | _ = s.Serve(l) 107 | }() 108 | 109 | return DepsInfo{ 110 | OptionsServerAddr: <-optionsServerCh, 111 | ConstructsServerAddr: <-constructsServerCh, 112 | HelloworldServerAddr: <-HelloworldServerCh, 113 | OptionServer: *optionsServer.Load(), 114 | ConstructServer: *constructsServer.Load(), 115 | HelloworldServer: *helloworldServer.Load(), 116 | } 117 | } 118 | 119 | type constructsServiceMock struct { 120 | pb.ConstructsServer 121 | } 122 | 123 | // FilterMessageFields 根据 FieldMask 过滤并返回指定字段的消息 124 | func FilterMessageFields(original protoiface.MessageV1, mask *fieldmaskpb.FieldMask) protoiface.MessageV1 { 125 | if mask == nil { 126 | return original 127 | } 128 | 129 | // 创建一个新的空消息,用于存储筛选后的字段 130 | filtered := proto.Clone(protoadapt.MessageV2Of(original)) 131 | filteredReflect := filtered.ProtoReflect() 132 | 133 | maskMap := make(map[string]struct{}) 134 | for _, path := range mask.GetPaths() { 135 | maskMap[path] = struct{}{} 136 | } 137 | 138 | // 清空不在mask的字段 139 | filteredReflect.Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool { 140 | if _, ok := maskMap[fd.JSONName()]; !ok { 141 | filteredReflect.Clear(fd) 142 | } 143 | return true 144 | }) 145 | 146 | return protoadapt.MessageV1Of(filtered) 147 | } 148 | 149 | func (c constructsServiceMock) Scalars_(ctx context.Context, scalars *pb.Scalars) (*pb.Scalars, error) { 150 | newScalers := FilterMessageFields(scalars, scalars.GetPaths()) 151 | return newScalers.(*pb.Scalars), nil 152 | } 153 | 154 | func (c constructsServiceMock) Repeated_(ctx context.Context, repeated *pb.Repeated) (*pb.Repeated, error) { 155 | return repeated, nil 156 | } 157 | 158 | func (c constructsServiceMock) Maps_(ctx context.Context, maps *pb.Maps) (*pb.Maps, error) { 159 | return maps, nil 160 | } 161 | 162 | func (c constructsServiceMock) Any_(ctx context.Context, any *anypb.Any) (*anypb.Any, error) { 163 | return any, nil 164 | } 165 | 166 | func (c constructsServiceMock) Empty_(ctx context.Context, empty *emptypb.Empty) (*pb.Empty, error) { 167 | return &pb.Empty{}, nil 168 | } 169 | 170 | func (c constructsServiceMock) Empty2_(ctx context.Context, recursive *pb.EmptyRecursive) (*pb.EmptyNested, error) { 171 | return &pb.EmptyNested{}, nil 172 | } 173 | 174 | func (c constructsServiceMock) Empty3_(ctx context.Context, empty3 *pb.Empty3) (*pb.Empty3, error) { 175 | return &pb.Empty3{}, nil 176 | } 177 | 178 | func (c constructsServiceMock) Ref_(ctx context.Context, ref *pb.Ref) (*pb.Ref, error) { 179 | return &pb.Ref{}, nil 180 | } 181 | 182 | func (c constructsServiceMock) Oneof_(ctx context.Context, oneof *pb.Oneof) (*pb.Oneof, error) { 183 | return oneof, nil 184 | } 185 | 186 | func (c constructsServiceMock) CallWithId(ctx context.Context, empty *pb.Empty) (*pb.Empty, error) { 187 | return &pb.Empty{}, nil 188 | } 189 | 190 | type optionsServiceMock struct { 191 | pb.ServiceServer 192 | } 193 | 194 | func (o *optionsServiceMock) Mutate1(ctx context.Context, data *pb.Data) (*pb.Data, error) { 195 | return data, nil 196 | } 197 | 198 | func (o *optionsServiceMock) Mutate2(ctx context.Context, data *pb.Data) (*pb.Data, error) { 199 | return data, nil 200 | } 201 | 202 | func (o *optionsServiceMock) Query1(ctx context.Context, data *pb.Data) (*pb.Data, error) { 203 | return data, nil 204 | } 205 | 206 | func (o *optionsServiceMock) Ignore(ctx context.Context, data *pb.Data) (*pb.Data, error) { 207 | return data, nil 208 | } 209 | 210 | func (o *optionsServiceMock) Name(ctx context.Context, data *pb.Data) (*pb.Data, error) { 211 | return data, nil 212 | } 213 | 214 | type optionsQueryMock struct { 215 | pb.UnimplementedQueryServer 216 | } 217 | 218 | func (o *optionsQueryMock) Query1(ctx context.Context, data *pb.Data) (*pb.Data, error) { 219 | fmt.Println(metadata.FromIncomingContext(ctx)) 220 | return data, nil 221 | } 222 | 223 | func (o *optionsQueryMock) Query2(ctx context.Context, data *pb.Data) (*pb.Data, error) { 224 | fmt.Println(metadata.FromIncomingContext(ctx)) 225 | return data, nil 226 | } 227 | 228 | type helloworldService struct { 229 | helloworld.UnimplementedGreeterServer 230 | } 231 | 232 | func (s *helloworldService) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) { 233 | if req.Name == "error" { 234 | return nil, fmt.Errorf("error") 235 | } 236 | 237 | return &helloworld.HelloReply{ 238 | Message: "Hello " + req.Name, 239 | }, nil 240 | } 241 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package main 5 | 6 | import ( 7 | _ "github.com/go-kod/kod/cmd/kod" 8 | _ "go.uber.org/mock/mockgen" 9 | ) 10 | --------------------------------------------------------------------------------