├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── api
├── test.proto
└── testpb
│ ├── test.pb.go
│ └── test_grpc.pb.go
├── benchmarks
├── README.md
├── api
│ ├── library.proto
│ ├── librarypb
│ │ ├── library.pb.go
│ │ ├── library.pb.gw.go
│ │ ├── library.twirp.go
│ │ ├── library_grpc.pb.go
│ │ └── librarypbconnect
│ │ │ └── library.connect.go
│ └── proto.pb
├── bench.txt
├── benchmain
│ └── main.go
├── connect_svc.go
├── gen.sh
├── go.mod
├── go.sum
├── grpc-bench.txt
├── main.go
├── main_test.go
├── server
│ └── server.go
├── svc.go
├── testdata
│ └── envoy.yaml
└── twirp_test.go
├── docs
├── .gitignore
├── LARK_PROTOCOL.md
├── intro.md
├── larking.svg
└── main.go
├── examples
├── BUILD.bazel
├── WORKSPACE
├── api
│ ├── BUILD.bazel
│ ├── hellostream.proto
│ ├── helloworld.proto
│ └── helloworld
│ │ ├── helloworld.pb.go
│ │ └── helloworld_grpc.pb.go
├── cpp
│ └── helloworld
│ │ ├── BUILD.bazel
│ │ └── hello_world.cc
├── deps.bzl
├── gen.sh
├── go.mod
├── go
│ └── helloworld
│ │ ├── BUILD.bazel
│ │ └── main.go
├── java
│ └── com
│ │ └── example
│ │ └── helloworld
│ │ ├── BUILD.bazel
│ │ └── HelloWorldServer.java
└── proto
│ └── hellostream
│ ├── hellostream.pb.go
│ └── hellostream_grpc.pb.go
├── gen.go
├── gen.sh
├── go.mod
├── go.sum
├── health
└── health.go
└── larking
├── bench.txt
├── code.go
├── codec.go
├── codec_test.go
├── compress.go
├── grpc.go
├── grpc_test.go
├── handler.go
├── handler_test.go
├── http.go
├── http_test.go
├── lexer.go
├── lexer_test.go
├── log.go
├── mux.go
├── mux_test.go
├── negotiate.go
├── negotiate_test.go
├── proxy.go
├── proxy_test.go
├── rules.go
├── rules_test.go
├── server.go
├── server_test.go
├── stats.go
├── web.go
├── web_test.go
├── websocket.go
└── websocket_test.go
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | tags:
5 | - v*
6 | branches:
7 | - master
8 | - main
9 | pull_request:
10 | jobs:
11 | test:
12 | strategy:
13 | matrix:
14 | go-version: [1.23.x]
15 | os: [ubuntu-latest] #, macos-latest, windows-latest]
16 | runs-on: ${{ matrix.os }}
17 | steps:
18 | - name: Install Go
19 | uses: actions/setup-go@v5
20 | with:
21 | go-version: ${{ matrix.go-version }}
22 | - name: Checkout code
23 | uses: actions/checkout@v4
24 | - name: Test
25 | run: go test -race ./larking
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bazel-*
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Edward McFarlane. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | 1. Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the
13 | distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived
17 | from this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | _,
3 | ( '> Welcome to larking.io
4 | / ) )
5 | /|^^
6 | ```
7 | [](https://pkg.go.dev/larking.io/larking)
8 |
9 | Larking is a [protoreflect](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect) gRPC-transcoding implementation with support for gRPC, gRPC-web and twirp protocols.
10 | Bind [`google.api.http`](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto) annotations to gRPC services without code generation.
11 | Works with existing go-protobuf generators
12 | [`protoc-gen-go`](https://pkg.go.dev/google.golang.org/protobuf@v1.30.0/cmd/protoc-gen-go) and
13 | [`protoc-gen-go-grpc`](https://pkg.go.dev/google.golang.org/grpc/cmd/protoc-gen-go-grpc)
14 | and Go's std library net/http stack.
15 | Bind to local services or proxy to other gRPC servers using gRPC server reflection.
16 | Use Google's [API design guide](https://cloud.google.com/apis/design) to design beautiful RESTful APIs for your gRPC services.
17 |
18 | - Supports [gRPC](https://grpc.io) clients
19 | - Supports [gRPC-transcoding](https://cloud.google.com/endpoints/docs/grpc/transcoding) clients
20 | - Supports [gRPC-web](https://github.com/grpc/grpc-web) clients
21 | - Supports [twirp](https://github.com/twitchtv/twirp) clients
22 | - Proxy gRPC servers with gRPC [server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md)
23 | - Implicit `/GRPC_SERVICE_FULL_NAME/METHOD_NAME` for all methods
24 | - Google API service configuration [syntax](https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#using-grpc-api-service-configuration)
25 | - Websocket streaming with `websocket` kind annotations
26 | - Content streaming with `google.api.HttpBody`
27 | - Streaming support with [StreamCodec](https://github.com/emcfarlane/larking#streaming-codecs)
28 | - Fast with low allocations: see [benchmarks](https://github.com/emcfarlane/larking/tree/main/benchmarks)
29 |
30 |
31 |

32 |
33 |
34 | ## Install
35 |
36 | ```
37 | go get larking.io@latest
38 | ```
39 |
40 | ## Quickstart
41 |
42 | Compile protobuffers with go and go-grpc libraries. Follow the guide [here](https://grpc.io/docs/languages/go/quickstart/#prerequisites). No other pre-compiled libraries are required. We need to create a `larking.Mux` and optionally `larking.Server` to serve both gRPC and REST.
43 |
44 | This example builds a server with the health service:
45 |
46 | ```go
47 | package main
48 |
49 | import (
50 | "log"
51 | "net"
52 |
53 | "google.golang.org/genproto/googleapis/api/serviceconfig"
54 | healthpb "google.golang.org/grpc/health/grpc_health_v1"
55 | "larking.io/health"
56 | "larking.io/larking"
57 | )
58 |
59 | func main() {
60 | // Create a health service. The health service is used to check the status
61 | // of services running within the server.
62 | healthSvc := health.NewServer()
63 | healthSvc.SetServingStatus("example.up.Service", healthpb.HealthCheckResponse_SERVING)
64 | healthSvc.SetServingStatus("example.down.Service", healthpb.HealthCheckResponse_NOT_SERVING)
65 |
66 | serviceConfig := &serviceconfig.Service{}
67 | // AddHealthz adds a /v1/healthz endpoint to the service binding to the
68 | // grpc.health.v1.Health service:
69 | // - get /v1/healthz -> grpc.health.v1.Health.Check
70 | // - websocket /v1/healthz -> grpc.health.v1.Health.Watch
71 | health.AddHealthz(serviceConfig)
72 |
73 | // Mux impements http.Handler and serves both gRPC and HTTP connections.
74 | mux, err := larking.NewMux(
75 | larking.ServiceConfigOption(serviceConfig),
76 | )
77 | if err != nil {
78 | log.Fatal(err)
79 | }
80 | // RegisterHealthServer registers a HealthServer to the mux.
81 | healthpb.RegisterHealthServer(mux, healthSvc)
82 |
83 | // Server creates a *http.Server.
84 | svr, err := larking.NewServer(mux)
85 | if err != nil {
86 | log.Fatal(err)
87 | }
88 |
89 | // Listen on TCP port 8080 on all interfaces.
90 | lis, err := net.Listen("tcp", "localhost:8080")
91 | if err != nil {
92 | log.Fatalf("failed to listen: %v", err)
93 | }
94 | defer lis.Close()
95 |
96 | // Serve starts the server and blocks until the server stops.
97 | // http://localhost:8080/v1/healthz
98 | log.Println("gRPC & HTTP server listening on", lis.Addr())
99 | if err := svr.Serve(lis); err != nil {
100 | log.Fatalf("failed to serve: %v", err)
101 | }
102 | ```
103 |
104 | Running the service we can check the health endpoints with curl:
105 | ```sh
106 | > curl localhost:8080/grpc.health.v1.Health/Check
107 | {"status":"SERVING"}
108 | ```
109 |
110 | To filter by service you can post the body `{"service": "example.down.Service"}` or use query params:
111 | ```sh
112 | > curl 'localhost:8080/grpc.health.v1.Health/Check?service=example.down.Service'
113 | {"status":"NOT_SERVING"}
114 | ```
115 |
116 | We can also use the `/v1/healthz` endpoint added by `health.AddHealthz`:
117 | ```sh
118 | > curl 'localhost:8080/v1/healthz?service=example.up.Service'
119 | {"status":"SERVING"}
120 | ```
121 |
122 | ## Features
123 |
124 | Transcoding provides methods to bind gRPC endpoints to HTTP methods.
125 | An example service `Library`:
126 | ```protobuf
127 | package larking.example
128 |
129 | service Library {
130 | // GetBook returns a book from a shelf.
131 | rpc GetBook(GetBookRequest) returns (Book) {};
132 | }
133 | ```
134 |
135 | Implicit bindings are provided on all methods bound to the URL
136 | `/GRPC_SERVICE_FULL_NAME/METHOD_NAME`.
137 |
138 | For example to get the book with curl we can send a `POST` request:
139 | ```
140 | curl -XPOST http://domain/larking.example.Library/GetBook -d '{"name":"shelves/1/books/2"}
141 | ```
142 |
143 | We can also use any method with query parameters. The equivalent `GET` request:
144 | ```
145 | curl http://domain/larking.example.Library/GetBook?name=shelves/1/books/2
146 | ```
147 |
148 | As an annotation this syntax would be written as a custom http option:
149 | ```protobuf
150 | rpc GetBook(GetBookRequest) returns (Book) {
151 | option (google.api.http) = {
152 | custom : {kind : "*" path : "/larking.example.Library/GetBook"}
153 | body : "*"
154 | };
155 | };
156 | ```
157 |
158 | To get better URL semantics lets define a custom annotation:
159 | ```protobuf
160 | rpc GetBook(GetBookRequest) returns (Book) {
161 | option (google.api.http) = {
162 | get : "/v1/{name=shelves/*/books/*}"
163 | };
164 | };
165 | ```
166 |
167 | Now the equivalent `GET` request would be:
168 | ```
169 | curl http://domain/v1/shelves/1/books/2
170 | ```
171 |
172 | See the reference docs for [google.api.HttpRule](https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#google.api.HttpRule) type for all features.
173 |
174 | ### Extensions
175 | It aims to be a superset of the gRPC transcoding spec with better support for streaming. The implementation also aims to be simple and fast.
176 | API's should be easy to use and have low overhead.
177 | See the `benchmarks/` for details and comparisons.
178 |
179 | #### Arbitrary Content
180 |
181 | Send any content type with the protobuf type `google.api.HttpBody`.
182 | Request bodies are unmarshalled from the body with the `ContentType` header.
183 | Response bodies marshal to the body and set the `ContentType` header.
184 | For large requests streaming RPCs support chunking the file into multiple messages.
185 |
186 | ```protobuf
187 | import "google/api/httpbody.proto";
188 |
189 | service Files {
190 | // HTTP | gRPC
191 | // -----|-----
192 | // `POST /files/cat.jpg ` | `UploadDownload(filename: "cat.jpg", file:
193 | // { content_type: "image/jpeg", data: })"`
194 | rpc UploadDownload(UploadFileRequest) returns (google.api.HttpBody) {
195 | option (google.api.http) = {
196 | post : "/files/{filename}"
197 | body : "file"
198 | };
199 | }
200 | rpc LargeUploadDownload(stream UploadFileRequest)
201 | returns (stream google.api.HttpBody) {
202 | option (google.api.http) = {
203 | post : "/files/large/{filename}"
204 | body : "file"
205 | };
206 | }
207 | }
208 | message UploadFileRequest {
209 | string filename = 1;
210 | google.api.HttpBody file = 2;
211 | }
212 | ```
213 |
214 | To better support stream uploads use the `AsHTTPBodyReader` and `AsHTTPBodyWriter` methods.
215 | The returned `io.Reader` and `io.Writer` efficiently stream bytes for large messages.
216 | This methods only works on streaming requests or streaming responses for gRPC-transcoding streams.
217 | ```go
218 | // LargeUploadDownload echoes the request body as the response body with contentType.
219 | func (s *asHTTPBodyServer) LargeUploadDownload(stream testpb.Files_LargeUploadDownloadServer) error {
220 | var req testpb.UploadFileRequest
221 | r, _ := larking.AsHTTPBodyReader(stream, &req)
222 | log.Printf("got %s!", req.Filename)
223 |
224 | rsp := httpbody.HttpBody{
225 | ContentType: req.File.GetContentType(),
226 | }
227 | w, _ := larking.AsHTTPBodyWriter(stream, &rsp)
228 |
229 | _, err := io.Copy(w, r)
230 | return err
231 | }
232 | ```
233 |
234 | #### Websockets Annotations
235 | Annotate a custom method kind `websocket` to enable clients to upgrade connections. This enables streams to be bidirectional over a websocket connection.
236 | ```protobuf
237 | // Chatroom shows the websocket extension.
238 | service ChatRoom {
239 | rpc Chat(stream ChatMessage) returns (stream ChatMessage) {
240 | option (google.api.http) = {
241 | custom : {kind : "websocket" path : "/v1/{name=rooms/*}"}
242 | body : "*"
243 | };
244 | }
245 | }
246 | ```
247 |
248 | #### Streaming Codecs
249 | Streaming requests will upgrade the codec interface to read and write marshalled messages to the stream.
250 | Control of framing is given to the application on a per content type basis.
251 | If the underlying protocol has it's own message framing, like 'websockets', streaming codecs won't be used.
252 | Unlike gRPC streams where compression is _per message_ here the compression is per _stream_ so only a message delimiter is needed.
253 | See the [StreamCodec](https://pkg.go.dev/larking.io/larking#StreamCodec) docs for implementation details.
254 | - Protobuf messages use a varint delimiter encoding: ``.
255 | - JSON messages are delimited on the outer JSON braces `{}`.
256 | - Arbitrary content is delimited by the message size limit, chunking into bytes slices of length limit.
257 |
258 | To stream json we can append payloads together as a single payload:
259 | ```
260 | curl -XPOST http://larking.io/v1/streaming -d '{"message":"one"}{"message":"two"}'
261 | ```
262 | The above creates an input stream of two messages.
263 | (N.B. when using HTTP/1 fully bidirectional streaming is not possible. All stream messages must be written before receiving a response)
264 |
265 | To stream protobuf we can use [protodelim](https://pkg.go.dev/google.golang.org/protobuf@v1.30.0/encoding/protodelim) to read and write varint streams of messages. Similar libraries are found in other [languages](https://github.com/protocolbuffers/protobuf/issues/10229).
266 |
267 | #### Twirp
268 | Twirp is supported through gRPC-transcoding with the implicit methods.
269 | The implicit methods cover `POST /package.Service/Method/` matching the content types for both `application/json` and `application/proto`.
270 | Before the [v7 spec](https://twitchtv.github.io/twirp/docs/spec_v7.html) the URL required the `/twirp` prefix.
271 | We can encode the server to use a ServerOption:
272 | ```go
273 | svr, _ := larking.NewServer(mux,
274 | larking.MuxHandleOption("/", "/twirp"), // Serve mux on '/' and '/twirp'
275 | )
276 | ```
277 |
278 | Twirp [errors](https://twitchtv.github.io/twirp/docs/errors.html) are created from `*grpc.Status` errors.
279 | Code enums are mapped to twirp error strings and messages. Currently meta fields aren't supported.
280 | Errors will only be converted to twirp errors when the header `Twirp-Version` is set.
281 | This is used to identify a twirp request.
282 |
283 | #### External Configuration
284 | Mux can be configured with external rules using [`*serviceconfig.Service`](https://pkg.go.dev/google.golang.org/genproto/googleapis/api/serviceconfig). Load the file from yaml defintions or declare in Go.
285 |
286 | ```go
287 | mux, _ := larking.NewMux(
288 | larking.ServiceConfigOption(sc), // sc type of *serviceconfig.Service
289 | )
290 | ```
291 |
--------------------------------------------------------------------------------
/benchmarks/README.md:
--------------------------------------------------------------------------------
1 | # Benchmarks
2 |
3 | Comparisons against other popular services. See `bench.txt` for results.
4 | Benchmarks should be taken with a grain of salt, please add more if you'd like to expand the test cases!
5 |
6 | ## Larking
7 | Larking serves both JSON and Protobuf encoded requests, tests marked with `+pb` are protobuf encoded.
8 |
9 | ### Optimisations
10 | - https://www.emcfarlane.com/blog/2023-04-18-profile-lexer
11 | - https://www.emcfarlane.com/blog/2023-05-01-bufferless-append
12 |
13 | ## gRPC-Gateway
14 |
15 | [gRPC-Gateway](https://github.com/grpc-ecosystem/grpc-gateway)
16 | generated `api/librarypb/library.pb.gw.go` file to proxy JSON requests.
17 |
18 | ## Envoy
19 |
20 | [gRPC-JSON transcoder](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter) with configuration in testdata/envoy.yaml.
21 | Slower than both larking and gRPC-Gateway, price for proxying requests.
22 |
23 | ## Gorilla Mux
24 |
25 | [Gorilla Mux](https://github.com/gorilla/mux) is a popular routing library for HTTP services in Go.
26 | Here we write a custom mux that replicates the gRPC annotations and binds the mux to the gRPC server.
27 | Compares speed with writing the annotations binding by hand, useful for compairsons with Go routing libraries.
28 |
29 | ## Connect-go
30 |
31 | [Connect-go](https://github.com/bufbuild/connect-go) is a slim library with gRPC compatible HTTP APIs.
32 |
33 | ## Twirp
34 |
35 | [Twirp](https://github.com/twitchtv/twirp) is a simple RPC protocol based on HTTP and Protocol Buffers (proto).
36 |
37 |
38 | ## gRPC
39 |
40 | Compare gRPC server benchmarks with the `mux.ServeHTTP`.
41 | We use an altered version of go-gRPC's [benchmain](https://github.com/grpc/grpc-go/blob/master/Documentation/benchmark.md)
42 | tool to run a benchmark and compare it to gRPC internal server.
43 |
44 | ```
45 | go run benchmain/main.go -benchtime=10s -workloads=all \
46 | -compression=gzip -maxConcurrentCalls=1 -trace=off \
47 | -reqSizeBytes=1,1048576 -respSizeBytes=1,1048576 -networkMode=Local \
48 | -cpuProfile=cpuProf -memProfile=memProf -memProfileRate=10000 -resultFile=result.bin
49 | ```
50 |
51 | ```
52 | go run google.golang.org/grpc/benchmark/benchresult grpc_result.bin result.bin
53 | ```
54 |
55 | See `grpc-bench.txt` for gRPC results.
56 |
--------------------------------------------------------------------------------
/benchmarks/api/library.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | syntax = "proto3";
6 |
7 | package larking.benchmarks.librarypb;
8 |
9 | import "google/api/annotations.proto";
10 | import "google/protobuf/timestamp.proto";
11 | import "google/protobuf/duration.proto";
12 | import "google/protobuf/field_mask.proto";
13 | import "google/protobuf/empty.proto";
14 | import "google/protobuf/wrappers.proto";
15 |
16 | option go_package = "larking.io/benchmarks/api/librarypb;librarypb";
17 |
18 | service LibraryService {
19 | rpc GetBook(GetBookRequest) returns (Book) {
20 | option (google.api.http) = {
21 | get : "/v1/{name=shelves/*/books/*}"
22 | };
23 | };
24 | rpc CreateBook(CreateBookRequest) returns (Book) {
25 | option (google.api.http) = {
26 | post : "/v1/{parent=shelves/*}/books"
27 | body : "book"
28 | };
29 | };
30 |
31 | // Lists books in a shelf.
32 | rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
33 | // List method maps to HTTP GET.
34 | option (google.api.http) = {
35 | // The `parent` captures the parent resource name, such as
36 | // "shelves/shelf1".
37 | get : "/v1/{parent=shelves/*}/books"
38 | };
39 | }
40 |
41 | // Updates a book.
42 | rpc UpdateBook(UpdateBookRequest) returns (Book) {
43 | // Update maps to HTTP PATCH. Resource name is mapped to a URL path.
44 | // Resource is contained in the HTTP request body.
45 | option (google.api.http) = {
46 | // Note the URL template variable which captures the resource name of the
47 | // book to update.
48 | patch : "/v1/{book.name=shelves/*/books/*}"
49 | body : "book"
50 | };
51 | }
52 |
53 | // Deletes a book.
54 | rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
55 | // Delete maps to HTTP DELETE. Resource name maps to the URL path.
56 | // There is no request body.
57 | option (google.api.http) = {
58 | // Note the URL template variable capturing the multi-segment name of the
59 | // book resource to be deleted, such as "shelves/shelf1/books/book2"
60 | delete : "/v1/{name=shelves/*/books/*}"
61 | };
62 | }
63 | }
64 |
65 | message Book {
66 | // Resource name of the book. It must have the format of "shelves/*/books/*".
67 | // For example: "shelves/shelf1/books/book2".
68 | string name = 1;
69 | // The title of the book.
70 | string title = 2;
71 | // The author of the book.
72 | string author = 3;
73 | // The number of pages in the book.
74 | int32 page_count = 4;
75 | // The date the book was published.
76 | google.protobuf.Timestamp publish_time = 5;
77 | // The duration of the book.
78 | google.protobuf.Duration duration = 6;
79 | // The price of the book.
80 | google.protobuf.DoubleValue price = 7;
81 | }
82 |
83 | message GetBookRequest {
84 | // Resource name of a book. For example: "shelves/shelf1/books/book2".
85 | string name = 1;
86 | }
87 |
88 | message CreateBookRequest {
89 | // Resource name of the parent resource where to create the book.
90 | // For example: "shelves/shelf1".
91 | string parent = 1;
92 | // The Book resource to be created. Client must not set the `Book.name` field.
93 | Book book = 2;
94 | }
95 |
96 | message ListBooksRequest {
97 | // The parent resource name, for example, "shelves/shelf1".
98 | string parent = 1;
99 |
100 | // The maximum number of items to return.
101 | int32 page_size = 2;
102 |
103 | // The next_page_token value returned from a previous List request, if any.
104 | string page_token = 3;
105 | }
106 |
107 | message ListBooksResponse {
108 | // The field name should match the noun "books" in the method name. There
109 | // will be a maximum number of items returned based on the page_size field
110 | // in the request.
111 | repeated Book books = 1;
112 |
113 | // Token to retrieve the next page of results, or empty if there are no
114 | // more results in the list.
115 | string next_page_token = 2;
116 | }
117 |
118 | message UpdateBookRequest {
119 | // The book resource which replaces the resource on the server.
120 | Book book = 1;
121 |
122 | // The update mask applies to the resource. For the `FieldMask` definition,
123 | // see
124 | // https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask
125 | google.protobuf.FieldMask update_mask = 2;
126 | }
127 |
128 | message DeleteBookRequest {
129 | // The resource name of the book to be deleted, for example:
130 | // "shelves/shelf1/books/book2"
131 | string name = 1;
132 | }
133 |
--------------------------------------------------------------------------------
/benchmarks/api/librarypb/library_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
6 | // versions:
7 | // - protoc-gen-go-grpc v1.3.0
8 | // - protoc v3.21.12
9 | // source: api/library.proto
10 |
11 | package librarypb
12 |
13 | import (
14 | context "context"
15 | grpc "google.golang.org/grpc"
16 | codes "google.golang.org/grpc/codes"
17 | status "google.golang.org/grpc/status"
18 | emptypb "google.golang.org/protobuf/types/known/emptypb"
19 | )
20 |
21 | // This is a compile-time assertion to ensure that this generated file
22 | // is compatible with the grpc package it is being compiled against.
23 | // Requires gRPC-Go v1.32.0 or later.
24 | const _ = grpc.SupportPackageIsVersion7
25 |
26 | const (
27 | LibraryService_GetBook_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/GetBook"
28 | LibraryService_CreateBook_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/CreateBook"
29 | LibraryService_ListBooks_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/ListBooks"
30 | LibraryService_UpdateBook_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/UpdateBook"
31 | LibraryService_DeleteBook_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/DeleteBook"
32 | )
33 |
34 | // LibraryServiceClient is the client API for LibraryService service.
35 | //
36 | // 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.
37 | type LibraryServiceClient interface {
38 | GetBook(ctx context.Context, in *GetBookRequest, opts ...grpc.CallOption) (*Book, error)
39 | CreateBook(ctx context.Context, in *CreateBookRequest, opts ...grpc.CallOption) (*Book, error)
40 | // Lists books in a shelf.
41 | ListBooks(ctx context.Context, in *ListBooksRequest, opts ...grpc.CallOption) (*ListBooksResponse, error)
42 | // Updates a book.
43 | UpdateBook(ctx context.Context, in *UpdateBookRequest, opts ...grpc.CallOption) (*Book, error)
44 | // Deletes a book.
45 | DeleteBook(ctx context.Context, in *DeleteBookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
46 | }
47 |
48 | type libraryServiceClient struct {
49 | cc grpc.ClientConnInterface
50 | }
51 |
52 | func NewLibraryServiceClient(cc grpc.ClientConnInterface) LibraryServiceClient {
53 | return &libraryServiceClient{cc}
54 | }
55 |
56 | func (c *libraryServiceClient) GetBook(ctx context.Context, in *GetBookRequest, opts ...grpc.CallOption) (*Book, error) {
57 | out := new(Book)
58 | err := c.cc.Invoke(ctx, LibraryService_GetBook_FullMethodName, in, out, opts...)
59 | if err != nil {
60 | return nil, err
61 | }
62 | return out, nil
63 | }
64 |
65 | func (c *libraryServiceClient) CreateBook(ctx context.Context, in *CreateBookRequest, opts ...grpc.CallOption) (*Book, error) {
66 | out := new(Book)
67 | err := c.cc.Invoke(ctx, LibraryService_CreateBook_FullMethodName, in, out, opts...)
68 | if err != nil {
69 | return nil, err
70 | }
71 | return out, nil
72 | }
73 |
74 | func (c *libraryServiceClient) ListBooks(ctx context.Context, in *ListBooksRequest, opts ...grpc.CallOption) (*ListBooksResponse, error) {
75 | out := new(ListBooksResponse)
76 | err := c.cc.Invoke(ctx, LibraryService_ListBooks_FullMethodName, in, out, opts...)
77 | if err != nil {
78 | return nil, err
79 | }
80 | return out, nil
81 | }
82 |
83 | func (c *libraryServiceClient) UpdateBook(ctx context.Context, in *UpdateBookRequest, opts ...grpc.CallOption) (*Book, error) {
84 | out := new(Book)
85 | err := c.cc.Invoke(ctx, LibraryService_UpdateBook_FullMethodName, in, out, opts...)
86 | if err != nil {
87 | return nil, err
88 | }
89 | return out, nil
90 | }
91 |
92 | func (c *libraryServiceClient) DeleteBook(ctx context.Context, in *DeleteBookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
93 | out := new(emptypb.Empty)
94 | err := c.cc.Invoke(ctx, LibraryService_DeleteBook_FullMethodName, in, out, opts...)
95 | if err != nil {
96 | return nil, err
97 | }
98 | return out, nil
99 | }
100 |
101 | // LibraryServiceServer is the server API for LibraryService service.
102 | // All implementations must embed UnimplementedLibraryServiceServer
103 | // for forward compatibility
104 | type LibraryServiceServer interface {
105 | GetBook(context.Context, *GetBookRequest) (*Book, error)
106 | CreateBook(context.Context, *CreateBookRequest) (*Book, error)
107 | // Lists books in a shelf.
108 | ListBooks(context.Context, *ListBooksRequest) (*ListBooksResponse, error)
109 | // Updates a book.
110 | UpdateBook(context.Context, *UpdateBookRequest) (*Book, error)
111 | // Deletes a book.
112 | DeleteBook(context.Context, *DeleteBookRequest) (*emptypb.Empty, error)
113 | mustEmbedUnimplementedLibraryServiceServer()
114 | }
115 |
116 | // UnimplementedLibraryServiceServer must be embedded to have forward compatible implementations.
117 | type UnimplementedLibraryServiceServer struct {
118 | }
119 |
120 | func (UnimplementedLibraryServiceServer) GetBook(context.Context, *GetBookRequest) (*Book, error) {
121 | return nil, status.Errorf(codes.Unimplemented, "method GetBook not implemented")
122 | }
123 | func (UnimplementedLibraryServiceServer) CreateBook(context.Context, *CreateBookRequest) (*Book, error) {
124 | return nil, status.Errorf(codes.Unimplemented, "method CreateBook not implemented")
125 | }
126 | func (UnimplementedLibraryServiceServer) ListBooks(context.Context, *ListBooksRequest) (*ListBooksResponse, error) {
127 | return nil, status.Errorf(codes.Unimplemented, "method ListBooks not implemented")
128 | }
129 | func (UnimplementedLibraryServiceServer) UpdateBook(context.Context, *UpdateBookRequest) (*Book, error) {
130 | return nil, status.Errorf(codes.Unimplemented, "method UpdateBook not implemented")
131 | }
132 | func (UnimplementedLibraryServiceServer) DeleteBook(context.Context, *DeleteBookRequest) (*emptypb.Empty, error) {
133 | return nil, status.Errorf(codes.Unimplemented, "method DeleteBook not implemented")
134 | }
135 | func (UnimplementedLibraryServiceServer) mustEmbedUnimplementedLibraryServiceServer() {}
136 |
137 | // UnsafeLibraryServiceServer may be embedded to opt out of forward compatibility for this service.
138 | // Use of this interface is not recommended, as added methods to LibraryServiceServer will
139 | // result in compilation errors.
140 | type UnsafeLibraryServiceServer interface {
141 | mustEmbedUnimplementedLibraryServiceServer()
142 | }
143 |
144 | func RegisterLibraryServiceServer(s grpc.ServiceRegistrar, srv LibraryServiceServer) {
145 | s.RegisterService(&LibraryService_ServiceDesc, srv)
146 | }
147 |
148 | func _LibraryService_GetBook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
149 | in := new(GetBookRequest)
150 | if err := dec(in); err != nil {
151 | return nil, err
152 | }
153 | if interceptor == nil {
154 | return srv.(LibraryServiceServer).GetBook(ctx, in)
155 | }
156 | info := &grpc.UnaryServerInfo{
157 | Server: srv,
158 | FullMethod: LibraryService_GetBook_FullMethodName,
159 | }
160 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
161 | return srv.(LibraryServiceServer).GetBook(ctx, req.(*GetBookRequest))
162 | }
163 | return interceptor(ctx, in, info, handler)
164 | }
165 |
166 | func _LibraryService_CreateBook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
167 | in := new(CreateBookRequest)
168 | if err := dec(in); err != nil {
169 | return nil, err
170 | }
171 | if interceptor == nil {
172 | return srv.(LibraryServiceServer).CreateBook(ctx, in)
173 | }
174 | info := &grpc.UnaryServerInfo{
175 | Server: srv,
176 | FullMethod: LibraryService_CreateBook_FullMethodName,
177 | }
178 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
179 | return srv.(LibraryServiceServer).CreateBook(ctx, req.(*CreateBookRequest))
180 | }
181 | return interceptor(ctx, in, info, handler)
182 | }
183 |
184 | func _LibraryService_ListBooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
185 | in := new(ListBooksRequest)
186 | if err := dec(in); err != nil {
187 | return nil, err
188 | }
189 | if interceptor == nil {
190 | return srv.(LibraryServiceServer).ListBooks(ctx, in)
191 | }
192 | info := &grpc.UnaryServerInfo{
193 | Server: srv,
194 | FullMethod: LibraryService_ListBooks_FullMethodName,
195 | }
196 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
197 | return srv.(LibraryServiceServer).ListBooks(ctx, req.(*ListBooksRequest))
198 | }
199 | return interceptor(ctx, in, info, handler)
200 | }
201 |
202 | func _LibraryService_UpdateBook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
203 | in := new(UpdateBookRequest)
204 | if err := dec(in); err != nil {
205 | return nil, err
206 | }
207 | if interceptor == nil {
208 | return srv.(LibraryServiceServer).UpdateBook(ctx, in)
209 | }
210 | info := &grpc.UnaryServerInfo{
211 | Server: srv,
212 | FullMethod: LibraryService_UpdateBook_FullMethodName,
213 | }
214 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
215 | return srv.(LibraryServiceServer).UpdateBook(ctx, req.(*UpdateBookRequest))
216 | }
217 | return interceptor(ctx, in, info, handler)
218 | }
219 |
220 | func _LibraryService_DeleteBook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
221 | in := new(DeleteBookRequest)
222 | if err := dec(in); err != nil {
223 | return nil, err
224 | }
225 | if interceptor == nil {
226 | return srv.(LibraryServiceServer).DeleteBook(ctx, in)
227 | }
228 | info := &grpc.UnaryServerInfo{
229 | Server: srv,
230 | FullMethod: LibraryService_DeleteBook_FullMethodName,
231 | }
232 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
233 | return srv.(LibraryServiceServer).DeleteBook(ctx, req.(*DeleteBookRequest))
234 | }
235 | return interceptor(ctx, in, info, handler)
236 | }
237 |
238 | // LibraryService_ServiceDesc is the grpc.ServiceDesc for LibraryService service.
239 | // It's only intended for direct use with grpc.RegisterService,
240 | // and not to be introspected or modified (even as a copy)
241 | var LibraryService_ServiceDesc = grpc.ServiceDesc{
242 | ServiceName: "larking.benchmarks.librarypb.LibraryService",
243 | HandlerType: (*LibraryServiceServer)(nil),
244 | Methods: []grpc.MethodDesc{
245 | {
246 | MethodName: "GetBook",
247 | Handler: _LibraryService_GetBook_Handler,
248 | },
249 | {
250 | MethodName: "CreateBook",
251 | Handler: _LibraryService_CreateBook_Handler,
252 | },
253 | {
254 | MethodName: "ListBooks",
255 | Handler: _LibraryService_ListBooks_Handler,
256 | },
257 | {
258 | MethodName: "UpdateBook",
259 | Handler: _LibraryService_UpdateBook_Handler,
260 | },
261 | {
262 | MethodName: "DeleteBook",
263 | Handler: _LibraryService_DeleteBook_Handler,
264 | },
265 | },
266 | Streams: []grpc.StreamDesc{},
267 | Metadata: "api/library.proto",
268 | }
269 |
--------------------------------------------------------------------------------
/benchmarks/api/librarypb/librarypbconnect/library.connect.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | // Code generated by protoc-gen-connect-go. DO NOT EDIT.
6 | //
7 | // Source: api/library.proto
8 |
9 | package librarypbconnect
10 |
11 | import (
12 | context "context"
13 | errors "errors"
14 | connect_go "github.com/bufbuild/connect-go"
15 | emptypb "google.golang.org/protobuf/types/known/emptypb"
16 | librarypb "larking.io/benchmarks/api/librarypb"
17 | http "net/http"
18 | strings "strings"
19 | )
20 |
21 | // This is a compile-time assertion to ensure that this generated file and the connect package are
22 | // compatible. If you get a compiler error that this constant is not defined, this code was
23 | // generated with a version of connect newer than the one compiled into your binary. You can fix the
24 | // problem by either regenerating this code with an older version of connect or updating the connect
25 | // version compiled into your binary.
26 | const _ = connect_go.IsAtLeastVersion0_1_0
27 |
28 | const (
29 | // LibraryServiceName is the fully-qualified name of the LibraryService service.
30 | LibraryServiceName = "larking.benchmarks.librarypb.LibraryService"
31 | )
32 |
33 | // These constants are the fully-qualified names of the RPCs defined in this package. They're
34 | // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
35 | //
36 | // Note that these are different from the fully-qualified method names used by
37 | // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
38 | // reflection-formatted method names, remove the leading slash and convert the remaining slash to a
39 | // period.
40 | const (
41 | // LibraryServiceGetBookProcedure is the fully-qualified name of the LibraryService's GetBook RPC.
42 | LibraryServiceGetBookProcedure = "/larking.benchmarks.librarypb.LibraryService/GetBook"
43 | // LibraryServiceCreateBookProcedure is the fully-qualified name of the LibraryService's CreateBook
44 | // RPC.
45 | LibraryServiceCreateBookProcedure = "/larking.benchmarks.librarypb.LibraryService/CreateBook"
46 | // LibraryServiceListBooksProcedure is the fully-qualified name of the LibraryService's ListBooks
47 | // RPC.
48 | LibraryServiceListBooksProcedure = "/larking.benchmarks.librarypb.LibraryService/ListBooks"
49 | // LibraryServiceUpdateBookProcedure is the fully-qualified name of the LibraryService's UpdateBook
50 | // RPC.
51 | LibraryServiceUpdateBookProcedure = "/larking.benchmarks.librarypb.LibraryService/UpdateBook"
52 | // LibraryServiceDeleteBookProcedure is the fully-qualified name of the LibraryService's DeleteBook
53 | // RPC.
54 | LibraryServiceDeleteBookProcedure = "/larking.benchmarks.librarypb.LibraryService/DeleteBook"
55 | )
56 |
57 | // LibraryServiceClient is a client for the larking.benchmarks.librarypb.LibraryService service.
58 | type LibraryServiceClient interface {
59 | GetBook(context.Context, *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error)
60 | CreateBook(context.Context, *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error)
61 | // Lists books in a shelf.
62 | ListBooks(context.Context, *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error)
63 | // Updates a book.
64 | UpdateBook(context.Context, *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error)
65 | // Deletes a book.
66 | DeleteBook(context.Context, *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error)
67 | }
68 |
69 | // NewLibraryServiceClient constructs a client for the larking.benchmarks.librarypb.LibraryService
70 | // service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
71 | // gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
72 | // the connect.WithGRPC() or connect.WithGRPCWeb() options.
73 | //
74 | // The URL supplied here should be the base URL for the Connect or gRPC server (for example,
75 | // http://api.acme.com or https://acme.com/grpc).
76 | func NewLibraryServiceClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) LibraryServiceClient {
77 | baseURL = strings.TrimRight(baseURL, "/")
78 | return &libraryServiceClient{
79 | getBook: connect_go.NewClient[librarypb.GetBookRequest, librarypb.Book](
80 | httpClient,
81 | baseURL+LibraryServiceGetBookProcedure,
82 | opts...,
83 | ),
84 | createBook: connect_go.NewClient[librarypb.CreateBookRequest, librarypb.Book](
85 | httpClient,
86 | baseURL+LibraryServiceCreateBookProcedure,
87 | opts...,
88 | ),
89 | listBooks: connect_go.NewClient[librarypb.ListBooksRequest, librarypb.ListBooksResponse](
90 | httpClient,
91 | baseURL+LibraryServiceListBooksProcedure,
92 | opts...,
93 | ),
94 | updateBook: connect_go.NewClient[librarypb.UpdateBookRequest, librarypb.Book](
95 | httpClient,
96 | baseURL+LibraryServiceUpdateBookProcedure,
97 | opts...,
98 | ),
99 | deleteBook: connect_go.NewClient[librarypb.DeleteBookRequest, emptypb.Empty](
100 | httpClient,
101 | baseURL+LibraryServiceDeleteBookProcedure,
102 | opts...,
103 | ),
104 | }
105 | }
106 |
107 | // libraryServiceClient implements LibraryServiceClient.
108 | type libraryServiceClient struct {
109 | getBook *connect_go.Client[librarypb.GetBookRequest, librarypb.Book]
110 | createBook *connect_go.Client[librarypb.CreateBookRequest, librarypb.Book]
111 | listBooks *connect_go.Client[librarypb.ListBooksRequest, librarypb.ListBooksResponse]
112 | updateBook *connect_go.Client[librarypb.UpdateBookRequest, librarypb.Book]
113 | deleteBook *connect_go.Client[librarypb.DeleteBookRequest, emptypb.Empty]
114 | }
115 |
116 | // GetBook calls larking.benchmarks.librarypb.LibraryService.GetBook.
117 | func (c *libraryServiceClient) GetBook(ctx context.Context, req *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error) {
118 | return c.getBook.CallUnary(ctx, req)
119 | }
120 |
121 | // CreateBook calls larking.benchmarks.librarypb.LibraryService.CreateBook.
122 | func (c *libraryServiceClient) CreateBook(ctx context.Context, req *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error) {
123 | return c.createBook.CallUnary(ctx, req)
124 | }
125 |
126 | // ListBooks calls larking.benchmarks.librarypb.LibraryService.ListBooks.
127 | func (c *libraryServiceClient) ListBooks(ctx context.Context, req *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error) {
128 | return c.listBooks.CallUnary(ctx, req)
129 | }
130 |
131 | // UpdateBook calls larking.benchmarks.librarypb.LibraryService.UpdateBook.
132 | func (c *libraryServiceClient) UpdateBook(ctx context.Context, req *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error) {
133 | return c.updateBook.CallUnary(ctx, req)
134 | }
135 |
136 | // DeleteBook calls larking.benchmarks.librarypb.LibraryService.DeleteBook.
137 | func (c *libraryServiceClient) DeleteBook(ctx context.Context, req *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error) {
138 | return c.deleteBook.CallUnary(ctx, req)
139 | }
140 |
141 | // LibraryServiceHandler is an implementation of the larking.benchmarks.librarypb.LibraryService
142 | // service.
143 | type LibraryServiceHandler interface {
144 | GetBook(context.Context, *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error)
145 | CreateBook(context.Context, *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error)
146 | // Lists books in a shelf.
147 | ListBooks(context.Context, *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error)
148 | // Updates a book.
149 | UpdateBook(context.Context, *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error)
150 | // Deletes a book.
151 | DeleteBook(context.Context, *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error)
152 | }
153 |
154 | // NewLibraryServiceHandler builds an HTTP handler from the service implementation. It returns the
155 | // path on which to mount the handler and the handler itself.
156 | //
157 | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
158 | // and JSON codecs. They also support gzip compression.
159 | func NewLibraryServiceHandler(svc LibraryServiceHandler, opts ...connect_go.HandlerOption) (string, http.Handler) {
160 | mux := http.NewServeMux()
161 | mux.Handle(LibraryServiceGetBookProcedure, connect_go.NewUnaryHandler(
162 | LibraryServiceGetBookProcedure,
163 | svc.GetBook,
164 | opts...,
165 | ))
166 | mux.Handle(LibraryServiceCreateBookProcedure, connect_go.NewUnaryHandler(
167 | LibraryServiceCreateBookProcedure,
168 | svc.CreateBook,
169 | opts...,
170 | ))
171 | mux.Handle(LibraryServiceListBooksProcedure, connect_go.NewUnaryHandler(
172 | LibraryServiceListBooksProcedure,
173 | svc.ListBooks,
174 | opts...,
175 | ))
176 | mux.Handle(LibraryServiceUpdateBookProcedure, connect_go.NewUnaryHandler(
177 | LibraryServiceUpdateBookProcedure,
178 | svc.UpdateBook,
179 | opts...,
180 | ))
181 | mux.Handle(LibraryServiceDeleteBookProcedure, connect_go.NewUnaryHandler(
182 | LibraryServiceDeleteBookProcedure,
183 | svc.DeleteBook,
184 | opts...,
185 | ))
186 | return "/larking.benchmarks.librarypb.LibraryService/", mux
187 | }
188 |
189 | // UnimplementedLibraryServiceHandler returns CodeUnimplemented from all methods.
190 | type UnimplementedLibraryServiceHandler struct{}
191 |
192 | func (UnimplementedLibraryServiceHandler) GetBook(context.Context, *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error) {
193 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.GetBook is not implemented"))
194 | }
195 |
196 | func (UnimplementedLibraryServiceHandler) CreateBook(context.Context, *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error) {
197 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.CreateBook is not implemented"))
198 | }
199 |
200 | func (UnimplementedLibraryServiceHandler) ListBooks(context.Context, *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error) {
201 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.ListBooks is not implemented"))
202 | }
203 |
204 | func (UnimplementedLibraryServiceHandler) UpdateBook(context.Context, *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error) {
205 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.UpdateBook is not implemented"))
206 | }
207 |
208 | func (UnimplementedLibraryServiceHandler) DeleteBook(context.Context, *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error) {
209 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.DeleteBook is not implemented"))
210 | }
211 |
--------------------------------------------------------------------------------
/benchmarks/api/proto.pb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emcfarlane/larking/191f5a2ad1102341ec7a4c7849ac417589b864c9/benchmarks/api/proto.pb
--------------------------------------------------------------------------------
/benchmarks/bench.txt:
--------------------------------------------------------------------------------
1 | goos: darwin
2 | goarch: arm64
3 | pkg: larking.io/benchmarks
4 | BenchmarkLarking/GRPC_GetBook-8 19376 60966 ns/op 13877 B/op 193 allocs/op
5 | BenchmarkLarking/HTTP_GetBook-8 25690 46071 ns/op 9230 B/op 144 allocs/op
6 | BenchmarkLarking/HTTP_UpdateBook-8 24384 49030 ns/op 11228 B/op 171 allocs/op
7 | BenchmarkLarking/HTTP_DeleteBook-8 31225 38384 ns/op 8002 B/op 96 allocs/op
8 | BenchmarkLarking/HTTP_GetBook+pb-8 30403 39225 ns/op 8351 B/op 102 allocs/op
9 | BenchmarkLarking/HTTP_UpdateBook+pb-8 26089 45797 ns/op 9929 B/op 132 allocs/op
10 | BenchmarkLarking/HTTP_DeleteBook+pb-8 31750 37533 ns/op 7773 B/op 93 allocs/op
11 | BenchmarkGRPCGateway/GRPC_GetBook-8 29191 40917 ns/op 9498 B/op 178 allocs/op
12 | BenchmarkGRPCGateway/HTTP_GetBook-8 25154 46624 ns/op 11200 B/op 179 allocs/op
13 | BenchmarkGRPCGateway/HTTP_UpdateBook-8 23169 51382 ns/op 16404 B/op 230 allocs/op
14 | BenchmarkGRPCGateway/HTTP_DeleteBook-8 31365 38172 ns/op 9160 B/op 119 allocs/op
15 | BenchmarkEnvoyGRPC/GRPC_GetBook-8 9067 131554 ns/op 10960 B/op 177 allocs/op
16 | BenchmarkEnvoyGRPC/HTTP_GetBook-8 8090 148991 ns/op 9974 B/op 163 allocs/op
17 | BenchmarkEnvoyGRPC/HTTP_UpdateBook-8 7807 150534 ns/op 10648 B/op 166 allocs/op
18 | BenchmarkEnvoyGRPC/HTTP_DeleteBook-8 8866 133976 ns/op 9026 B/op 126 allocs/op
19 | BenchmarkGorillaMux/HTTP_GetBook-8 26590 45013 ns/op 9874 B/op 143 allocs/op
20 | BenchmarkGorillaMux/HTTP_UpdateBook-8 26713 44912 ns/op 11791 B/op 166 allocs/op
21 | BenchmarkGorillaMux/HTTP_DeleteBook-8 32792 36645 ns/op 8143 B/op 89 allocs/op
22 | BenchmarkConnectGo/GRPC_GetBook-8 18492 64467 ns/op 13437 B/op 194 allocs/op
23 | BenchmarkConnectGo/HTTP_GetBook-8 24248 49106 ns/op 10996 B/op 176 allocs/op
24 | BenchmarkConnectGo/HTTP_UpdateBook-8 25184 47556 ns/op 11232 B/op 183 allocs/op
25 | BenchmarkConnectGo/HTTP_DeleteBook-8 28917 41493 ns/op 9299 B/op 122 allocs/op
26 | BenchmarkConnectGo/Connect_GetBook-8 15196 79021 ns/op 77348 B/op 151 allocs/op
27 | BenchmarkConnectGo/Connect_UpdateBook-8 16684 73370 ns/op 79413 B/op 151 allocs/op
28 | BenchmarkConnectGo/Connect_DeleteBook-8 18038 66268 ns/op 69503 B/op 140 allocs/op
29 | BenchmarkTwirp/HTTP_GetBook-8 23119 49839 ns/op 12679 B/op 196 allocs/op
30 | BenchmarkTwirp/HTTP_UpdateBook-8 23336 52310 ns/op 13456 B/op 214 allocs/op
31 | BenchmarkTwirp/HTTP_DeleteBook-8 28702 41848 ns/op 10766 B/op 140 allocs/op
32 | PASS
33 | ok larking.io/benchmarks 47.310s
34 |
--------------------------------------------------------------------------------
/benchmarks/connect_svc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | connect_go "github.com/bufbuild/connect-go"
8 | "google.golang.org/grpc"
9 | "google.golang.org/grpc/codes"
10 | "google.golang.org/protobuf/types/known/durationpb"
11 | "google.golang.org/protobuf/types/known/emptypb"
12 | "google.golang.org/protobuf/types/known/timestamppb"
13 | "google.golang.org/protobuf/types/known/wrapperspb"
14 | "larking.io/benchmarks/api/librarypb"
15 | cpb "larking.io/benchmarks/api/librarypb/librarypbconnect"
16 | )
17 |
18 | type testConnectService struct {
19 | cpb.UnimplementedLibraryServiceHandler
20 | }
21 |
22 | func (testConnectService) GetBook(ctx context.Context, req *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error) {
23 | if req.Msg.Name != "shelves/1/books/1" {
24 | return nil, grpc.Errorf(codes.NotFound, "not found")
25 | }
26 | return connect_go.NewResponse(&librarypb.Book{
27 | Name: "shelves/1/books/1",
28 | Title: "The Great Gatsby",
29 | Author: "F. Scott Fitzgerald",
30 | PageCount: 180,
31 | PublishTime: timestamppb.New(time.Now()),
32 | Duration: durationpb.New(1 * time.Hour),
33 | Price: wrapperspb.Double(9.99),
34 | }), nil
35 | }
36 |
37 | func (testConnectService) ListBooks(ctx context.Context, req *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error) {
38 | if req.Msg.Parent != "shelves/1" {
39 | return nil, grpc.Errorf(codes.NotFound, "not found")
40 | }
41 | return connect_go.NewResponse(&librarypb.ListBooksResponse{
42 | Books: []*librarypb.Book{
43 | {
44 | Name: "shelves/1/books/1",
45 | Title: "The Great Gatsby",
46 | },
47 | {
48 | Name: "shelves/1/books/2",
49 | Title: "The Catcher in the Rye",
50 | },
51 | {
52 | Name: "shelves/1/books/3",
53 | Title: "The Grapes of Wrath",
54 | },
55 | },
56 | }), nil
57 | }
58 |
59 | func (testConnectService) CreateBook(ctx context.Context, req *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error) {
60 | return connect_go.NewResponse(req.Msg.Book), nil
61 | }
62 | func (testConnectService) UpdateBook(ctx context.Context, req *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error) {
63 | if req.Msg.Book.GetName() != "shelves/1/books/1" {
64 | return nil, grpc.Errorf(codes.NotFound, "not found")
65 | }
66 | if req.Msg.UpdateMask.Paths[0] != "book.title" {
67 | return nil, grpc.Errorf(codes.InvalidArgument, "invalid field mask")
68 | }
69 | return connect_go.NewResponse(req.Msg.Book), nil
70 | }
71 | func (testConnectService) DeleteBook(ctx context.Context, req *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error) {
72 | if req.Msg.Name != "shelves/1/books/1" {
73 | return nil, grpc.Errorf(codes.NotFound, "not found")
74 | }
75 | return connect_go.NewResponse(&emptypb.Empty{}), nil
76 | }
77 |
--------------------------------------------------------------------------------
/benchmarks/gen.sh:
--------------------------------------------------------------------------------
1 | protoc -I ~/src/github.com/googleapis/api-common-protos/ -I. \
2 | --go_out=module=larking.io/benchmarks:. \
3 | --go-grpc_out=module=larking.io/benchmarks:. \
4 | --grpc-gateway_out=module=larking.io/benchmarks:. \
5 | --connect-go_out=module=larking.io/benchmarks:. \
6 | --twirp_out=module=larking.io/benchmarks:. \
7 | --include_imports --include_source_info \
8 | --descriptor_set_out=api/proto.pb \
9 | ./api/*.proto
10 |
--------------------------------------------------------------------------------
/benchmarks/go.mod:
--------------------------------------------------------------------------------
1 | module larking.io/benchmarks
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/bufbuild/connect-go v1.7.0
7 | github.com/gorilla/mux v1.8.0
8 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
9 | github.com/soheilhy/cmux v0.1.5
10 | github.com/twitchtv/twirp v8.1.3+incompatible
11 | golang.org/x/net v0.9.0
12 | golang.org/x/sync v0.1.0
13 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1
14 | google.golang.org/grpc v1.54.0
15 | google.golang.org/protobuf v1.30.1-0.20230501154320-cf06b0c33cda
16 | larking.io v0.0.0-20230415140254-4fbc95c206cd
17 | )
18 |
19 | require (
20 | github.com/gobwas/httphead v0.1.0 // indirect
21 | github.com/gobwas/pool v0.2.1 // indirect
22 | github.com/gobwas/ws v1.2.0 // indirect
23 | github.com/golang/protobuf v1.5.3 // indirect
24 | github.com/pkg/errors v0.9.1 // indirect
25 | golang.org/x/sys v0.7.0 // indirect
26 | golang.org/x/text v0.9.0 // indirect
27 | )
28 |
29 | replace larking.io => ../
30 |
--------------------------------------------------------------------------------
/benchmarks/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bufbuild/connect-go v1.7.0 h1:MGp82v7SCza+3RhsVhV7aMikwxvI3ZfD72YiGt8FYJo=
2 | github.com/bufbuild/connect-go v1.7.0/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk=
3 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
4 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
5 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
6 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
7 | github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
8 | github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
9 | github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
10 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
11 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
12 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
13 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
14 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
15 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
16 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
17 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE=
18 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08=
19 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
21 | github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
22 | github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
23 | github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
24 | github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
26 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
27 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
28 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
29 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
30 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
31 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
32 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
34 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
35 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
36 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
37 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
38 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
40 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
41 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
42 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
43 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
44 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
45 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
46 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
47 | google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
48 | google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
49 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
50 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
51 | google.golang.org/protobuf v1.30.1-0.20230501154320-cf06b0c33cda h1:CGKs/jtLmFiQ0tmmt8ykIoaKqn+yi8T/reVFvwOR5aY=
52 | google.golang.org/protobuf v1.30.1-0.20230501154320-cf06b0c33cda/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
53 |
--------------------------------------------------------------------------------
/benchmarks/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net"
6 | "net/http"
7 |
8 | "google.golang.org/grpc"
9 | "google.golang.org/grpc/credentials/insecure"
10 | "larking.io/benchmarks/api/librarypb"
11 | )
12 |
13 | //go:generate sh gen.sh
14 |
15 | func run() error {
16 | svc := &testService{}
17 | gs := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
18 | librarypb.RegisterLibraryServiceServer(gs, svc)
19 |
20 | lis, err := net.Listen("tcp", "localhost:5050")
21 | if err != nil {
22 | return err
23 | }
24 | defer lis.Close()
25 |
26 | log.Printf("listening on %s", lis.Addr().String())
27 | if err := gs.Serve(lis); err != nil && err != http.ErrServerClosed {
28 | return err
29 | }
30 | return nil
31 | }
32 |
33 | func main() {
34 | if err := run(); err != nil {
35 | log.Fatal(err)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/benchmarks/server/server.go:
--------------------------------------------------------------------------------
1 | /*
2 | *
3 | * Copyright 2014 gRPC authors.
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | *
17 | */
18 |
19 | /*
20 | Package server implements the building blocks to setup end-to-end gRPC benchmarks.
21 | */
22 | package server
23 |
24 | import (
25 | "context"
26 | "fmt"
27 | "io"
28 | "log"
29 | "net"
30 |
31 | "google.golang.org/grpc"
32 | "google.golang.org/grpc/codes"
33 | "google.golang.org/grpc/grpclog"
34 | "google.golang.org/grpc/metadata"
35 | "google.golang.org/grpc/status"
36 |
37 | testpb "google.golang.org/grpc/interop/grpc_testing"
38 | "larking.io/larking"
39 | )
40 |
41 | var logger = grpclog.Component("benchmark")
42 |
43 | // Allows reuse of the same testpb.Payload object.
44 | func setPayload(p *testpb.Payload, t testpb.PayloadType, size int) {
45 | if size < 0 {
46 | logger.Fatalf("Requested a response with invalid length %d", size)
47 | }
48 | body := make([]byte, size)
49 | switch t {
50 | case testpb.PayloadType_COMPRESSABLE:
51 | default:
52 | logger.Fatalf("Unsupported payload type: %d", t)
53 | }
54 | p.Type = t
55 | p.Body = body
56 | }
57 |
58 | // NewPayload creates a payload with the given type and size.
59 | func NewPayload(t testpb.PayloadType, size int) *testpb.Payload {
60 | p := new(testpb.Payload)
61 | setPayload(p, t, size)
62 | return p
63 | }
64 |
65 | type testServer struct {
66 | testpb.UnimplementedBenchmarkServiceServer
67 | }
68 |
69 | func (s *testServer) UnaryCall(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
70 | return &testpb.SimpleResponse{
71 | Payload: NewPayload(in.ResponseType, int(in.ResponseSize)),
72 | }, nil
73 | }
74 |
75 | // UnconstrainedStreamingHeader indicates to the StreamingCall handler that its
76 | // behavior should be unconstrained (constant send/receive in parallel) instead
77 | // of ping-pong.
78 | const UnconstrainedStreamingHeader = "unconstrained-streaming"
79 |
80 | func (s *testServer) StreamingCall(stream testpb.BenchmarkService_StreamingCallServer) error {
81 | if md, ok := metadata.FromIncomingContext(stream.Context()); ok && len(md[UnconstrainedStreamingHeader]) != 0 {
82 | return s.UnconstrainedStreamingCall(stream)
83 | }
84 | response := &testpb.SimpleResponse{
85 | Payload: new(testpb.Payload),
86 | }
87 | in := new(testpb.SimpleRequest)
88 | for {
89 | // use ServerStream directly to reuse the same testpb.SimpleRequest object
90 | err := stream.(grpc.ServerStream).RecvMsg(in)
91 | if err == io.EOF {
92 | // read done.
93 | return nil
94 | }
95 | if err != nil {
96 | return err
97 | }
98 | setPayload(response.Payload, in.ResponseType, int(in.ResponseSize))
99 | if err := stream.Send(response); err != nil {
100 | return err
101 | }
102 | }
103 | }
104 |
105 | func (s *testServer) UnconstrainedStreamingCall(stream testpb.BenchmarkService_StreamingCallServer) error {
106 | in := new(testpb.SimpleRequest)
107 | // Receive a message to learn response type and size.
108 | err := stream.RecvMsg(in)
109 | if err == io.EOF {
110 | // read done.
111 | return nil
112 | }
113 | if err != nil {
114 | return err
115 | }
116 |
117 | response := &testpb.SimpleResponse{
118 | Payload: new(testpb.Payload),
119 | }
120 | setPayload(response.Payload, in.ResponseType, int(in.ResponseSize))
121 |
122 | go func() {
123 | for {
124 | // Using RecvMsg rather than Recv to prevent reallocation of SimpleRequest.
125 | err := stream.RecvMsg(in)
126 | switch status.Code(err) {
127 | case codes.Canceled:
128 | return
129 | case codes.OK:
130 | default:
131 | log.Fatalf("server recv error: %v", err)
132 | }
133 | }
134 | }()
135 |
136 | go func() {
137 | for {
138 | err := stream.Send(response)
139 | switch status.Code(err) {
140 | case codes.Unavailable, codes.Canceled:
141 | return
142 | case codes.OK:
143 | default:
144 | log.Fatalf("server send error: %v", err)
145 | }
146 | }
147 | }()
148 |
149 | <-stream.Context().Done()
150 | return stream.Context().Err()
151 | }
152 |
153 | // byteBufServer is a gRPC server that sends and receives byte buffer.
154 | // The purpose is to benchmark the gRPC performance without protobuf serialization/deserialization overhead.
155 | type byteBufServer struct {
156 | testpb.UnimplementedBenchmarkServiceServer
157 | respSize int32
158 | }
159 |
160 | // UnaryCall is an empty function and is not used for benchmark.
161 | // If bytebuf UnaryCall benchmark is needed later, the function body needs to be updated.
162 | func (s *byteBufServer) UnaryCall(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
163 | return &testpb.SimpleResponse{}, nil
164 | }
165 |
166 | func (s *byteBufServer) StreamingCall(stream testpb.BenchmarkService_StreamingCallServer) error {
167 | for {
168 | var in []byte
169 | err := stream.(grpc.ServerStream).RecvMsg(&in)
170 | if err == io.EOF {
171 | return nil
172 | }
173 | if err != nil {
174 | return err
175 | }
176 | out := make([]byte, s.respSize)
177 | if err := stream.(grpc.ServerStream).SendMsg(&out); err != nil {
178 | return err
179 | }
180 | }
181 | }
182 |
183 | // ServerInfo contains the information to create a gRPC benchmark server.
184 | type ServerInfo struct {
185 | // Type is the type of the server.
186 | // It should be "protobuf" or "bytebuf".
187 | Type string
188 |
189 | // Metadata is an optional configuration.
190 | // For "protobuf", it's ignored.
191 | // For "bytebuf", it should be an int representing response size.
192 | Metadata interface{}
193 |
194 | // Listener is the network listener for the server to use
195 | Listener net.Listener
196 | }
197 |
198 | // StartServer starts a gRPC server serving a benchmark service according to info.
199 | // It returns a function to stop the server.
200 | func StartServer(info ServerInfo, _ ...grpc.ServerOption) func() {
201 | m, err := larking.NewMux()
202 | if err != nil {
203 | logger.Fatalf("failed to StartServer, NewMux failed: %v", err)
204 | }
205 |
206 | //s := grpc.NewServer(opts...)
207 | switch info.Type {
208 | case "protobuf":
209 | testpb.RegisterBenchmarkServiceServer(m, &testServer{})
210 | case "bytebuf":
211 | respSize, ok := info.Metadata.(int32)
212 | if !ok {
213 | logger.Fatalf("failed to StartServer, invalid metadata: %v, for Type: %v", info.Metadata, info.Type)
214 | }
215 | testpb.RegisterBenchmarkServiceServer(m, &byteBufServer{respSize: respSize})
216 | default:
217 | logger.Fatalf("failed to StartServer, unknown Type: %v", info.Type)
218 | }
219 |
220 | s, err := larking.NewServer(m)
221 | if err != nil {
222 | logger.Fatalf("failed to StartServer, NewServer failed: %v", err)
223 | }
224 |
225 | go s.Serve(info.Listener)
226 | return func() {
227 | s.Close()
228 | }
229 | }
230 |
231 | // DoUnaryCall performs an unary RPC with given stub and request and response sizes.
232 | func DoUnaryCall(tc testpb.BenchmarkServiceClient, reqSize, respSize int) error {
233 | pl := NewPayload(testpb.PayloadType_COMPRESSABLE, reqSize)
234 | req := &testpb.SimpleRequest{
235 | ResponseType: pl.Type,
236 | ResponseSize: int32(respSize),
237 | Payload: pl,
238 | }
239 | if _, err := tc.UnaryCall(context.Background(), req); err != nil {
240 | return fmt.Errorf("/BenchmarkService/UnaryCall(_, _) = _, %v, want _, ", err)
241 | }
242 | return nil
243 | }
244 |
245 | // DoStreamingRoundTrip performs a round trip for a single streaming rpc.
246 | func DoStreamingRoundTrip(stream testpb.BenchmarkService_StreamingCallClient, reqSize, respSize int) error {
247 | pl := NewPayload(testpb.PayloadType_COMPRESSABLE, reqSize)
248 | req := &testpb.SimpleRequest{
249 | ResponseType: pl.Type,
250 | ResponseSize: int32(respSize),
251 | Payload: pl,
252 | }
253 | if err := stream.Send(req); err != nil {
254 | return fmt.Errorf("/BenchmarkService/StreamingCall.Send(_) = %v, want ", err)
255 | }
256 | if _, err := stream.Recv(); err != nil {
257 | // EOF is a valid error here.
258 | if err == io.EOF {
259 | return nil
260 | }
261 | return fmt.Errorf("/BenchmarkService/StreamingCall.Recv(_) = %v, want ", err)
262 | }
263 | return nil
264 | }
265 |
266 | // DoByteBufStreamingRoundTrip performs a round trip for a single streaming rpc, using a custom codec for byte buffer.
267 | func DoByteBufStreamingRoundTrip(stream testpb.BenchmarkService_StreamingCallClient, reqSize, respSize int) error {
268 | out := make([]byte, reqSize)
269 | if err := stream.(grpc.ClientStream).SendMsg(&out); err != nil {
270 | return fmt.Errorf("/BenchmarkService/StreamingCall.(ClientStream).SendMsg(_) = %v, want ", err)
271 | }
272 | var in []byte
273 | if err := stream.(grpc.ClientStream).RecvMsg(&in); err != nil {
274 | // EOF is a valid error here.
275 | if err == io.EOF {
276 | return nil
277 | }
278 | return fmt.Errorf("/BenchmarkService/StreamingCall.(ClientStream).RecvMsg(_) = %v, want ", err)
279 | }
280 | return nil
281 | }
282 |
283 | // NewClientConn creates a gRPC client connection to addr.
284 | func NewClientConn(addr string, opts ...grpc.DialOption) *grpc.ClientConn {
285 | return NewClientConnWithContext(context.Background(), addr, opts...)
286 | }
287 |
288 | // NewClientConnWithContext creates a gRPC client connection to addr using ctx.
289 | func NewClientConnWithContext(ctx context.Context, addr string, opts ...grpc.DialOption) *grpc.ClientConn {
290 | conn, err := grpc.DialContext(ctx, addr, opts...)
291 | if err != nil {
292 | logger.Fatalf("NewClientConn(%q) failed to create a ClientConn: %v", addr, err)
293 | }
294 | return conn
295 | }
296 |
--------------------------------------------------------------------------------
/benchmarks/svc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "google.golang.org/grpc"
8 | "google.golang.org/grpc/codes"
9 | "google.golang.org/protobuf/types/known/durationpb"
10 | "google.golang.org/protobuf/types/known/emptypb"
11 | "google.golang.org/protobuf/types/known/timestamppb"
12 | "google.golang.org/protobuf/types/known/wrapperspb"
13 | "larking.io/benchmarks/api/librarypb"
14 | )
15 |
16 | type testService struct {
17 | librarypb.UnimplementedLibraryServiceServer
18 | }
19 |
20 | func (testService) GetBook(ctx context.Context, req *librarypb.GetBookRequest) (*librarypb.Book, error) {
21 | if req.Name != "shelves/1/books/1" {
22 | return nil, grpc.Errorf(codes.NotFound, "not found")
23 | }
24 | return &librarypb.Book{
25 | Name: "shelves/1/books/1",
26 | Title: "The Great Gatsby",
27 | Author: "F. Scott Fitzgerald",
28 | PageCount: 180,
29 | PublishTime: timestamppb.New(time.Now()),
30 | Duration: durationpb.New(1 * time.Hour),
31 | Price: wrapperspb.Double(9.99),
32 | }, nil
33 | }
34 |
35 | func (testService) ListBooks(ctx context.Context, req *librarypb.ListBooksRequest) (*librarypb.ListBooksResponse, error) {
36 | if req.Parent != "shelves/1" {
37 | return nil, grpc.Errorf(codes.NotFound, "not found")
38 | }
39 | return &librarypb.ListBooksResponse{
40 | Books: []*librarypb.Book{
41 | {
42 | Name: "shelves/1/books/1",
43 | Title: "The Great Gatsby",
44 | },
45 | {
46 | Name: "shelves/1/books/2",
47 | Title: "The Catcher in the Rye",
48 | },
49 | {
50 | Name: "shelves/1/books/3",
51 | Title: "The Grapes of Wrath",
52 | },
53 | },
54 | }, nil
55 | }
56 |
57 | func (testService) CreateBook(ctx context.Context, req *librarypb.CreateBookRequest) (*librarypb.Book, error) {
58 | return req.Book, nil
59 | }
60 | func (testService) UpdateBook(ctx context.Context, req *librarypb.UpdateBookRequest) (*librarypb.Book, error) {
61 | if req.Book.GetName() != "shelves/1/books/1" {
62 | return nil, grpc.Errorf(codes.NotFound, "not found")
63 | }
64 | if req.UpdateMask.Paths[0] != "book.title" {
65 | return nil, grpc.Errorf(codes.InvalidArgument, "invalid field mask")
66 | }
67 | return req.Book, nil
68 | }
69 | func (testService) DeleteBook(ctx context.Context, req *librarypb.DeleteBookRequest) (*emptypb.Empty, error) {
70 | if req.Name != "shelves/1/books/1" {
71 | return nil, grpc.Errorf(codes.NotFound, "not found")
72 | }
73 | return &emptypb.Empty{}, nil
74 | }
75 |
--------------------------------------------------------------------------------
/benchmarks/testdata/envoy.yaml:
--------------------------------------------------------------------------------
1 | admin:
2 | address:
3 | socket_address:
4 | address: 127.0.0.1
5 | port_value: 9901
6 |
7 | static_resources:
8 | listeners:
9 | - name: listener_0
10 | address:
11 | socket_address:
12 | address: 0.0.0.0
13 | port_value: 10000
14 | filter_chains:
15 | - filters:
16 | - name: envoy.filters.network.http_connection_manager
17 | typed_config:
18 | "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
19 | stat_prefix: grpc_json
20 | codec_type: AUTO
21 | route_config:
22 | name: local_route
23 | virtual_hosts:
24 | - name: local_service
25 | domains: ["*"]
26 | routes:
27 | # NOTE: by default, matching happens based on the gRPC route, and not on the incoming request path.
28 | # Reference: https://envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter#route-configs-for-transcoded-requests
29 | - match: {prefix: "/larking.benchmarks.librarypb.LibraryService"}
30 | route: {cluster: grpc, timeout: 60s}
31 | http_filters:
32 | - name: envoy.filters.http.grpc_json_transcoder
33 | typed_config:
34 | "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
35 | proto_descriptor: "api/proto.pb"
36 | services: ["larking.benchmarks.librarypb.LibraryService"]
37 | print_options:
38 | add_whitespace: true
39 | always_print_primitive_fields: true
40 | always_print_enums_as_ints: false
41 | preserve_proto_field_names: false
42 | - name: envoy.filters.http.router
43 | typed_config:
44 | "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
45 |
46 | clusters:
47 | - name: grpc
48 | type: LOGICAL_DNS
49 | lb_policy: ROUND_ROBIN
50 | dns_lookup_family: V4_ONLY
51 | typed_extension_protocol_options:
52 | envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
53 | "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
54 | explicit_http_config:
55 | http2_protocol_options: {}
56 | load_assignment:
57 | cluster_name: grpc
58 | endpoints:
59 | - lb_endpoints:
60 | - endpoint:
61 | address:
62 | socket_address:
63 | address: localhost
64 | port_value: 5050
65 |
--------------------------------------------------------------------------------
/benchmarks/twirp_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "net"
7 | "net/http"
8 | "testing"
9 |
10 | "github.com/twitchtv/twirp"
11 | "golang.org/x/sync/errgroup"
12 | "larking.io/benchmarks/api/librarypb"
13 | "larking.io/larking"
14 | )
15 |
16 | func TestTwirp(t *testing.T) {
17 | ctx := context.Background()
18 | svc := &testService{}
19 |
20 | mux, err := larking.NewMux()
21 | if err != nil {
22 | t.Fatal(err)
23 | }
24 | librarypb.RegisterLibraryServiceServer(mux, svc)
25 |
26 | ts, err := larking.NewServer(mux,
27 | larking.MuxHandleOption("/", "/twirp"),
28 | )
29 | if err != nil {
30 | t.Fatal(err)
31 | }
32 |
33 | lis, err := net.Listen("tcp", "localhost:0")
34 | if err != nil {
35 | t.Fatalf("failed to listen: %v", err)
36 | }
37 | defer lis.Close()
38 |
39 | var g errgroup.Group
40 | defer func() {
41 | if err := g.Wait(); err != nil {
42 | t.Fatal(err)
43 | }
44 | t.Log("all server shutdown")
45 | }()
46 | g.Go(func() (err error) {
47 | if err := ts.Serve(lis); err != nil && err != http.ErrServerClosed {
48 | return err
49 | }
50 | return nil
51 | })
52 | defer func() {
53 | t.Log("shutdown server")
54 | if err := ts.Shutdown(ctx); err != nil {
55 | t.Fatal(err)
56 | }
57 | }()
58 |
59 | { // proto
60 | ccproto := librarypb.NewLibraryServiceProtobufClient("http://"+lis.Addr().String(), &http.Client{})
61 |
62 | book, err := ccproto.GetBook(ctx, &librarypb.GetBookRequest{
63 | Name: "shelves/1/books/1",
64 | })
65 | if err != nil {
66 | t.Fatal(err)
67 | }
68 | t.Log(book, err)
69 |
70 | // 404
71 | if _, err = ccproto.GetBook(ctx, &librarypb.GetBookRequest{
72 | Name: "shelves/1/books/404",
73 | }); err == nil {
74 | t.Fatal("should be 404")
75 | } else {
76 | var twerr twirp.Error
77 | if errors.As(err, &twerr) {
78 | t.Log(twerr.Code(), twerr.Msg())
79 | if twerr.Code() != twirp.NotFound {
80 | t.Errorf("should be %s, but got %s", twirp.NotFound, twerr.Code())
81 | }
82 | } else {
83 | t.Error(err)
84 | }
85 | }
86 |
87 | if _, err := ccproto.CreateBook(ctx, &librarypb.CreateBookRequest{
88 | Parent: "shelves/1",
89 | Book: &librarypb.Book{
90 | Name: "shelves/1/books/2",
91 | Title: "book2",
92 | },
93 | }); err != nil {
94 | t.Error(err)
95 | }
96 |
97 | if _, err := ccproto.DeleteBook(ctx, &librarypb.DeleteBookRequest{
98 | Name: "shelves/1/books/1",
99 | }); err != nil {
100 | t.Error(err)
101 | }
102 | }
103 |
104 | { // json
105 | ccjson := librarypb.NewLibraryServiceJSONClient("http://"+lis.Addr().String(), &http.Client{})
106 | book, err := ccjson.GetBook(ctx, &librarypb.GetBookRequest{
107 | Name: "shelves/1/books/1",
108 | })
109 | if err != nil {
110 | t.Fatal(err)
111 | }
112 | t.Log(book, err)
113 |
114 | // 404
115 | if _, err = ccjson.GetBook(ctx, &librarypb.GetBookRequest{
116 | Name: "shelves/1/books/404",
117 | }); err == nil {
118 | t.Fatal("should be 404")
119 | } else {
120 | var twerr twirp.Error
121 | if errors.As(err, &twerr) {
122 | t.Log(twerr.Code(), twerr.Msg())
123 | if twerr.Code() != twirp.NotFound {
124 | t.Errorf("should be %s, but got %s", twirp.NotFound, twerr.Code())
125 | }
126 | } else {
127 | t.Error(err)
128 | }
129 | }
130 |
131 | if _, err := ccjson.CreateBook(ctx, &librarypb.CreateBookRequest{
132 | Parent: "shelves/1",
133 | Book: &librarypb.Book{
134 | Name: "shelves/1/books/2",
135 | Title: "book2",
136 | },
137 | }); err != nil {
138 | t.Error(err)
139 | }
140 |
141 | if _, err := ccjson.DeleteBook(ctx, &librarypb.DeleteBookRequest{
142 | Name: "shelves/1/books/1",
143 | }); err != nil {
144 | t.Error(err)
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/docs/LARK_PROTOCOL.md:
--------------------------------------------------------------------------------
1 | # lark (alpha)
2 |
3 | ## Overview
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/intro.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: intro
3 | title: Getting Started
4 | sidebar_label: Getting Started
5 | ---
6 |
7 | TODO
8 |
9 | ---
10 |
11 | ## Setup
12 |
13 | ## Debugging
14 |
15 | Checkout [protobuf](https://github.com/golang/protobuf) at the latest v2 relase.
16 | Go install each protoc generation bin.
17 |
18 | Regenerate protoc buffers:
19 |
20 | ```
21 | protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. testpb/test.proto
22 | ```
23 |
24 | ### Protoc
25 |
26 | Must have googleapis protos avaliable.
27 | Just link API to `/usr/local/include/google` so protoc can find it.
28 | ```
29 | ln -s ~/src/github.com/googleapis/googleapis/google/api/ /usr/local/include/google/
30 | ```
31 |
--------------------------------------------------------------------------------
/docs/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net"
6 |
7 | "google.golang.org/genproto/googleapis/api/serviceconfig"
8 | healthpb "google.golang.org/grpc/health/grpc_health_v1"
9 | "larking.io/health"
10 | "larking.io/larking"
11 | )
12 |
13 | func main() {
14 | // Create a health service. The health service is used to check the status
15 | // of services running within the server.
16 | healthSvc := health.NewServer()
17 | healthSvc.SetServingStatus("example.up.Service", healthpb.HealthCheckResponse_SERVING)
18 | healthSvc.SetServingStatus("example.down.Service", healthpb.HealthCheckResponse_NOT_SERVING)
19 |
20 | serviceConfig := &serviceconfig.Service{}
21 | // AddHealthz adds a /v1/healthz endpoint to the service binding to the
22 | // grpc.health.v1.Health service:
23 | // - get /v1/healthz -> grpc.health.v1.Health.Check
24 | // - websocket /v1/healthz -> grpc.health.v1.Health.Watch
25 | health.AddHealthz(serviceConfig)
26 |
27 | // Mux impements http.Handler and serves both gRPC and HTTP connections.
28 | mux, err := larking.NewMux(
29 | larking.ServiceConfigOption(serviceConfig),
30 | )
31 | if err != nil {
32 | log.Fatal(err)
33 | }
34 | // RegisterHealthServer registers a HealthServer to the mux.
35 | healthpb.RegisterHealthServer(mux, healthSvc)
36 |
37 | // Server creates a *http.Server.
38 | svr, err := larking.NewServer(mux)
39 | if err != nil {
40 | log.Fatal(err)
41 | }
42 |
43 | // Listen on TCP port 8080 on all interfaces.
44 | lis, err := net.Listen("tcp", "localhost:8080")
45 | if err != nil {
46 | log.Fatalf("failed to listen: %v", err)
47 | }
48 | defer lis.Close()
49 |
50 | // Serve starts the server and blocks until the server stops.
51 | // http://localhost:8080/v1/healthz
52 | log.Println("gRPC & HTTP server listening on", lis.Addr())
53 | if err := svr.Serve(lis); err != nil {
54 | log.Fatalf("failed to serve: %v", err)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/examples/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@bazel_gazelle//:def.bzl", "gazelle")
2 |
3 | # gazelle:prefix larking.io/examples
4 | gazelle(name = "gazelle")
5 |
--------------------------------------------------------------------------------
/examples/WORKSPACE:
--------------------------------------------------------------------------------
1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
2 |
3 | http_archive(
4 | name = "com_google_protobuf",
5 | sha256 = "782160a6eae4bddfa4061ff5f7dcf04c9ed1494a0f0c6408b4af6b4a31ab9876",
6 | strip_prefix = "protobuf-22.0",
7 | urls = [
8 | "https://mirror.bazel.build/github.com/protocolbuffers/protobuf/archive/v22.0.tar.gz",
9 | "https://github.com/protocolbuffers/protobuf/archive/v22.0.tar.gz",
10 | ],
11 | )
12 |
13 | load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
14 |
15 | protobuf_deps()
16 |
17 | # Download rules_go ruleset.
18 | # Bazel makes a https call and downloads the zip file, and then
19 | # checks the sha.
20 | http_archive(
21 | name = "io_bazel_rules_go",
22 | sha256 = "6b65cb7917b4d1709f9410ffe00ecf3e160edf674b78c54a894471320862184f",
23 | urls = [
24 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.39.0/rules_go-v0.39.0.zip",
25 | "https://github.com/bazelbuild/rules_go/releases/download/v0.39.0/rules_go-v0.39.0.zip",
26 | ],
27 | )
28 |
29 | # Download the bazel_gazelle ruleset.
30 | http_archive(
31 | name = "bazel_gazelle",
32 | sha256 = "efbbba6ac1a4fd342d5122cbdfdb82aeb2cf2862e35022c752eaddffada7c3f3",
33 | urls = [
34 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.27.0/bazel-gazelle-v0.27.0.tar.gz",
35 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.27.0/bazel-gazelle-v0.27.0.tar.gz",
36 | ],
37 | )
38 |
39 | # Load rules_go ruleset and expose the toolchain and dep rules.
40 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
41 |
42 | # the line below instructs gazelle to save the go dependency definitions
43 | # in the deps.bzl file. Located under '//'.
44 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
45 | load("//:deps.bzl", "go_dependencies")
46 |
47 | # gazelle:repository_macro deps.bzl%go_dependencies
48 | go_dependencies()
49 |
50 | go_rules_dependencies()
51 |
52 | go_register_toolchains(version = "1.20.2")
53 |
54 | gazelle_dependencies()
55 |
56 | rules_python_version = "740825b7f74930c62f44af95c9a4c1bd428d2c53" # Latest @ 2021-06-23
57 |
58 | http_archive(
59 | name = "rules_python",
60 | # Bazel will print the proper value to add here during the first build.
61 | # sha256 = "FIXME",
62 | strip_prefix = "rules_python-{}".format(rules_python_version),
63 | url = "https://github.com/bazelbuild/rules_python/archive/{}.zip".format(rules_python_version),
64 | )
65 |
--------------------------------------------------------------------------------
/examples/api/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_library")
2 |
3 | #load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_proto_library")
4 | load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
5 | load("@rules_proto//proto:defs.bzl", "proto_library")
6 | load("@com_github_grpc_grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library")
7 | load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library")
8 |
9 | proto_library(
10 | name = "helloworld_proto",
11 | srcs = ["helloworld.proto"],
12 | visibility = ["//visibility:public"],
13 | deps = ["@go_googleapis//google/api:annotations_proto"],
14 | )
15 |
16 | cc_proto_library(
17 | name = "helloworld_cc_proto",
18 | visibility = ["//visibility:public"],
19 | deps = [":helloworld_proto"],
20 | )
21 |
22 | cc_grpc_library(
23 | name = "helloworld_cc_grpc",
24 | srcs = [":helloworld_proto"],
25 | grpc_only = True,
26 | visibility = ["//visibility:public"],
27 | deps = [":helloworld_cc_proto"],
28 | )
29 |
30 | java_proto_library(
31 | name = "helloworld_java_proto",
32 | visibility = ["//visibility:public"],
33 | deps = [":helloworld_proto"],
34 | )
35 |
36 | java_grpc_library(
37 | name = "helloworld_java_grpc",
38 | srcs = [":helloworld_proto"],
39 | visibility = ["//visibility:public"],
40 | deps = [":helloworld_java_proto"],
41 | )
42 |
43 | proto_library(
44 | name = "hellostream_proto",
45 | srcs = ["hellostream.proto"],
46 | visibility = ["//visibility:public"],
47 | deps = ["@go_googleapis//google/api:annotations_proto"],
48 | )
49 |
50 | cc_proto_library(
51 | name = "hellostream_cc_proto",
52 | visibility = ["//visibility:public"],
53 | deps = [":hellostream_proto"],
54 | )
55 |
56 | cc_grpc_library(
57 | name = "hellostream_cc_grpc",
58 | srcs = [":hellostream_proto"],
59 | grpc_only = True,
60 | visibility = ["//visibility:public"],
61 | deps = [":hellostream_cc_proto"],
62 | )
63 |
64 | java_proto_library(
65 | name = "hello_streaming_java_proto",
66 | visibility = ["//visibility:public"],
67 | deps = [":hello_streaming_proto"],
68 | )
69 |
70 | java_grpc_library(
71 | name = "hello_streaming_java_grpc",
72 | srcs = [":hello_streaming_proto"],
73 | visibility = ["//visibility:public"],
74 | deps = [":hello_streaming_java_proto"],
75 | )
76 |
77 | go_proto_library(
78 | name = "annotations_go_proto",
79 | importpath = "",
80 | protos = ["@googleapis//google/api:annotations_proto"],
81 | )
82 |
83 | go_proto_library(
84 | name = "helloworld_go_proto",
85 | compilers = ["@io_bazel_rules_go//proto:go_grpc"],
86 | importpath = "github.com/emcfarlane/larking/examples/proto/helloworld",
87 | protos = [
88 | #":hellostream_proto",
89 | ":helloworld_proto",
90 | ],
91 | visibility = ["//visibility:public"],
92 | deps = [
93 | "@go_googleapis//google/api:annotations_go_proto",
94 | "@go_googleapis//google/api:httpbody_go_proto",
95 | "@org_golang_google_grpc//:go_default_library",
96 | "@org_golang_google_grpc//codes:go_default_library",
97 | #"@org_golang_google_grpc//status:go_default_library",
98 | "@org_golang_google_protobuf//reflect/protoreflect:go_default_library",
99 | "@org_golang_google_protobuf//runtime/protoimpl:go_default_library",
100 | ],
101 | )
102 |
103 | # TODO: https://github.com/bazelbuild/rules_go/pull/2740
104 | #go_library(
105 | # name = "go_default_library",
106 | # srcs = [
107 | # "hellostream.pb.go",
108 | # "hellostream_grpc.pb.go",
109 | # "helloworld.pb.go",
110 | # "helloworld_grpc.pb.go",
111 | # ],
112 | # importpath = "github.com/emcfarlane/larking/examples/proto",
113 | # visibility = ["//visibility:public"],
114 | # deps = [
115 | # "@com_github_golang_protobuf//proto:go_default_library",
116 | # "@go_googleapis//google/api:annotations_go_proto",
117 | # "@org_golang_google_grpc//:go_default_library",
118 | # "@org_golang_google_grpc//codes:go_default_library",
119 | # "@org_golang_google_grpc//status:go_default_library",
120 | # "@org_golang_google_protobuf//reflect/protoreflect:go_default_library",
121 | # "@org_golang_google_protobuf//runtime/protoimpl:go_default_library",
122 | # ],
123 | #)
124 |
125 | go_proto_library(
126 | name = "proto_go_proto",
127 | compilers = ["@io_bazel_rules_go//proto:go_grpc"],
128 | importpath = "larking.io/example/proto/hellostream",
129 | proto = ":hellostream_proto",
130 | visibility = ["//visibility:public"],
131 | deps = ["@go_googleapis//google/api:annotations_go_proto"],
132 | )
133 |
134 | go_library(
135 | name = "helloworld",
136 | embed = [":proto_go_proto"],
137 | importpath = "larking.io/example/proto/helloworld",
138 | visibility = ["//visibility:public"],
139 | )
140 |
141 | go_library(
142 | name = "hellostream",
143 | embed = [":proto_go_proto"],
144 | importpath = "larking.io/example/proto/hellostream",
145 | visibility = ["//visibility:public"],
146 | )
147 |
--------------------------------------------------------------------------------
/examples/api/hellostream.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_multiple_files = true;
4 | option java_package = "com.examples.hellostream";
5 | option java_outer_classname = "HelloStreamProto";
6 | option objc_class_prefix = "HLWS";
7 | option go_package = "larking.io/examples/proto/hellostream;proto";
8 |
9 | package hellostream;
10 |
11 | import "google/api/annotations.proto";
12 |
13 | // The greeting service definition.
14 | service StreamingGreeter {
15 | // Streams a many greetings
16 | rpc SayHelloStreaming(stream HelloRequest) returns (stream HelloReply) {
17 | option (google.api.http) = {
18 | post : "/hello"
19 | };
20 | }
21 | }
22 |
23 | // The request message containing the user's name.
24 | message HelloRequest { string name = 1; }
25 |
26 | // The response message containing the greetings
27 | message HelloReply { string message = 1; }
28 |
--------------------------------------------------------------------------------
/examples/api/helloworld.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option java_multiple_files = true;
4 | option java_package = "com.examples.helloworld";
5 | option java_outer_classname = "HelloWorldProto";
6 | option objc_class_prefix = "HLW";
7 | option go_package = "larking.io/examples/api/helloworld;proto";
8 |
9 | package helloworld;
10 |
11 | import "google/api/annotations.proto";
12 |
13 | // The greeting service definition.
14 | service Greeter {
15 | // Sends a greeting
16 | rpc SayHello(HelloRequest) returns (HelloReply) {
17 | option (google.api.http) = {
18 | get : "/hello/{name}"
19 | };
20 | }
21 | }
22 |
23 | // The request message containing the user's name.
24 | message HelloRequest { string name = 1; }
25 |
26 | // The response message containing the greetings
27 | message HelloReply { string message = 1; }
28 |
--------------------------------------------------------------------------------
/examples/api/helloworld/helloworld.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.30.0
4 | // protoc v3.21.12
5 | // source: api/helloworld.proto
6 |
7 | package proto
8 |
9 | import (
10 | _ "google.golang.org/genproto/googleapis/api/annotations"
11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
13 | reflect "reflect"
14 | sync "sync"
15 | )
16 |
17 | const (
18 | // Verify that this generated code is sufficiently up-to-date.
19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
20 | // Verify that runtime/protoimpl is sufficiently up-to-date.
21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
22 | )
23 |
24 | // The request message containing the user's name.
25 | type HelloRequest struct {
26 | state protoimpl.MessageState
27 | sizeCache protoimpl.SizeCache
28 | unknownFields protoimpl.UnknownFields
29 |
30 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
31 | }
32 |
33 | func (x *HelloRequest) Reset() {
34 | *x = HelloRequest{}
35 | if protoimpl.UnsafeEnabled {
36 | mi := &file_api_helloworld_proto_msgTypes[0]
37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
38 | ms.StoreMessageInfo(mi)
39 | }
40 | }
41 |
42 | func (x *HelloRequest) String() string {
43 | return protoimpl.X.MessageStringOf(x)
44 | }
45 |
46 | func (*HelloRequest) ProtoMessage() {}
47 |
48 | func (x *HelloRequest) ProtoReflect() protoreflect.Message {
49 | mi := &file_api_helloworld_proto_msgTypes[0]
50 | if protoimpl.UnsafeEnabled && x != nil {
51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
52 | if ms.LoadMessageInfo() == nil {
53 | ms.StoreMessageInfo(mi)
54 | }
55 | return ms
56 | }
57 | return mi.MessageOf(x)
58 | }
59 |
60 | // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
61 | func (*HelloRequest) Descriptor() ([]byte, []int) {
62 | return file_api_helloworld_proto_rawDescGZIP(), []int{0}
63 | }
64 |
65 | func (x *HelloRequest) GetName() string {
66 | if x != nil {
67 | return x.Name
68 | }
69 | return ""
70 | }
71 |
72 | // The response message containing the greetings
73 | type HelloReply struct {
74 | state protoimpl.MessageState
75 | sizeCache protoimpl.SizeCache
76 | unknownFields protoimpl.UnknownFields
77 |
78 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
79 | }
80 |
81 | func (x *HelloReply) Reset() {
82 | *x = HelloReply{}
83 | if protoimpl.UnsafeEnabled {
84 | mi := &file_api_helloworld_proto_msgTypes[1]
85 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
86 | ms.StoreMessageInfo(mi)
87 | }
88 | }
89 |
90 | func (x *HelloReply) String() string {
91 | return protoimpl.X.MessageStringOf(x)
92 | }
93 |
94 | func (*HelloReply) ProtoMessage() {}
95 |
96 | func (x *HelloReply) ProtoReflect() protoreflect.Message {
97 | mi := &file_api_helloworld_proto_msgTypes[1]
98 | if protoimpl.UnsafeEnabled && x != nil {
99 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
100 | if ms.LoadMessageInfo() == nil {
101 | ms.StoreMessageInfo(mi)
102 | }
103 | return ms
104 | }
105 | return mi.MessageOf(x)
106 | }
107 |
108 | // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead.
109 | func (*HelloReply) Descriptor() ([]byte, []int) {
110 | return file_api_helloworld_proto_rawDescGZIP(), []int{1}
111 | }
112 |
113 | func (x *HelloReply) GetMessage() string {
114 | if x != nil {
115 | return x.Message
116 | }
117 | return ""
118 | }
119 |
120 | var File_api_helloworld_proto protoreflect.FileDescriptor
121 |
122 | var file_api_helloworld_proto_rawDesc = []byte{
123 | 0x0a, 0x14, 0x61, 0x70, 0x69, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64,
124 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72,
125 | 0x6c, 0x64, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61,
126 | 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
127 | 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
128 | 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
129 | 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70,
130 | 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20,
131 | 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x5e, 0x0a, 0x07,
132 | 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65,
133 | 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64,
134 | 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e,
135 | 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
136 | 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f,
137 | 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x42, 0x5c, 0x0a, 0x17,
138 | 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x68, 0x65, 0x6c,
139 | 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x42, 0x0f, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x57, 0x6f,
140 | 0x72, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x28, 0x6c, 0x61, 0x72, 0x6b,
141 | 0x69, 0x6e, 0x67, 0x2e, 0x69, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f,
142 | 0x61, 0x70, 0x69, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x3b, 0x70,
143 | 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, 0x03, 0x48, 0x4c, 0x57, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
144 | 0x6f, 0x33,
145 | }
146 |
147 | var (
148 | file_api_helloworld_proto_rawDescOnce sync.Once
149 | file_api_helloworld_proto_rawDescData = file_api_helloworld_proto_rawDesc
150 | )
151 |
152 | func file_api_helloworld_proto_rawDescGZIP() []byte {
153 | file_api_helloworld_proto_rawDescOnce.Do(func() {
154 | file_api_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_helloworld_proto_rawDescData)
155 | })
156 | return file_api_helloworld_proto_rawDescData
157 | }
158 |
159 | var file_api_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
160 | var file_api_helloworld_proto_goTypes = []interface{}{
161 | (*HelloRequest)(nil), // 0: helloworld.HelloRequest
162 | (*HelloReply)(nil), // 1: helloworld.HelloReply
163 | }
164 | var file_api_helloworld_proto_depIdxs = []int32{
165 | 0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest
166 | 1, // 1: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply
167 | 1, // [1:2] is the sub-list for method output_type
168 | 0, // [0:1] is the sub-list for method input_type
169 | 0, // [0:0] is the sub-list for extension type_name
170 | 0, // [0:0] is the sub-list for extension extendee
171 | 0, // [0:0] is the sub-list for field type_name
172 | }
173 |
174 | func init() { file_api_helloworld_proto_init() }
175 | func file_api_helloworld_proto_init() {
176 | if File_api_helloworld_proto != nil {
177 | return
178 | }
179 | if !protoimpl.UnsafeEnabled {
180 | file_api_helloworld_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
181 | switch v := v.(*HelloRequest); i {
182 | case 0:
183 | return &v.state
184 | case 1:
185 | return &v.sizeCache
186 | case 2:
187 | return &v.unknownFields
188 | default:
189 | return nil
190 | }
191 | }
192 | file_api_helloworld_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
193 | switch v := v.(*HelloReply); i {
194 | case 0:
195 | return &v.state
196 | case 1:
197 | return &v.sizeCache
198 | case 2:
199 | return &v.unknownFields
200 | default:
201 | return nil
202 | }
203 | }
204 | }
205 | type x struct{}
206 | out := protoimpl.TypeBuilder{
207 | File: protoimpl.DescBuilder{
208 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
209 | RawDescriptor: file_api_helloworld_proto_rawDesc,
210 | NumEnums: 0,
211 | NumMessages: 2,
212 | NumExtensions: 0,
213 | NumServices: 1,
214 | },
215 | GoTypes: file_api_helloworld_proto_goTypes,
216 | DependencyIndexes: file_api_helloworld_proto_depIdxs,
217 | MessageInfos: file_api_helloworld_proto_msgTypes,
218 | }.Build()
219 | File_api_helloworld_proto = out.File
220 | file_api_helloworld_proto_rawDesc = nil
221 | file_api_helloworld_proto_goTypes = nil
222 | file_api_helloworld_proto_depIdxs = nil
223 | }
224 |
--------------------------------------------------------------------------------
/examples/api/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 v3.21.12
5 | // source: api/helloworld.proto
6 |
7 | package proto
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 | // Sends a greeting
30 | SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
31 | }
32 |
33 | type greeterClient struct {
34 | cc grpc.ClientConnInterface
35 | }
36 |
37 | func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
38 | return &greeterClient{cc}
39 | }
40 |
41 | func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
42 | out := new(HelloReply)
43 | err := c.cc.Invoke(ctx, Greeter_SayHello_FullMethodName, in, out, opts...)
44 | if err != nil {
45 | return nil, err
46 | }
47 | return out, nil
48 | }
49 |
50 | // GreeterServer is the server API for Greeter service.
51 | // All implementations must embed UnimplementedGreeterServer
52 | // for forward compatibility
53 | type GreeterServer interface {
54 | // Sends a greeting
55 | SayHello(context.Context, *HelloRequest) (*HelloReply, error)
56 | mustEmbedUnimplementedGreeterServer()
57 | }
58 |
59 | // UnimplementedGreeterServer must be embedded to have forward compatible implementations.
60 | type UnimplementedGreeterServer struct {
61 | }
62 |
63 | func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
64 | return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
65 | }
66 | func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
67 |
68 | // UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service.
69 | // Use of this interface is not recommended, as added methods to GreeterServer will
70 | // result in compilation errors.
71 | type UnsafeGreeterServer interface {
72 | mustEmbedUnimplementedGreeterServer()
73 | }
74 |
75 | func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) {
76 | s.RegisterService(&Greeter_ServiceDesc, srv)
77 | }
78 |
79 | func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
80 | in := new(HelloRequest)
81 | if err := dec(in); err != nil {
82 | return nil, err
83 | }
84 | if interceptor == nil {
85 | return srv.(GreeterServer).SayHello(ctx, in)
86 | }
87 | info := &grpc.UnaryServerInfo{
88 | Server: srv,
89 | FullMethod: Greeter_SayHello_FullMethodName,
90 | }
91 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
92 | return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
93 | }
94 | return interceptor(ctx, in, info, handler)
95 | }
96 |
97 | // Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service.
98 | // It's only intended for direct use with grpc.RegisterService,
99 | // and not to be introspected or modified (even as a copy)
100 | var Greeter_ServiceDesc = grpc.ServiceDesc{
101 | ServiceName: "helloworld.Greeter",
102 | HandlerType: (*GreeterServer)(nil),
103 | Methods: []grpc.MethodDesc{
104 | {
105 | MethodName: "SayHello",
106 | Handler: _Greeter_SayHello_Handler,
107 | },
108 | },
109 | Streams: []grpc.StreamDesc{},
110 | Metadata: "api/helloworld.proto",
111 | }
112 |
--------------------------------------------------------------------------------
/examples/cpp/helloworld/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@rules_cc//cc:defs.bzl", "cc_binary")
2 |
3 | cc_binary(
4 | name = "helloworld",
5 | srcs = ["hello_world.cc"],
6 | defines = ["BAZEL_BUILD"],
7 | deps = [
8 | "//examples/api:helloworld_cc_grpc",
9 | "//examples/api:helloworld_cc_proto",
10 | "@com_github_grpc_grpc//:grpc++",
11 | "@com_github_grpc_grpc//:grpc++_reflection",
12 | ],
13 | )
14 |
--------------------------------------------------------------------------------
/examples/cpp/helloworld/hello_world.cc:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #include
6 | #include
7 | #include
8 |
9 | #ifdef BAZEL_BUILD
10 | #include "examples/proto/helloworld.grpc.pb.h"
11 | #else
12 | #include "helloworld.grpc.pb.h"
13 | #endif
14 |
15 | using grpc::Server;
16 | using grpc::ServerBuilder;
17 | using grpc::ServerContext;
18 | using grpc::Status;
19 | using helloworld::Greeter;
20 | using helloworld::HelloReply;
21 | using helloworld::HelloRequest;
22 |
23 | // Logic and data behind the server's behavior.
24 | class GreeterServiceImpl final : public Greeter::Service {
25 | Status SayHello(ServerContext *context, const HelloRequest *request,
26 | HelloReply *reply) override {
27 | std::string prefix("Hello ");
28 | reply->set_message(prefix + request->name());
29 | return Status::OK;
30 | }
31 | };
32 |
33 | void RunServer() {
34 | std::string server_address("0.0.0.0:50051");
35 | GreeterServiceImpl service;
36 |
37 | grpc::EnableDefaultHealthCheckService(true);
38 | grpc::reflection::InitProtoReflectionServerBuilderPlugin();
39 | ServerBuilder builder;
40 | // Listen on the given address without any authentication mechanism.
41 | builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
42 | // Register "service" as the instance through which we'll communicate with
43 | // clients. In this case it corresponds to an *synchronous* service.
44 | builder.RegisterService(&service);
45 | // Finally assemble the server.
46 | std::unique_ptr server(builder.BuildAndStart());
47 | std::cout << "Server listening on " << server_address << std::endl;
48 |
49 | // Wait for the server to shutdown. Note that some other thread must be
50 | // responsible for shutting down the server for this call to ever return.
51 | server->Wait();
52 | }
53 |
54 | int main(int argc, char **argv) {
55 | RunServer();
56 |
57 | return 0;
58 | }
59 |
--------------------------------------------------------------------------------
/examples/deps.bzl:
--------------------------------------------------------------------------------
1 | load("@bazel_gazelle//:deps.bzl", "go_repository")
2 |
3 | def go_dependencies():
4 | go_repository(
5 | name = "com_github_soheilhy_cmux",
6 | importpath = "github.com/soheilhy/cmux",
7 | sum = "h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=",
8 | version = "v0.1.5",
9 | )
10 |
11 | go_repository(
12 | name = "org_golang_google_grpc",
13 | importpath = "google.golang.org/grpc",
14 | sum = "h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=",
15 | version = "v1.54.0",
16 | )
17 |
--------------------------------------------------------------------------------
/examples/gen.sh:
--------------------------------------------------------------------------------
1 | protoc -I ~/src/github.com/googleapis/api-common-protos/ --go_out=module=larking.io/examples:. --go-grpc_out=module=larking.io/examples:. -I. ./api/*.proto
2 |
--------------------------------------------------------------------------------
/examples/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/emcfarlane/larking/examples
2 |
3 | replace larking.io/ => ../
4 |
5 |
--------------------------------------------------------------------------------
/examples/go/helloworld/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
2 |
3 | go_library(
4 | name = "go_default_library",
5 | srcs = ["main.go"],
6 | importpath = "github.com/emcfarlane/larking/examples/go/helloworld",
7 | visibility = ["//visibility:private"],
8 | deps = [
9 | "//:go_default_library",
10 | "//examples/proto:helloworld_go_proto",
11 | "@com_github_soheilhy_cmux//:go_default_library",
12 | "@org_golang_google_grpc//:go_default_library",
13 | "@org_golang_google_grpc//reflection:go_default_library",
14 | ],
15 | )
16 |
17 | go_binary(
18 | name = "helloworld",
19 | embed = [":helloworld_lib"],
20 | visibility = ["//visibility:public"],
21 | )
22 |
23 | go_library(
24 | name = "helloworld_lib",
25 | srcs = ["main.go"],
26 | importpath = "larking.io/examples/go/helloworld",
27 | visibility = ["//visibility:private"],
28 | deps = [
29 | "@com_github_soheilhy_cmux//:cmux",
30 | "@org_golang_google_grpc//:go_default_library",
31 | "@org_golang_google_grpc//reflection",
32 | ],
33 | )
34 |
--------------------------------------------------------------------------------
/examples/go/helloworld/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "net"
9 | "net/http"
10 |
11 | "github.com/soheilhy/cmux"
12 | "google.golang.org/grpc"
13 | "google.golang.org/grpc/reflection"
14 | pb "larking.io/examples/proto/helloworld"
15 | "larking.io/larking"
16 | )
17 |
18 | var (
19 | flagPort = flag.String("port", "8000", "Port")
20 | flagHost = flag.String("host", "", "Host")
21 | )
22 |
23 | type Server struct {
24 | pb.UnimplementedGreeterServer
25 | }
26 |
27 | func (s *Server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
28 | return &pb.HelloReply{
29 | Message: fmt.Sprintf("Hello, %s! <3 Go", req.Name),
30 | }, nil
31 | }
32 |
33 | func (s *Server) serve(l net.Listener) error {
34 | m := cmux.New(l)
35 |
36 | grpcL := m.Match(cmux.HTTP2HeaderField("content-type", "application/grpc"))
37 | httpL := m.Match(cmux.Any())
38 |
39 | gs := grpc.NewServer()
40 | pb.RegisterGreeterServer(gs, s)
41 | reflection.Register(gs)
42 |
43 | // Register HTTP handler in-process to server both the GRPC server and
44 | // the HTTP server on one port.
45 | hd := &larking.Handler{}
46 | if err := hd.RegisterServiceByName("helloworld.Greeter", s); err != nil {
47 | return err
48 | }
49 |
50 | hs := &http.Server{
51 | Handler: hd,
52 | }
53 |
54 | errs := make(chan error, 3)
55 |
56 | go func() { errs <- gs.Serve(grpcL) }()
57 | defer gs.Stop()
58 |
59 | go func() { errs <- hs.Serve(httpL) }()
60 | defer hs.Close()
61 |
62 | go func() { errs <- m.Serve() }()
63 |
64 | return <-errs
65 | }
66 |
67 | func run() error {
68 | flag.Parse()
69 |
70 | l, err := net.Listen("tcp", fmt.Sprintf("%s:%s", *flagHost, *flagPort))
71 | if err != nil {
72 | return err
73 | }
74 |
75 | s := &Server{}
76 | return s.serve(l)
77 | }
78 |
79 | func main() {
80 | flag.Parse()
81 |
82 | if err := run(); err != nil {
83 | log.Fatal(err)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/examples/java/com/example/helloworld/BUILD.bazel:
--------------------------------------------------------------------------------
1 | java_library(
2 | name = "helloworld",
3 | testonly = 1,
4 | srcs = glob(
5 | ["*.java"],
6 | ),
7 | runtime_deps = [
8 | "@io_grpc_grpc_java//netty",
9 | ],
10 | deps = [
11 | "//examples/api:helloworld_java_grpc",
12 | "//examples/api:helloworld_java_proto",
13 | "@com_google_protobuf//:protobuf_java",
14 | "@com_google_protobuf//:protobuf_java_util",
15 | "@io_grpc_grpc_java//api",
16 | "@io_grpc_grpc_java//protobuf",
17 | "@io_grpc_grpc_java//services:reflection",
18 | "@io_grpc_grpc_java//stub",
19 | "@maven//:com_google_api_grpc_proto_google_common_protos",
20 | "@maven//:com_google_code_findbugs_jsr305",
21 | "@maven//:com_google_code_gson_gson",
22 | "@maven//:com_google_guava_guava",
23 | ],
24 | )
25 |
26 | java_binary(
27 | name = "HelloWorldServer",
28 | testonly = 1,
29 | main_class = "com.examples.helloworld.HelloWorldServer",
30 | runtime_deps = [
31 | ":helloworld",
32 | ],
33 | )
34 |
--------------------------------------------------------------------------------
/examples/java/com/example/helloworld/HelloWorldServer.java:
--------------------------------------------------------------------------------
1 | package com.examples.helloworld;
2 |
3 | import io.grpc.Server;
4 | import io.grpc.ServerBuilder;
5 | import io.grpc.protobuf.services.ProtoReflectionService;
6 | import io.grpc.stub.StreamObserver;
7 | import java.io.IOException;
8 | import java.util.concurrent.TimeUnit;
9 | import java.util.logging.Logger;
10 |
11 | /**
12 | * Server that manages startup/shutdown of a {@code Greeter} server.
13 | */
14 | public class HelloWorldServer {
15 | private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName());
16 |
17 | private Server server;
18 |
19 | private void start() throws IOException {
20 | /* The port on which the server should run */
21 | int port = 50051;
22 | server = ServerBuilder.forPort(port)
23 | .addService(new GreeterImpl())
24 | .addService(ProtoReflectionService.newInstance())
25 | .build()
26 | .start();
27 | logger.info("Server started, listening on " + port);
28 | Runtime.getRuntime().addShutdownHook(new Thread() {
29 | @Override
30 | public void run() {
31 | // Use stderr here since the logger may have been reset by its JVM shutdown hook.
32 | System.err.println("*** shutting down gRPC server since JVM is shutting down");
33 | try {
34 | HelloWorldServer.this.stop();
35 | } catch (InterruptedException e) {
36 | e.printStackTrace(System.err);
37 | }
38 | System.err.println("*** server shut down");
39 | }
40 | });
41 | }
42 |
43 | private void stop() throws InterruptedException {
44 | if (server != null) {
45 | server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
46 | }
47 | }
48 |
49 | /**
50 | * Await termination on the main thread since the grpc library uses daemon threads.
51 | */
52 | private void blockUntilShutdown() throws InterruptedException {
53 | if (server != null) {
54 | server.awaitTermination();
55 | }
56 | }
57 |
58 | /**
59 | * Main launches the server from the command line.
60 | */
61 | public static void main(String[] args) throws IOException, InterruptedException {
62 | final HelloWorldServer server = new HelloWorldServer();
63 | server.start();
64 | server.blockUntilShutdown();
65 | }
66 |
67 | static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
68 |
69 | @Override
70 | public void sayHello(HelloRequest req, StreamObserver responseObserver) {
71 | HelloReply reply = HelloReply.newBuilder().setMessage("Hi " + req.getName()).build();
72 | responseObserver.onNext(reply);
73 | responseObserver.onCompleted();
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/examples/proto/hellostream/hellostream.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.30.0
4 | // protoc v3.21.12
5 | // source: api/hellostream.proto
6 |
7 | package proto
8 |
9 | import (
10 | _ "google.golang.org/genproto/googleapis/api/annotations"
11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
13 | reflect "reflect"
14 | sync "sync"
15 | )
16 |
17 | const (
18 | // Verify that this generated code is sufficiently up-to-date.
19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
20 | // Verify that runtime/protoimpl is sufficiently up-to-date.
21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
22 | )
23 |
24 | // The request message containing the user's name.
25 | type HelloRequest struct {
26 | state protoimpl.MessageState
27 | sizeCache protoimpl.SizeCache
28 | unknownFields protoimpl.UnknownFields
29 |
30 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
31 | }
32 |
33 | func (x *HelloRequest) Reset() {
34 | *x = HelloRequest{}
35 | if protoimpl.UnsafeEnabled {
36 | mi := &file_api_hellostream_proto_msgTypes[0]
37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
38 | ms.StoreMessageInfo(mi)
39 | }
40 | }
41 |
42 | func (x *HelloRequest) String() string {
43 | return protoimpl.X.MessageStringOf(x)
44 | }
45 |
46 | func (*HelloRequest) ProtoMessage() {}
47 |
48 | func (x *HelloRequest) ProtoReflect() protoreflect.Message {
49 | mi := &file_api_hellostream_proto_msgTypes[0]
50 | if protoimpl.UnsafeEnabled && x != nil {
51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
52 | if ms.LoadMessageInfo() == nil {
53 | ms.StoreMessageInfo(mi)
54 | }
55 | return ms
56 | }
57 | return mi.MessageOf(x)
58 | }
59 |
60 | // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
61 | func (*HelloRequest) Descriptor() ([]byte, []int) {
62 | return file_api_hellostream_proto_rawDescGZIP(), []int{0}
63 | }
64 |
65 | func (x *HelloRequest) GetName() string {
66 | if x != nil {
67 | return x.Name
68 | }
69 | return ""
70 | }
71 |
72 | // The response message containing the greetings
73 | type HelloReply struct {
74 | state protoimpl.MessageState
75 | sizeCache protoimpl.SizeCache
76 | unknownFields protoimpl.UnknownFields
77 |
78 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
79 | }
80 |
81 | func (x *HelloReply) Reset() {
82 | *x = HelloReply{}
83 | if protoimpl.UnsafeEnabled {
84 | mi := &file_api_hellostream_proto_msgTypes[1]
85 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
86 | ms.StoreMessageInfo(mi)
87 | }
88 | }
89 |
90 | func (x *HelloReply) String() string {
91 | return protoimpl.X.MessageStringOf(x)
92 | }
93 |
94 | func (*HelloReply) ProtoMessage() {}
95 |
96 | func (x *HelloReply) ProtoReflect() protoreflect.Message {
97 | mi := &file_api_hellostream_proto_msgTypes[1]
98 | if protoimpl.UnsafeEnabled && x != nil {
99 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
100 | if ms.LoadMessageInfo() == nil {
101 | ms.StoreMessageInfo(mi)
102 | }
103 | return ms
104 | }
105 | return mi.MessageOf(x)
106 | }
107 |
108 | // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead.
109 | func (*HelloReply) Descriptor() ([]byte, []int) {
110 | return file_api_hellostream_proto_rawDescGZIP(), []int{1}
111 | }
112 |
113 | func (x *HelloReply) GetMessage() string {
114 | if x != nil {
115 | return x.Message
116 | }
117 | return ""
118 | }
119 |
120 | var File_api_hellostream_proto protoreflect.FileDescriptor
121 |
122 | var file_api_hellostream_proto_rawDesc = []byte{
123 | 0x0a, 0x15, 0x61, 0x70, 0x69, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 0x74, 0x72, 0x65, 0x61,
124 | 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 0x74,
125 | 0x72, 0x65, 0x61, 0x6d, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69,
126 | 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f,
127 | 0x74, 0x6f, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65,
128 | 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
129 | 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52,
130 | 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
131 | 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x6f,
132 | 0x0a, 0x10, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x47, 0x72, 0x65, 0x65, 0x74,
133 | 0x65, 0x72, 0x12, 0x5b, 0x0a, 0x11, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74,
134 | 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x19, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73,
135 | 0x74, 0x72, 0x65, 0x61, 0x6d, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65,
136 | 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d,
137 | 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x0e, 0x82, 0xd3, 0xe4,
138 | 0x93, 0x02, 0x08, 0x22, 0x06, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x28, 0x01, 0x30, 0x01, 0x42,
139 | 0x62, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e,
140 | 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x10, 0x48, 0x65, 0x6c,
141 | 0x6c, 0x6f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a,
142 | 0x2b, 0x6c, 0x61, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x69, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d,
143 | 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f,
144 | 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, 0x04, 0x48,
145 | 0x4c, 0x57, 0x53, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
146 | }
147 |
148 | var (
149 | file_api_hellostream_proto_rawDescOnce sync.Once
150 | file_api_hellostream_proto_rawDescData = file_api_hellostream_proto_rawDesc
151 | )
152 |
153 | func file_api_hellostream_proto_rawDescGZIP() []byte {
154 | file_api_hellostream_proto_rawDescOnce.Do(func() {
155 | file_api_hellostream_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_hellostream_proto_rawDescData)
156 | })
157 | return file_api_hellostream_proto_rawDescData
158 | }
159 |
160 | var file_api_hellostream_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
161 | var file_api_hellostream_proto_goTypes = []interface{}{
162 | (*HelloRequest)(nil), // 0: hellostream.HelloRequest
163 | (*HelloReply)(nil), // 1: hellostream.HelloReply
164 | }
165 | var file_api_hellostream_proto_depIdxs = []int32{
166 | 0, // 0: hellostream.StreamingGreeter.SayHelloStreaming:input_type -> hellostream.HelloRequest
167 | 1, // 1: hellostream.StreamingGreeter.SayHelloStreaming:output_type -> hellostream.HelloReply
168 | 1, // [1:2] is the sub-list for method output_type
169 | 0, // [0:1] is the sub-list for method input_type
170 | 0, // [0:0] is the sub-list for extension type_name
171 | 0, // [0:0] is the sub-list for extension extendee
172 | 0, // [0:0] is the sub-list for field type_name
173 | }
174 |
175 | func init() { file_api_hellostream_proto_init() }
176 | func file_api_hellostream_proto_init() {
177 | if File_api_hellostream_proto != nil {
178 | return
179 | }
180 | if !protoimpl.UnsafeEnabled {
181 | file_api_hellostream_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
182 | switch v := v.(*HelloRequest); i {
183 | case 0:
184 | return &v.state
185 | case 1:
186 | return &v.sizeCache
187 | case 2:
188 | return &v.unknownFields
189 | default:
190 | return nil
191 | }
192 | }
193 | file_api_hellostream_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
194 | switch v := v.(*HelloReply); i {
195 | case 0:
196 | return &v.state
197 | case 1:
198 | return &v.sizeCache
199 | case 2:
200 | return &v.unknownFields
201 | default:
202 | return nil
203 | }
204 | }
205 | }
206 | type x struct{}
207 | out := protoimpl.TypeBuilder{
208 | File: protoimpl.DescBuilder{
209 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
210 | RawDescriptor: file_api_hellostream_proto_rawDesc,
211 | NumEnums: 0,
212 | NumMessages: 2,
213 | NumExtensions: 0,
214 | NumServices: 1,
215 | },
216 | GoTypes: file_api_hellostream_proto_goTypes,
217 | DependencyIndexes: file_api_hellostream_proto_depIdxs,
218 | MessageInfos: file_api_hellostream_proto_msgTypes,
219 | }.Build()
220 | File_api_hellostream_proto = out.File
221 | file_api_hellostream_proto_rawDesc = nil
222 | file_api_hellostream_proto_goTypes = nil
223 | file_api_hellostream_proto_depIdxs = nil
224 | }
225 |
--------------------------------------------------------------------------------
/examples/proto/hellostream/hellostream_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 v3.21.12
5 | // source: api/hellostream.proto
6 |
7 | package proto
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 | StreamingGreeter_SayHelloStreaming_FullMethodName = "/hellostream.StreamingGreeter/SayHelloStreaming"
23 | )
24 |
25 | // StreamingGreeterClient is the client API for StreamingGreeter 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 StreamingGreeterClient interface {
29 | // Streams a many greetings
30 | SayHelloStreaming(ctx context.Context, opts ...grpc.CallOption) (StreamingGreeter_SayHelloStreamingClient, error)
31 | }
32 |
33 | type streamingGreeterClient struct {
34 | cc grpc.ClientConnInterface
35 | }
36 |
37 | func NewStreamingGreeterClient(cc grpc.ClientConnInterface) StreamingGreeterClient {
38 | return &streamingGreeterClient{cc}
39 | }
40 |
41 | func (c *streamingGreeterClient) SayHelloStreaming(ctx context.Context, opts ...grpc.CallOption) (StreamingGreeter_SayHelloStreamingClient, error) {
42 | stream, err := c.cc.NewStream(ctx, &StreamingGreeter_ServiceDesc.Streams[0], StreamingGreeter_SayHelloStreaming_FullMethodName, opts...)
43 | if err != nil {
44 | return nil, err
45 | }
46 | x := &streamingGreeterSayHelloStreamingClient{stream}
47 | return x, nil
48 | }
49 |
50 | type StreamingGreeter_SayHelloStreamingClient interface {
51 | Send(*HelloRequest) error
52 | Recv() (*HelloReply, error)
53 | grpc.ClientStream
54 | }
55 |
56 | type streamingGreeterSayHelloStreamingClient struct {
57 | grpc.ClientStream
58 | }
59 |
60 | func (x *streamingGreeterSayHelloStreamingClient) Send(m *HelloRequest) error {
61 | return x.ClientStream.SendMsg(m)
62 | }
63 |
64 | func (x *streamingGreeterSayHelloStreamingClient) Recv() (*HelloReply, error) {
65 | m := new(HelloReply)
66 | if err := x.ClientStream.RecvMsg(m); err != nil {
67 | return nil, err
68 | }
69 | return m, nil
70 | }
71 |
72 | // StreamingGreeterServer is the server API for StreamingGreeter service.
73 | // All implementations must embed UnimplementedStreamingGreeterServer
74 | // for forward compatibility
75 | type StreamingGreeterServer interface {
76 | // Streams a many greetings
77 | SayHelloStreaming(StreamingGreeter_SayHelloStreamingServer) error
78 | mustEmbedUnimplementedStreamingGreeterServer()
79 | }
80 |
81 | // UnimplementedStreamingGreeterServer must be embedded to have forward compatible implementations.
82 | type UnimplementedStreamingGreeterServer struct {
83 | }
84 |
85 | func (UnimplementedStreamingGreeterServer) SayHelloStreaming(StreamingGreeter_SayHelloStreamingServer) error {
86 | return status.Errorf(codes.Unimplemented, "method SayHelloStreaming not implemented")
87 | }
88 | func (UnimplementedStreamingGreeterServer) mustEmbedUnimplementedStreamingGreeterServer() {}
89 |
90 | // UnsafeStreamingGreeterServer may be embedded to opt out of forward compatibility for this service.
91 | // Use of this interface is not recommended, as added methods to StreamingGreeterServer will
92 | // result in compilation errors.
93 | type UnsafeStreamingGreeterServer interface {
94 | mustEmbedUnimplementedStreamingGreeterServer()
95 | }
96 |
97 | func RegisterStreamingGreeterServer(s grpc.ServiceRegistrar, srv StreamingGreeterServer) {
98 | s.RegisterService(&StreamingGreeter_ServiceDesc, srv)
99 | }
100 |
101 | func _StreamingGreeter_SayHelloStreaming_Handler(srv interface{}, stream grpc.ServerStream) error {
102 | return srv.(StreamingGreeterServer).SayHelloStreaming(&streamingGreeterSayHelloStreamingServer{stream})
103 | }
104 |
105 | type StreamingGreeter_SayHelloStreamingServer interface {
106 | Send(*HelloReply) error
107 | Recv() (*HelloRequest, error)
108 | grpc.ServerStream
109 | }
110 |
111 | type streamingGreeterSayHelloStreamingServer struct {
112 | grpc.ServerStream
113 | }
114 |
115 | func (x *streamingGreeterSayHelloStreamingServer) Send(m *HelloReply) error {
116 | return x.ServerStream.SendMsg(m)
117 | }
118 |
119 | func (x *streamingGreeterSayHelloStreamingServer) Recv() (*HelloRequest, error) {
120 | m := new(HelloRequest)
121 | if err := x.ServerStream.RecvMsg(m); err != nil {
122 | return nil, err
123 | }
124 | return m, nil
125 | }
126 |
127 | // StreamingGreeter_ServiceDesc is the grpc.ServiceDesc for StreamingGreeter service.
128 | // It's only intended for direct use with grpc.RegisterService,
129 | // and not to be introspected or modified (even as a copy)
130 | var StreamingGreeter_ServiceDesc = grpc.ServiceDesc{
131 | ServiceName: "hellostream.StreamingGreeter",
132 | HandlerType: (*StreamingGreeterServer)(nil),
133 | Methods: []grpc.MethodDesc{},
134 | Streams: []grpc.StreamDesc{
135 | {
136 | StreamName: "SayHelloStreaming",
137 | Handler: _StreamingGreeter_SayHelloStreaming_Handler,
138 | ServerStreams: true,
139 | ClientStreams: true,
140 | },
141 | },
142 | Metadata: "api/hellostream.proto",
143 | }
144 |
--------------------------------------------------------------------------------
/gen.go:
--------------------------------------------------------------------------------
1 | package larking_io
2 |
3 | //go:generate sh gen.sh
4 |
--------------------------------------------------------------------------------
/gen.sh:
--------------------------------------------------------------------------------
1 | protoc -I ~/src/github.com/googleapis/api-common-protos/ -I.. \
2 | --go_out=module=larking.io:. \
3 | --go-grpc_out=module=larking.io:. \
4 | ../larking/api/*.proto
5 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module larking.io
2 |
3 | go 1.22.7
4 |
5 | toolchain go1.22.9
6 |
7 | require (
8 | github.com/gobwas/ws v1.2.0
9 | github.com/google/go-cmp v0.6.0
10 | golang.org/x/net v0.29.0
11 | golang.org/x/sync v0.8.0
12 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1
13 | google.golang.org/grpc v1.68.0
14 | google.golang.org/protobuf v1.34.2
15 | )
16 |
17 | require (
18 | github.com/gobwas/httphead v0.1.0 // indirect
19 | github.com/gobwas/pool v0.2.1 // indirect
20 | golang.org/x/sys v0.25.0 // indirect
21 | golang.org/x/text v0.18.0 // indirect
22 | )
23 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
2 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
3 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
4 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
5 | github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I=
6 | github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
7 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
8 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
11 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
12 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
13 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
14 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
15 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
16 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
17 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
18 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
19 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
20 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
21 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
22 | google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
23 | google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
24 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
25 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
26 |
--------------------------------------------------------------------------------
/health/health.go:
--------------------------------------------------------------------------------
1 | // package health provides gRPC health server HTTP annotations.
2 | package health
3 |
4 | import (
5 | "google.golang.org/genproto/googleapis/api/annotations"
6 | "google.golang.org/genproto/googleapis/api/serviceconfig"
7 | "google.golang.org/grpc/health"
8 | "google.golang.org/protobuf/proto"
9 | )
10 |
11 | // AddHealthz adds a /v1/healthz endpoint to the service binding to the
12 | // grpc.health.v1.Health service:
13 | // - get /v1/healthz -> grpc.health.v1.Health.Check
14 | // - websocket /v1/healthz -> grpc.health.v1.Health.Watch
15 | func AddHealthz(dst *serviceconfig.Service) {
16 | src := &serviceconfig.Service{
17 | Http: &annotations.Http{Rules: []*annotations.HttpRule{{
18 | Selector: "grpc.health.v1.Health.Check",
19 | Pattern: &annotations.HttpRule_Get{
20 | // Get is a HTTP GET.
21 | Get: "/v1/healthz",
22 | },
23 | }, {
24 | Selector: "grpc.health.v1.Health.Watch",
25 | Pattern: &annotations.HttpRule_Custom{
26 | Custom: &annotations.CustomHttpPattern{
27 | Kind: "WEBSOCKET",
28 | Path: "/v1/healthz",
29 | },
30 | },
31 | }}},
32 | }
33 | proto.Merge(dst, src)
34 | }
35 |
36 | // NewServer returns a new grpc.Health server.
37 | func NewServer() *health.Server { return health.NewServer() }
38 |
--------------------------------------------------------------------------------
/larking/bench.txt:
--------------------------------------------------------------------------------
1 | goos: darwin
2 | goarch: amd64
3 | pkg: larking.io/larking
4 | cpu: VirtualApple @ 2.50GHz
5 | BenchmarkLexer-8 5162886 232.7 ns/op 0 B/op 0 allocs/op
6 | PASS
7 | ok larking.io/larking 1.953s
8 |
--------------------------------------------------------------------------------
/larking/code.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "net/http"
9 |
10 | "github.com/gobwas/ws"
11 | "google.golang.org/grpc/codes"
12 | )
13 |
14 | var codeToHTTPStatus = [...]int{
15 | http.StatusOK, // 0
16 | http.StatusRequestTimeout, // 1
17 | http.StatusInternalServerError, // 2
18 | http.StatusBadRequest, // 3
19 | http.StatusGatewayTimeout, // 4
20 | http.StatusNotFound, // 5
21 | http.StatusConflict, // 6
22 | http.StatusForbidden, // 7
23 | http.StatusTooManyRequests, // 8
24 | http.StatusBadRequest, // 9
25 | http.StatusConflict, // 10
26 | http.StatusBadRequest, // 11
27 | http.StatusNotImplemented, // 12
28 | http.StatusInternalServerError, // 13
29 | http.StatusServiceUnavailable, // 14
30 | http.StatusInternalServerError, // 15
31 | http.StatusUnauthorized, // 16
32 | }
33 |
34 | func HTTPStatusCode(c codes.Code) int {
35 | if int(c) > len(codeToHTTPStatus) {
36 | return http.StatusInternalServerError
37 | }
38 | return codeToHTTPStatus[c]
39 | }
40 |
41 | // TODO: validate error codes.
42 | var codeToWSStatus = [...]ws.StatusCode{
43 | ws.StatusNormalClosure, // 0
44 | ws.StatusGoingAway, // 1
45 | ws.StatusInternalServerError, // 2
46 | ws.StatusUnsupportedData, // 3
47 | ws.StatusGoingAway, // 4
48 | ws.StatusInternalServerError, // 5
49 | ws.StatusGoingAway, // 6
50 | ws.StatusInternalServerError, // 7
51 | ws.StatusInternalServerError, // 8
52 | ws.StatusInternalServerError, // 9
53 | ws.StatusInternalServerError, // 10
54 | ws.StatusInternalServerError, // 11
55 | ws.StatusUnsupportedData, // 12
56 | ws.StatusInternalServerError, // 13
57 | ws.StatusInternalServerError, // 14
58 | ws.StatusInternalServerError, // 15
59 | ws.StatusPolicyViolation, // 16
60 | }
61 |
62 | func WSStatusCode(c codes.Code) ws.StatusCode {
63 | if int(c) > len(codeToHTTPStatus) {
64 | return ws.StatusInternalServerError
65 | }
66 | return codeToWSStatus[c]
67 | }
68 |
--------------------------------------------------------------------------------
/larking/codec.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "encoding/binary"
9 | "fmt"
10 | "io"
11 | "sync"
12 |
13 | "google.golang.org/grpc/encoding"
14 | "google.golang.org/protobuf/encoding/protodelim"
15 | "google.golang.org/protobuf/encoding/protojson"
16 | "google.golang.org/protobuf/encoding/protowire"
17 | "google.golang.org/protobuf/proto"
18 | )
19 |
20 | var bytesPool = sync.Pool{
21 | New: func() interface{} {
22 | b := make([]byte, 0, 64)
23 | return &b
24 | },
25 | }
26 |
27 | // growcap scales up the capacity of a slice.
28 | // Taken from the Go 1.14 runtime and proto package.
29 | func growcap(oldcap, wantcap int) (newcap int) {
30 | if wantcap > oldcap*2 {
31 | newcap = wantcap
32 | } else if oldcap < 1024 {
33 | // The Go 1.14 runtime takes this case when len(s) < 1024,
34 | // not when cap(s) < 1024. The difference doesn't seem
35 | // significant here.
36 | newcap = oldcap * 2
37 | } else {
38 | newcap = oldcap
39 | for 0 < newcap && newcap < wantcap {
40 | newcap += newcap / 4
41 | }
42 | if newcap <= 0 {
43 | newcap = wantcap
44 | }
45 | }
46 | return newcap
47 | }
48 |
49 | // Codec defines the interface used to encode and decode messages.
50 | type Codec interface {
51 | encoding.Codec
52 | // MarshalAppend appends the marshaled form of v to b and returns the result.
53 | MarshalAppend([]byte, interface{}) ([]byte, error)
54 | }
55 |
56 | // StreamCodec is used in streaming RPCs where the message boundaries are
57 | // determined by the codec.
58 | type StreamCodec interface {
59 | Codec
60 |
61 | // ReadNext returns the size of the next message appended to buf.
62 | // ReadNext reads from r until either it has read a complete message or
63 | // encountered an error and returns all the data read from r.
64 | // The message is contained in dst[:n].
65 | // Excess data read from r is stored in dst[n:].
66 | ReadNext(buf []byte, r io.Reader, limit int) (dst []byte, n int, err error)
67 | // WriteNext writes the message to w with a size aware encoding
68 | // returning the number of bytes written.
69 | WriteNext(w io.Writer, src []byte) (n int, err error)
70 | }
71 |
72 | func errInvalidType(v any) error {
73 | return fmt.Errorf("marshal invalid type %T", v)
74 | }
75 |
76 | // CodecProto is a Codec implementation with protobuf binary format.
77 | type CodecProto struct {
78 | proto.MarshalOptions
79 | }
80 |
81 | func (c CodecProto) Marshal(v interface{}) ([]byte, error) {
82 | m, ok := v.(proto.Message)
83 | if !ok {
84 | return nil, errInvalidType(v)
85 | }
86 | return c.MarshalOptions.Marshal(m)
87 | }
88 |
89 | func (c CodecProto) MarshalAppend(b []byte, v interface{}) ([]byte, error) {
90 | m, ok := v.(proto.Message)
91 | if !ok {
92 | return nil, errInvalidType(v)
93 | }
94 | return c.MarshalOptions.MarshalAppend(b, m)
95 | }
96 |
97 | func (CodecProto) Unmarshal(data []byte, v interface{}) error {
98 | m, ok := v.(proto.Message)
99 | if !ok {
100 | return errInvalidType(v)
101 | }
102 | return proto.Unmarshal(data, m)
103 | }
104 |
105 | // ReadNext reads a varint size-delimited wire-format message from r.
106 | func (c CodecProto) ReadNext(b []byte, r io.Reader, limit int) ([]byte, int, error) {
107 | for i := 0; i < binary.MaxVarintLen64; i++ {
108 | for i >= len(b) {
109 | if len(b) == cap(b) {
110 | // Add more capacity (let append pick how much).
111 | b = append(b, 0)[:len(b)]
112 | }
113 | n, err := r.Read(b[len(b):cap(b)])
114 | b = b[:len(b)+n]
115 | if err != nil {
116 | return b, 0, err
117 | }
118 | }
119 | if b[i] < 0x80 {
120 | break
121 | }
122 | }
123 |
124 | size, n := protowire.ConsumeVarint(b)
125 | if n < 0 {
126 | return b, 0, protowire.ParseError(n)
127 | }
128 | if limit > 0 && int(size) > limit {
129 | return b, 0, &protodelim.SizeTooLargeError{Size: size, MaxSize: uint64(limit)}
130 | }
131 | b = b[n:] // consume the varint
132 | n = int(size)
133 |
134 | if len(b) < n {
135 | if cap(b) < n {
136 | dst := make([]byte, len(b), growcap(cap(b), n))
137 | copy(dst, b)
138 | b = dst
139 | }
140 | if _, err := io.ReadFull(r, b[len(b):n]); err != nil {
141 | if err == io.EOF {
142 | return b, 0, io.ErrUnexpectedEOF
143 | }
144 | return b, 0, err
145 | }
146 | b = b[:n]
147 | }
148 | return b, n, nil
149 | }
150 |
151 | // WriteNext writes the length of the message encoded as 4 byte unsigned integer
152 | // and then writes the message to w.
153 | func (c CodecProto) WriteNext(w io.Writer, b []byte) (int, error) {
154 | var sizeArr [binary.MaxVarintLen64]byte
155 | sizeBuf := protowire.AppendVarint(sizeArr[:0], uint64(len(b)))
156 | if _, err := w.Write(sizeBuf); err != nil {
157 | return 0, err
158 | }
159 | return w.Write(b)
160 | }
161 |
162 | // Name == "proto" overwritting internal proto codec
163 | func (CodecProto) Name() string { return "proto" }
164 |
165 | // CodecJSON is a Codec implementation with protobuf json format.
166 | type CodecJSON struct {
167 | protojson.MarshalOptions
168 | protojson.UnmarshalOptions
169 | }
170 |
171 | func (c CodecJSON) Marshal(v interface{}) ([]byte, error) {
172 | m, ok := v.(proto.Message)
173 | if !ok {
174 | return nil, errInvalidType(v)
175 | }
176 | return c.MarshalOptions.Marshal(m)
177 | }
178 |
179 | func (c CodecJSON) MarshalAppend(b []byte, v interface{}) ([]byte, error) {
180 | m, ok := v.(proto.Message)
181 | if !ok {
182 | return nil, errInvalidType(v)
183 | }
184 | return c.MarshalOptions.MarshalAppend(b, m)
185 | }
186 |
187 | func (c CodecJSON) Unmarshal(data []byte, v interface{}) error {
188 | m, ok := v.(proto.Message)
189 | if !ok {
190 | return errInvalidType(v)
191 | }
192 | return c.UnmarshalOptions.Unmarshal(data, m)
193 | }
194 |
195 | // ReadNext reads the length of the message around the json object.
196 | // It reads until it finds a matching number of braces.
197 | // It does not validate the JSON.
198 | func (c CodecJSON) ReadNext(b []byte, r io.Reader, limit int) ([]byte, int, error) {
199 | var (
200 | braceCount int
201 | isString bool
202 | isEscaped bool
203 | )
204 | for i := 0; i < int(limit); i++ {
205 | for i >= len(b) {
206 | if len(b) == cap(b) {
207 | // Add more capacity (let append pick how much).
208 | b = append(b, 0)[:len(b)]
209 | }
210 | n, err := r.Read(b[len(b):cap(b)])
211 | b = b[:len(b)+n]
212 | if err != nil {
213 | return b, 0, err
214 | }
215 | }
216 |
217 | switch {
218 | case isEscaped:
219 | isEscaped = false
220 | case isString:
221 | switch b[i] {
222 | case '\\':
223 | isEscaped = true
224 | case '"':
225 | isString = false
226 | }
227 | default:
228 | switch b[i] {
229 | case '{':
230 | braceCount++
231 | case '}':
232 | braceCount--
233 | if braceCount == 0 {
234 | return b, i + 1, nil
235 | }
236 | if braceCount < 0 {
237 | return b, 0, fmt.Errorf("unbalanced braces")
238 | }
239 | case '"':
240 | isString = true
241 | }
242 | }
243 | }
244 | return b, 0, &protodelim.SizeTooLargeError{Size: uint64(len(b)), MaxSize: uint64(limit)}
245 | }
246 |
247 | // WriteNext writes the raw JSON message to w without any size prefix.
248 | func (c CodecJSON) WriteNext(w io.Writer, b []byte) (int, error) {
249 | return w.Write(b)
250 | }
251 |
252 | func (CodecJSON) Name() string { return "json" }
253 |
254 | type codecHTTPBody struct{}
255 |
256 | func (codecHTTPBody) Marshal(v interface{}) ([]byte, error) {
257 | panic("not implemented")
258 | }
259 |
260 | func (codecHTTPBody) MarshalAppend(b []byte, v interface{}) ([]byte, error) {
261 | panic("not implemented")
262 | }
263 |
264 | func (codecHTTPBody) Unmarshal(data []byte, v interface{}) error {
265 | panic("not implemented")
266 | }
267 |
268 | func (codecHTTPBody) Name() string { return "body" }
269 |
270 | func (codecHTTPBody) ReadNext(b []byte, r io.Reader, limit int) ([]byte, int, error) {
271 | var total int
272 | for {
273 | if len(b) == cap(b) {
274 | // Add more capacity (let append pick how much).
275 | b = append(b, 0)[:len(b)]
276 | }
277 | n, err := r.Read(b[len(b):cap(b)])
278 | b = b[:len(b)+n]
279 | total += int(n)
280 | if total > limit {
281 | total = limit
282 | }
283 | if err != nil || total == limit {
284 | return b, total, err
285 | }
286 | }
287 | }
288 |
289 | func (codecHTTPBody) WriteNext(w io.Writer, b []byte) (int, error) {
290 | return w.Write(b)
291 | }
292 |
--------------------------------------------------------------------------------
/larking/codec_test.go:
--------------------------------------------------------------------------------
1 | package larking
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "testing"
7 |
8 | "google.golang.org/protobuf/encoding/protowire"
9 | "google.golang.org/protobuf/proto"
10 | "larking.io/api/testpb"
11 | )
12 |
13 | func TestStreamCodecs(t *testing.T) {
14 |
15 | protob, err := proto.Marshal(&testpb.Message{
16 | Text: "hello, protobuf",
17 | })
18 | if err != nil {
19 | t.Fatal(err)
20 | }
21 | protobig, err := proto.Marshal(&testpb.Message{
22 | Text: string(bytes.Repeat([]byte{'a'}, 1<<20)),
23 | })
24 | if err != nil {
25 | t.Fatal(err)
26 | }
27 | jsonb, err := (&CodecJSON{}).Marshal(&testpb.Message{
28 | Text: "hello, json",
29 | })
30 | if err != nil {
31 | t.Fatal(err)
32 | }
33 | jsonescape := []byte(`{"text":"hello, json} \" }}"}`)
34 |
35 | tests := []struct {
36 | name string
37 | codec Codec
38 | input []byte
39 | extra []byte
40 | want []byte
41 | wantErr error
42 | }{{
43 | name: "proto buffered",
44 | codec: CodecProto{},
45 | input: func() []byte {
46 | b := protowire.AppendVarint(nil, uint64(len(protob)))
47 | t.Log("len(b)=", len(b))
48 | return append(b, protob...)
49 | }(),
50 | want: protob,
51 | }, {
52 | name: "proto unbuffered",
53 | codec: CodecProto{},
54 | input: make([]byte, 0, 4+len(protob)),
55 | extra: func() []byte {
56 | b := protowire.AppendVarint(nil, uint64(len(protob)))
57 | return append(b, protob...)
58 | }(),
59 | want: protob,
60 | }, {
61 | name: "proto partial size",
62 | codec: CodecProto{},
63 | input: func() []byte {
64 | b := protowire.AppendVarint(nil, uint64(len(protob)))
65 | return append(b, protob...)[:1]
66 | }(),
67 | extra: func() []byte {
68 | b := protowire.AppendVarint(nil, uint64(len(protob)))
69 | return append(b, protob...)[1:]
70 | }(),
71 | want: protob,
72 | }, {
73 | name: "proto partial message",
74 | codec: CodecProto{},
75 | input: func() []byte {
76 | b := protowire.AppendVarint(nil, uint64(len(protob)))
77 | return append(b, protob...)[:6]
78 | }(),
79 | extra: func() []byte {
80 | b := protowire.AppendVarint(nil, uint64(len(protob)))
81 | return append(b, protob...)[6:]
82 | }(),
83 | want: protob,
84 | }, {
85 | name: "proto zero size",
86 | codec: CodecProto{},
87 | extra: func() []byte {
88 | b := protowire.AppendVarint(nil, 0)
89 | return b
90 | }(),
91 | want: []byte{},
92 | }, {
93 | name: "proto big size",
94 | codec: CodecProto{},
95 | extra: func() []byte {
96 | b := protowire.AppendVarint(nil, uint64(len(protobig)))
97 | return append(b, protobig...)
98 | }(),
99 | want: protobig,
100 | }, {
101 | name: "json buffered",
102 | codec: CodecJSON{},
103 | input: jsonb,
104 | want: jsonb,
105 | }, {
106 | name: "json unbuffered",
107 | codec: CodecJSON{},
108 | input: make([]byte, 0, 4+len(jsonb)),
109 | extra: jsonb,
110 | want: jsonb,
111 | }, {
112 | name: "json partial object",
113 | codec: CodecJSON{},
114 | input: jsonb[:2],
115 | extra: jsonb[2:],
116 | want: jsonb,
117 | }, {
118 | name: "json escape",
119 | codec: CodecJSON{},
120 | input: jsonescape,
121 | want: jsonescape,
122 | }}
123 |
124 | for _, tt := range tests {
125 | t.Run(tt.name, func(t *testing.T) {
126 | codec := tt.codec.(StreamCodec)
127 |
128 | r := bytes.NewReader(tt.extra)
129 | b, n, err := codec.ReadNext(tt.input, r, len(tt.want))
130 | if err != nil {
131 | if tt.wantErr != nil {
132 | if !errors.Is(err, tt.wantErr) {
133 | t.Fatalf("got %v, want %v", err, tt.wantErr)
134 | }
135 | return
136 | }
137 | t.Fatal(err)
138 | }
139 | if n > len(b) {
140 | t.Fatalf("n %v > %v", n, len(b))
141 | }
142 |
143 | got := b[:n]
144 | if !bytes.Equal(got, tt.want) {
145 | t.Errorf("got %s, want %s", got, tt.want)
146 | }
147 |
148 | var msg testpb.Message
149 | if err := codec.Unmarshal(got, &msg); err != nil {
150 | t.Error(err)
151 | }
152 |
153 | b, err = codec.MarshalAppend(b[:0], &msg)
154 | if err != nil {
155 | t.Fatal(err)
156 | }
157 |
158 | var buf bytes.Buffer
159 | if _, err := codec.WriteNext(&buf, b); err != nil {
160 | t.Fatal(err)
161 | }
162 |
163 | onwire := append([]byte(nil), tt.input...)
164 | onwire = append(onwire, tt.extra...)
165 | if !bytes.Equal(buf.Bytes(), onwire) {
166 | t.Errorf("onwire got %v, want %v", buf.Bytes(), onwire)
167 | }
168 | })
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/larking/compress.go:
--------------------------------------------------------------------------------
1 | package larking
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "io"
7 | "sync"
8 |
9 | "google.golang.org/grpc/encoding"
10 | )
11 |
12 | var bufPool = sync.Pool{
13 | New: func() interface{} {
14 | return &bytes.Buffer{}
15 | },
16 | }
17 |
18 | // Compressor is used to compress and decompress messages.
19 | // Based on grpc/encoding.
20 | type Compressor interface {
21 | encoding.Compressor
22 | }
23 |
24 | // CompressorGzip implements the Compressor interface.
25 | // Based on grpc/encoding/gzip.
26 | type CompressorGzip struct {
27 | Level *int
28 | poolCompressor sync.Pool
29 | poolDecompressor sync.Pool
30 | }
31 |
32 | // Name returns gzip.
33 | func (*CompressorGzip) Name() string { return "gzip" }
34 |
35 | type gzipWriter struct {
36 | *gzip.Writer
37 | pool *sync.Pool
38 | }
39 |
40 | // Compress implements the Compressor interface.
41 | func (c *CompressorGzip) Compress(w io.Writer) (io.WriteCloser, error) {
42 | z, ok := c.poolCompressor.Get().(*gzipWriter)
43 | if !ok {
44 | level := gzip.DefaultCompression
45 | if c.Level != nil {
46 | level = *c.Level
47 | }
48 | newZ, err := gzip.NewWriterLevel(w, level)
49 | if err != nil {
50 | return nil, err
51 | }
52 | return &gzipWriter{Writer: newZ, pool: &c.poolCompressor}, nil
53 | }
54 | z.Reset(w)
55 | return z, nil
56 | }
57 |
58 | func (z *gzipWriter) Close() error {
59 | defer z.pool.Put(z)
60 | return z.Writer.Close()
61 | }
62 |
63 | type gzipReader struct {
64 | *gzip.Reader
65 | pool *sync.Pool
66 | }
67 |
68 | // Decompress implements the Compressor interface.
69 | func (c *CompressorGzip) Decompress(r io.Reader) (io.Reader, error) {
70 | z, ok := c.poolDecompressor.Get().(*gzipReader)
71 | if !ok {
72 | newZ, err := gzip.NewReader(r)
73 | if err != nil {
74 | return nil, err
75 | }
76 | return &gzipReader{Reader: newZ, pool: &c.poolDecompressor}, nil
77 | }
78 | if err := z.Reset(r); err != nil {
79 | z.pool.Put(z)
80 | return nil, err
81 | }
82 | return z, nil
83 | }
84 |
85 | func (z *gzipReader) Read(p []byte) (n int, err error) {
86 | n, err = z.Reader.Read(p)
87 | if err == io.EOF {
88 | z.pool.Put(z)
89 | }
90 | return n, err
91 | }
92 |
--------------------------------------------------------------------------------
/larking/grpc_test.go:
--------------------------------------------------------------------------------
1 | package larking
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "net/http"
8 | "os"
9 | "testing"
10 | "time"
11 |
12 | "github.com/google/go-cmp/cmp"
13 | "golang.org/x/net/http2"
14 | "golang.org/x/net/http2/h2c"
15 | "google.golang.org/grpc"
16 | "google.golang.org/grpc/credentials/insecure"
17 | "google.golang.org/grpc/encoding"
18 | grpc_testing "google.golang.org/grpc/interop/grpc_testing"
19 | "google.golang.org/grpc/metadata"
20 | "google.golang.org/protobuf/testing/protocmp"
21 | )
22 |
23 | func TestGRPC(t *testing.T) {
24 | // Create test server.
25 | ts := grpc_testing.UnimplementedTestServiceServer{}
26 |
27 | o := new(overrides)
28 | m, err := NewMux(
29 | UnaryServerInterceptorOption(o.unary()),
30 | StreamServerInterceptorOption(o.stream()),
31 | )
32 | if err != nil {
33 | t.Fatalf("failed to create mux: %v", err)
34 | }
35 | grpc_testing.RegisterTestServiceServer(m, ts)
36 |
37 | index := http.HandlerFunc(m.serveGRPC)
38 |
39 | h2s := &http2.Server{}
40 | hs := &http.Server{
41 | ReadTimeout: 10 * time.Second,
42 | WriteTimeout: 10 * time.Second,
43 | MaxHeaderBytes: 1 << 20, // 1 MB
44 | Handler: h2c.NewHandler(index, h2s),
45 | }
46 | if err := http2.ConfigureServer(hs, h2s); err != nil {
47 | t.Fatalf("failed to configure server: %v", err)
48 | }
49 |
50 | lis, err := net.Listen("tcp", "localhost:0")
51 | if err != nil {
52 | t.Fatalf("failed to listen: %v", err)
53 | }
54 | defer lis.Close()
55 |
56 | // Start server.
57 | go func() {
58 | if err := hs.Serve(lis); err != nil && err != http.ErrServerClosed {
59 | fmt.Println(err)
60 | os.Exit(1)
61 | }
62 | }()
63 | defer hs.Close()
64 |
65 | encoding.RegisterCompressor(&CompressorGzip{})
66 |
67 | conns := []struct {
68 | name string
69 | opts []grpc.DialOption
70 | }{{
71 | name: "insecure",
72 | opts: []grpc.DialOption{
73 | grpc.WithTransportCredentials(insecure.NewCredentials()),
74 | grpc.WithBlock(),
75 | },
76 | }, {
77 | name: "compressed",
78 | opts: []grpc.DialOption{
79 | grpc.WithTransportCredentials(insecure.NewCredentials()),
80 | grpc.WithBlock(),
81 | grpc.WithDefaultCallOptions(grpc.UseCompressor("gzip")),
82 | },
83 | }}
84 |
85 | // https://github.com/grpc/grpc/blob/master/src/proto/grpc/testing/test.proto
86 | tests := []struct {
87 | name string
88 | method string
89 | desc grpc.StreamDesc
90 | inouts []any
91 | }{{
92 | name: "unary",
93 | method: "/grpc.testing.TestService/UnaryCall",
94 | desc: grpc.StreamDesc{},
95 | inouts: []any{
96 | in{
97 | msg: &grpc_testing.SimpleRequest{
98 | Payload: &grpc_testing.Payload{Body: []byte{0}},
99 | },
100 | },
101 | out{
102 | msg: &grpc_testing.SimpleResponse{
103 | Payload: &grpc_testing.Payload{Body: []byte{0}},
104 | },
105 | },
106 | },
107 | }, {
108 | name: "client_streaming",
109 | method: "/grpc.testing.TestService/StreamingInputCall",
110 | desc: grpc.StreamDesc{
111 | ClientStreams: true,
112 | },
113 | inouts: []any{
114 | in{
115 | msg: &grpc_testing.StreamingInputCallRequest{
116 | Payload: &grpc_testing.Payload{Body: []byte{0}},
117 | },
118 | },
119 | in{
120 | msg: &grpc_testing.StreamingInputCallRequest{
121 | Payload: &grpc_testing.Payload{Body: []byte{0}},
122 | },
123 | },
124 | out{
125 | msg: &grpc_testing.StreamingInputCallResponse{
126 | AggregatedPayloadSize: 2,
127 | },
128 | },
129 | },
130 | }, {
131 | name: "server_streaming",
132 | method: "/grpc.testing.TestService/StreamingOutputCall",
133 | desc: grpc.StreamDesc{
134 | ServerStreams: true,
135 | },
136 | inouts: []any{
137 | in{
138 | msg: &grpc_testing.StreamingOutputCallRequest{
139 | Payload: &grpc_testing.Payload{Body: []byte{0}},
140 | },
141 | },
142 | out{
143 | msg: &grpc_testing.StreamingOutputCallResponse{
144 | Payload: &grpc_testing.Payload{Body: []byte{0}},
145 | },
146 | },
147 | out{
148 | msg: &grpc_testing.StreamingOutputCallResponse{
149 | Payload: &grpc_testing.Payload{Body: []byte{0}},
150 | },
151 | },
152 | },
153 | }, {
154 | name: "full_streaming",
155 | method: "/grpc.testing.TestService/FullDuplexCall",
156 | desc: grpc.StreamDesc{
157 | ClientStreams: true,
158 | ServerStreams: true,
159 | },
160 | inouts: []any{
161 | in{
162 | msg: &grpc_testing.StreamingOutputCallRequest{
163 | Payload: &grpc_testing.Payload{Body: []byte{0}},
164 | },
165 | },
166 | out{
167 | msg: &grpc_testing.StreamingOutputCallResponse{
168 | Payload: &grpc_testing.Payload{Body: []byte{0}},
169 | },
170 | },
171 | in{
172 | msg: &grpc_testing.StreamingOutputCallRequest{
173 | Payload: &grpc_testing.Payload{Body: []byte{0}},
174 | },
175 | },
176 | out{
177 | msg: &grpc_testing.StreamingOutputCallResponse{
178 | Payload: &grpc_testing.Payload{Body: []byte{0}},
179 | },
180 | },
181 | },
182 | }, {
183 | name: "half_streaming",
184 | method: "/grpc.testing.TestService/HalfDuplexCall",
185 | desc: grpc.StreamDesc{
186 | ClientStreams: true,
187 | ServerStreams: true,
188 | },
189 | inouts: []any{
190 | in{
191 | msg: &grpc_testing.StreamingOutputCallRequest{
192 | Payload: &grpc_testing.Payload{Body: []byte{0}},
193 | },
194 | },
195 | in{
196 | msg: &grpc_testing.StreamingOutputCallRequest{
197 | Payload: &grpc_testing.Payload{Body: []byte{0}},
198 | },
199 | },
200 | out{
201 | msg: &grpc_testing.StreamingOutputCallResponse{
202 | Payload: &grpc_testing.Payload{Body: []byte{0}},
203 | },
204 | },
205 | out{
206 | msg: &grpc_testing.StreamingOutputCallResponse{
207 | Payload: &grpc_testing.Payload{Body: []byte{0}},
208 | },
209 | },
210 | },
211 | }, {
212 | name: "large_client_streaming",
213 | method: "/grpc.testing.TestService/StreamingInputCall",
214 | desc: grpc.StreamDesc{
215 | ClientStreams: true,
216 | },
217 | inouts: []any{
218 | in{
219 | msg: &grpc_testing.StreamingInputCallRequest{
220 | Payload: &grpc_testing.Payload{Body: make([]byte, 1024)},
221 | },
222 | },
223 | in{
224 | msg: &grpc_testing.StreamingInputCallRequest{
225 | Payload: &grpc_testing.Payload{Body: make([]byte, 1024)},
226 | },
227 | },
228 | out{
229 | msg: &grpc_testing.StreamingInputCallResponse{
230 | AggregatedPayloadSize: 2,
231 | },
232 | },
233 | },
234 | }}
235 |
236 | opts := cmp.Options{protocmp.Transform()}
237 | for _, tc := range conns {
238 | t.Run(tc.name, func(t *testing.T) {
239 | conn, err := grpc.Dial(lis.Addr().String(), tc.opts...)
240 | if err != nil {
241 | t.Fatalf("failed to dial: %v", err)
242 | }
243 | defer conn.Close()
244 |
245 | for _, tt := range tests {
246 | t.Run(tt.name, func(t *testing.T) {
247 | o.reset(t, "test", tt.inouts)
248 |
249 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
250 | defer cancel()
251 |
252 | ctx = metadata.AppendToOutgoingContext(ctx, "test", tt.method)
253 |
254 | stream, err := conn.NewStream(ctx, &tt.desc, tt.method)
255 | if err != nil {
256 | t.Fatalf("failed to create stream: %v", err)
257 | }
258 |
259 | for i, inout := range tt.inouts {
260 |
261 | switch v := inout.(type) {
262 | case in:
263 | t.Logf("stream.SendMsg: %d", i)
264 | if err := stream.SendMsg(v.msg); err != nil {
265 | t.Fatalf("failed to send msg: %v", err)
266 | }
267 | case out:
268 | t.Logf("stream.RecvMsg: %d", i)
269 | want := v.msg
270 | got := v.msg.ProtoReflect().New().Interface()
271 | if err := stream.RecvMsg(got); err != nil {
272 | t.Fatalf("failed to recv msg: %v", err)
273 | }
274 | diff := cmp.Diff(got, want, opts...)
275 | if diff != "" {
276 | t.Error(diff)
277 | }
278 | }
279 | }
280 | })
281 | }
282 | })
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/larking/handler.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "fmt"
9 | "log"
10 | "reflect"
11 |
12 | "google.golang.org/grpc"
13 | "google.golang.org/grpc/metadata"
14 | "google.golang.org/protobuf/reflect/protoreflect"
15 | )
16 |
17 | type handlerFunc func(*muxOptions, grpc.ServerStream) error
18 |
19 | type handler struct {
20 | desc protoreflect.MethodDescriptor
21 | handler handlerFunc
22 | method string // /Service/Method
23 | }
24 |
25 | // TODO: use grpclog?
26 | //var logger = grpclog.Component("core")
27 |
28 | // RegisterService satisfies grpc.ServiceRegistrar for generated service code hooks.
29 | func (m *Mux) RegisterService(sd *grpc.ServiceDesc, ss interface{}) {
30 | if ss != nil {
31 | ht := reflect.TypeOf(sd.HandlerType).Elem()
32 | st := reflect.TypeOf(ss)
33 | if !st.Implements(ht) {
34 | log.Fatalf("larking: RegisterService found the handler of type %v that does not satisfy %v", st, ht)
35 | }
36 | }
37 | if err := m.registerService(sd, ss); err != nil {
38 | log.Fatalf("larking: RegisterService error: %v", err)
39 | }
40 | }
41 |
42 | func (m *Mux) registerService(gsd *grpc.ServiceDesc, ss interface{}) error {
43 |
44 | // Load the state for writing.
45 | m.mu.Lock()
46 | defer m.mu.Unlock()
47 | s := m.loadState().clone()
48 |
49 | d, err := m.opts.files.FindDescriptorByName(protoreflect.FullName(gsd.ServiceName))
50 | if err != nil {
51 | return err
52 | }
53 | sd, ok := d.(protoreflect.ServiceDescriptor)
54 | if !ok {
55 | return fmt.Errorf("invalid method descriptor %T", d)
56 | }
57 | mds := sd.Methods()
58 |
59 | findMethod := func(methodName string) (protoreflect.MethodDescriptor, error) {
60 | md := mds.ByName(protoreflect.Name(methodName))
61 | if md == nil {
62 | return nil, fmt.Errorf("missing method descriptor for %v", methodName)
63 | }
64 | return md, nil
65 | }
66 |
67 | for i := range gsd.Methods {
68 | d := &gsd.Methods[i]
69 | method := "/" + gsd.ServiceName + "/" + d.MethodName
70 |
71 | md, err := findMethod(d.MethodName)
72 | if err != nil {
73 | return err
74 | }
75 |
76 | h := &handler{
77 | method: method,
78 | desc: md,
79 | handler: func(opts *muxOptions, stream grpc.ServerStream) error {
80 | ctx := stream.Context()
81 |
82 | // TODO: opts?
83 | reply, err := d.Handler(ss, ctx, stream.RecvMsg, opts.unaryInterceptor)
84 | if err != nil {
85 | return err
86 | }
87 | return stream.SendMsg(reply)
88 | },
89 | }
90 |
91 | if err := s.appendHandler(m.opts, md, h); err != nil {
92 | return err
93 | }
94 | }
95 | for i := range gsd.Streams {
96 | d := &gsd.Streams[i]
97 | method := "/" + gsd.ServiceName + "/" + d.StreamName
98 | md, err := findMethod(d.StreamName)
99 | if err != nil {
100 | return err
101 | }
102 |
103 | h := &handler{
104 | method: method,
105 | desc: md,
106 | handler: func(opts *muxOptions, stream grpc.ServerStream) error {
107 | info := &grpc.StreamServerInfo{
108 | FullMethod: method,
109 | IsClientStream: d.ClientStreams,
110 | IsServerStream: d.ServerStreams,
111 | }
112 |
113 | return opts.stream(ss, stream, info, d.Handler)
114 | },
115 | }
116 | if err := s.appendHandler(m.opts, md, h); err != nil {
117 | return err
118 | }
119 | }
120 |
121 | m.storeState(s)
122 | return nil
123 | }
124 |
125 | var _ grpc.ServerTransportStream = (*serverTransportStream)(nil)
126 |
127 | // serverTransportStream wraps gprc.SeverStream to support header/trailers.
128 | type serverTransportStream struct {
129 | grpc.ServerStream
130 | method string
131 | }
132 |
133 | func (s *serverTransportStream) Method() string { return s.method }
134 | func (s *serverTransportStream) SetHeader(md metadata.MD) error {
135 | return s.ServerStream.SetHeader(md)
136 | }
137 | func (s *serverTransportStream) SendHeader(md metadata.MD) error {
138 | return s.ServerStream.SendHeader(md)
139 | }
140 | func (s *serverTransportStream) SetTrailer(md metadata.MD) error {
141 | s.ServerStream.SetTrailer(md)
142 | return nil
143 | }
144 |
--------------------------------------------------------------------------------
/larking/handler_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "context"
9 | "encoding/json"
10 | "fmt"
11 | "io"
12 | "net/http"
13 | "net/http/httptest"
14 | "strings"
15 | "testing"
16 |
17 | "github.com/google/go-cmp/cmp"
18 | "google.golang.org/grpc"
19 | "google.golang.org/grpc/metadata"
20 | "google.golang.org/protobuf/proto"
21 | "google.golang.org/protobuf/testing/protocmp"
22 | "larking.io/api/testpb"
23 | )
24 |
25 | func TestHandler(t *testing.T) {
26 | ms := &testpb.UnimplementedMessagingServer{}
27 |
28 | req := httptest.NewRequest(http.MethodPatch, "/v1/messages/msg_123", strings.NewReader(
29 | `{ "text": "Hi!" }`,
30 | ))
31 | req.Header.Set("Content-Type", "application/json")
32 |
33 | interceptor := func(
34 | ctx context.Context,
35 | req interface{},
36 | info *grpc.UnaryServerInfo,
37 | _ grpc.UnaryHandler,
38 | ) (interface{}, error) {
39 | _, ok := metadata.FromIncomingContext(ctx)
40 | if !ok {
41 | return nil, fmt.Errorf("missing context metadata")
42 | }
43 |
44 | if info.FullMethod != "/larking.testpb.Messaging/UpdateMessage" {
45 | return nil, fmt.Errorf("invalid method %s", info.FullMethod)
46 | }
47 |
48 | in := req.(proto.Message)
49 | want := &testpb.UpdateMessageRequestOne{
50 | MessageId: "msg_123",
51 | Message: &testpb.Message{
52 | Text: "Hi!",
53 | },
54 | }
55 |
56 | if !proto.Equal(want, in) {
57 | diff := cmp.Diff(in, want, protocmp.Transform())
58 | t.Fatal(diff)
59 | return nil, fmt.Errorf("unexpected message")
60 | }
61 | return &testpb.Message{Text: "hello, patch!"}, nil
62 | }
63 |
64 | m, err := NewMux(UnaryServerInterceptorOption(interceptor))
65 | if err != nil {
66 | t.Fatal(err)
67 | }
68 | testpb.RegisterMessagingServer(m, ms)
69 |
70 | w := httptest.NewRecorder()
71 |
72 | m.ServeHTTP(w, req)
73 | t.Log(w)
74 | r := w.Result()
75 | if r.StatusCode != 200 {
76 | t.Fatal(r.Status)
77 | }
78 |
79 | data, err := io.ReadAll(r.Body)
80 | if err != nil {
81 | t.Fatal(err)
82 | }
83 | t.Log(string(data))
84 |
85 | rsp := &testpb.Message{}
86 | if err := json.Unmarshal(data, rsp); err != nil {
87 | t.Fatal(err)
88 | }
89 | want := &testpb.Message{Text: "hello, patch!"}
90 |
91 | if !proto.Equal(want, rsp) {
92 | t.Fatal(cmp.Diff(rsp, want, protocmp.Transform()))
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/larking/http_test.go:
--------------------------------------------------------------------------------
1 | package larking
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "io"
7 | "math/rand"
8 | "net/http/httptest"
9 | "testing"
10 |
11 | "google.golang.org/genproto/googleapis/api/httpbody"
12 | "google.golang.org/grpc/codes"
13 | "google.golang.org/grpc/status"
14 | "larking.io/api/testpb"
15 | )
16 |
17 | type asHTTPBodyServer struct {
18 | testpb.UnimplementedFilesServer
19 | }
20 |
21 | func (s *asHTTPBodyServer) UploadDownload(ctx context.Context, req *testpb.UploadFileRequest) (*httpbody.HttpBody, error) {
22 | return &httpbody.HttpBody{
23 | ContentType: req.File.GetContentType(),
24 | Data: req.File.GetData(),
25 | }, nil
26 | }
27 |
28 | // LargeUploadDownload implements testpb.FilesServer
29 | // Echoes the request body as the response body.
30 | func (s *asHTTPBodyServer) LargeUploadDownload(stream testpb.Files_LargeUploadDownloadServer) error {
31 | var req testpb.UploadFileRequest
32 | r, err := AsHTTPBodyReader(stream, &req)
33 | if err != nil {
34 | return err
35 | }
36 | if req.File.Data != nil {
37 | return status.Error(codes.Internal, "unexpected data")
38 | }
39 | if req.File.ContentType != "image/jpeg" {
40 | return status.Error(codes.Internal, "unexpected content type")
41 | }
42 | if req.Filename != "cat.jpg" {
43 | return status.Error(codes.Internal, "unexpected filename")
44 | }
45 |
46 | rsp := &httpbody.HttpBody{
47 | ContentType: req.File.GetContentType(),
48 | }
49 | w, err := AsHTTPBodyWriter(stream, rsp)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | n, err := io.Copy(w, r)
55 | if err != nil {
56 | return status.Errorf(codes.Internal, "copy error: %d %v", n, err)
57 | }
58 | if n == 0 {
59 | return status.Error(codes.Internal, "zero bytes read")
60 | }
61 | return err
62 | }
63 |
64 | func TestAsHTTPBody(t *testing.T) {
65 | // Create test server.
66 | ts := &asHTTPBodyServer{}
67 |
68 | m, err := NewMux()
69 | if err != nil {
70 | t.Fatal(err)
71 | }
72 | testpb.RegisterFilesServer(m, ts)
73 |
74 | b := make([]byte, 1024*1024)
75 | if _, err := rand.Read(b); err != nil {
76 | t.Fatal(err)
77 | }
78 | body := bytes.NewReader(b)
79 | w := httptest.NewRecorder()
80 | r := httptest.NewRequest("POST", "/files/large/cat.jpg", body)
81 | r.Header.Set("Content-Type", "image/jpeg")
82 | m.ServeHTTP(w, r)
83 |
84 | if w.Code != 200 {
85 | t.Errorf("unexpected status: %d", w.Code)
86 | t.Log(w.Body.String())
87 | return
88 | }
89 | if w.Header().Get("Content-Type") != "image/jpeg" {
90 | t.Errorf("unexpected content type: %s", w.Header().Get("Content-Type"))
91 | }
92 | if !bytes.Equal(b, w.Body.Bytes()) {
93 | t.Errorf("bytes not equal: %d != %d", len(b), len(w.Body.Bytes()))
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/larking/lexer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "strings"
9 | "unicode"
10 | "unicode/utf8"
11 |
12 | "google.golang.org/grpc/codes"
13 | "google.golang.org/grpc/status"
14 | )
15 |
16 | // ### Path template syntax
17 | //
18 | // Template = "/" Segments [ Verb ] ;
19 | // Segments = Segment { "/" Segment } ;
20 | // Segment = "*" | "**" | LITERAL | Variable ;
21 | // Variable = "{" FieldPath [ "=" Segments ] "}" ;
22 | // FieldPath = IDENT { "." IDENT } ;
23 | // Verb = ":" LITERAL ;
24 |
25 | type tokenType uint16
26 |
27 | const (
28 | tokenError tokenType = 0
29 | tokenSlash tokenType = 1 << iota // /
30 | tokenStar // *
31 | tokenStarStar // **
32 | tokenVariableStart // {
33 | tokenVariableEnd // }
34 | tokenEqual // =
35 | tokenIdent // a-z A-Z 0-9 - _
36 | tokenLiteral // a-z A-Z 0-9 - _ .
37 | tokenDot // .
38 | tokenVerb // :
39 | tokenPath // a-z A-Z 0-9 - _ . ~ ! $ & ' ( ) * + , ; = @
40 | tokenEOF
41 | )
42 |
43 | type token struct {
44 | val string
45 | typ tokenType
46 | }
47 |
48 | type tokens []token
49 |
50 | func (toks tokens) String() string {
51 | var b strings.Builder
52 | for _, tok := range toks {
53 | b.WriteString(tok.val)
54 | }
55 | return b.String()
56 | }
57 |
58 | func (toks tokens) index(typ tokenType) int {
59 | for i, tok := range toks {
60 | if tok.typ == typ {
61 | return i
62 | }
63 | }
64 | return -1
65 | }
66 |
67 | func (toks tokens) indexAny(s tokenType) int {
68 | for i, tok := range toks {
69 | if s&tok.typ != 0 {
70 | return i
71 | }
72 | }
73 | return -1
74 | }
75 |
76 | type lexer struct {
77 | input string
78 | toks [64]token
79 | len int
80 | start int
81 | pos int
82 | width int
83 | }
84 |
85 | func (l *lexer) tokens() tokens { return l.toks[:l.len] }
86 |
87 | const eof = -1
88 |
89 | func (l *lexer) next() (r rune) {
90 | if l.pos >= len(l.input) {
91 | l.width = 0
92 | return eof
93 | }
94 | if c := l.input[l.pos]; c < utf8.RuneSelf {
95 | l.width = 1
96 | r = rune(c)
97 | } else {
98 | r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
99 | }
100 | l.pos += l.width
101 | return r
102 | }
103 |
104 | func (l *lexer) current() (r rune) {
105 | if l.width == 0 {
106 | return 0
107 | } else if l.pos > l.width {
108 | r, _ = utf8.DecodeRuneInString(l.input[l.pos-l.width:])
109 | } else {
110 | r, _ = utf8.DecodeRuneInString(l.input)
111 | }
112 | return r
113 | }
114 |
115 | func (l *lexer) backup() {
116 | l.pos -= l.width
117 | }
118 |
119 | func (l *lexer) acceptRun(isValid func(r rune) bool) int {
120 | var i int
121 | for isValid(l.next()) {
122 | i++
123 | }
124 | l.backup()
125 | return i
126 | }
127 |
128 | var errTokenLimit = status.Errorf(codes.InvalidArgument, "path: too many tokens")
129 |
130 | func (l *lexer) emit(typ tokenType) error {
131 | if l.len >= len(l.toks) {
132 | return errTokenLimit
133 | }
134 | tok := token{typ: typ, val: l.input[l.start:l.pos]}
135 |
136 | l.toks[l.len] = tok
137 | l.len++
138 | l.start = l.pos
139 | return nil
140 | }
141 |
142 | func (l *lexer) errUnexpected() error {
143 | if err := l.emit(tokenError); err != nil {
144 | return err
145 | }
146 | r := l.current()
147 | return status.Errorf(codes.InvalidArgument, "path: %v:%v unexpected rune %q", l.pos-l.width, l.pos, r)
148 | }
149 | func (l *lexer) errShort() error {
150 | if err := l.emit(tokenError); err != nil {
151 | return err
152 | }
153 | r := l.current()
154 | return status.Errorf(codes.InvalidArgument, "path: %v:%v short read %q", l.pos-l.width, l.pos, r)
155 | }
156 |
157 | func isIdent(r rune) bool {
158 | return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-'
159 | }
160 |
161 | func isLiteral(r rune) bool {
162 | return isIdent(r) || r == '.'
163 | }
164 |
165 | func isPath(r rune) bool {
166 | return isLiteral(r) || r == '~' || r == '!' || r == '$' || r == '&' ||
167 | r == '\'' || r == '(' || r == ')' || r == '*' || r == '+' ||
168 | r == ',' || r == ';' || r == '=' || r == '@'
169 | }
170 |
171 | func lexIdent(l *lexer) error {
172 | if i := l.acceptRun(isIdent); i == 0 {
173 | return l.errShort()
174 | }
175 | return l.emit(tokenIdent)
176 | }
177 |
178 | func lexLiteral(l *lexer) error {
179 | if i := l.acceptRun(isLiteral); i == 0 {
180 | return l.errShort()
181 | }
182 | return l.emit(tokenLiteral)
183 | }
184 |
185 | func lexFieldPath(l *lexer) error {
186 | if err := lexIdent(l); err != nil {
187 | return err
188 | }
189 | for {
190 | if r := l.next(); r != '.' {
191 | l.backup() // unknown
192 | return nil
193 | }
194 | if err := l.emit(tokenDot); err != nil {
195 | return err
196 | }
197 | if err := lexIdent(l); err != nil {
198 | return err
199 | }
200 | }
201 | }
202 |
203 | func lexVerb(l *lexer) error {
204 | if err := lexLiteral(l); err != nil {
205 | return err
206 | }
207 | if r := l.next(); r == eof {
208 | return l.emit(tokenEOF)
209 | }
210 | return l.errUnexpected()
211 | }
212 |
213 | func lexVariable(l *lexer) error {
214 | r := l.next()
215 | if r != '{' {
216 | return l.errUnexpected()
217 | }
218 | if err := l.emit(tokenVariableStart); err != nil {
219 | return err
220 | }
221 | if err := lexFieldPath(l); err != nil {
222 | return err
223 | }
224 |
225 | r = l.next()
226 | if r == '=' {
227 | if err := l.emit(tokenEqual); err != nil {
228 | return err
229 | }
230 |
231 | if err := lexSegments(l); err != nil {
232 | return err
233 | }
234 | r = l.next()
235 | }
236 |
237 | if r != '}' {
238 | return l.errUnexpected()
239 | }
240 | return l.emit(tokenVariableEnd)
241 | }
242 |
243 | func lexSegment(l *lexer) error {
244 | r := l.next()
245 | switch {
246 | case unicode.IsLetter(r):
247 | return lexLiteral(l)
248 | case r == '*':
249 | rn := l.next()
250 | if rn == '*' {
251 | return l.emit(tokenStarStar)
252 | }
253 | l.backup()
254 | return l.emit(tokenStar)
255 | case r == '{':
256 | l.backup()
257 | return lexVariable(l)
258 | default:
259 | return l.errUnexpected()
260 | }
261 | }
262 |
263 | func lexSegments(l *lexer) error {
264 | for {
265 | if err := lexSegment(l); err != nil {
266 | return err
267 | }
268 | if r := l.next(); r != '/' {
269 | l.backup() // unknown
270 | return nil
271 | }
272 | if err := l.emit(tokenSlash); err != nil {
273 | return err
274 | }
275 | }
276 | }
277 |
278 | func lexTemplate(l *lexer) error {
279 | if r := l.next(); r != '/' {
280 | return l.errUnexpected()
281 | }
282 | if err := l.emit(tokenSlash); err != nil {
283 | return err
284 | }
285 | if err := lexSegments(l); err != nil {
286 | return err
287 | }
288 |
289 | switch r := l.next(); r {
290 | case ':':
291 | if err := l.emit(tokenVerb); err != nil {
292 | return err
293 | }
294 | return lexVerb(l)
295 | case eof:
296 | if err := l.emit(tokenEOF); err != nil {
297 | return err
298 | }
299 | return nil
300 | default:
301 | return l.errUnexpected()
302 | }
303 | }
304 |
305 | func lexPathSegment(l *lexer) error {
306 | if i := l.acceptRun(isPath); i == 0 {
307 | return l.errShort()
308 | }
309 | return l.emit(tokenPath)
310 | }
311 |
312 | // lexPath emits all tokenSlash, tokenVerb and the rest as tokenPath
313 | func lexPath(l *lexer) error {
314 | for {
315 | switch r := l.next(); r {
316 | case '/':
317 | if err := l.emit(tokenSlash); err != nil {
318 | return err
319 | }
320 | if err := lexPathSegment(l); err != nil {
321 | return err
322 | }
323 | case ':':
324 | if err := l.emit(tokenVerb); err != nil {
325 | return err
326 | }
327 | if err := lexPathSegment(l); err != nil {
328 | return err
329 | }
330 | case eof:
331 | return l.emit(tokenEOF)
332 | default:
333 | return l.errUnexpected()
334 | }
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/larking/lexer_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "testing"
9 | "unsafe"
10 | )
11 |
12 | func TestLexer(t *testing.T) {
13 | tests := []struct {
14 | name string
15 | tmpl string
16 | want tokens
17 | wantErr bool
18 | }{{
19 | name: "one",
20 | tmpl: "/v1/messages/{name=name/*}",
21 | want: tokens{
22 | {typ: tokenSlash, val: "/"},
23 | {typ: tokenLiteral, val: "v1"},
24 | {typ: tokenSlash, val: "/"},
25 | {typ: tokenLiteral, val: "messages"},
26 | {typ: tokenSlash, val: "/"},
27 | {typ: tokenVariableStart, val: "{"},
28 | {typ: tokenIdent, val: "name"},
29 | {typ: tokenEqual, val: "="},
30 | {typ: tokenLiteral, val: "name"},
31 | {typ: tokenSlash, val: "/"},
32 | {typ: tokenStar, val: "*"},
33 | {typ: tokenVariableEnd, val: "}"},
34 | {typ: tokenEOF, val: ""},
35 | },
36 | }}
37 |
38 | for _, tt := range tests {
39 | t.Run(tt.name, func(t *testing.T) {
40 |
41 | l := &lexer{
42 | input: tt.tmpl,
43 | }
44 | err := lexTemplate(l)
45 | if tt.wantErr {
46 | if err == nil {
47 | t.Error("wanted failure but succeeded")
48 | }
49 | return
50 | }
51 | if err != nil {
52 | t.Error(err)
53 | }
54 | if n, m := len(tt.want), len(l.tokens()); n != m {
55 | t.Errorf("mismatch length %v != %v:\n\t%v\n\t%v", n, m, tt.want, l.tokens())
56 | return
57 | }
58 | for i, want := range tt.want {
59 | tok := l.toks[i]
60 | if want.typ != tok.typ || want.val != tok.val {
61 | t.Errorf("%d: %v != %v", i, tok, want)
62 | }
63 | }
64 | })
65 | }
66 | }
67 |
68 | func BenchmarkLexer(b *testing.B) {
69 | var l lexer
70 | input := "/v1/books/1/shevles/1:read"
71 |
72 | b.ReportAllocs()
73 | b.ResetTimer()
74 | for i := 0; i < b.N; i++ {
75 | l = lexer{input: input}
76 | if err := lexPath(&l); err != nil {
77 | b.Fatal(err)
78 | }
79 | }
80 | b.StopTimer()
81 | if n := l.len; n != 13 {
82 | b.Errorf("expected %d tokens: %d", 7, n)
83 | }
84 | b.Logf("%v", unsafe.Sizeof(l))
85 | }
86 |
--------------------------------------------------------------------------------
/larking/log.go:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "context"
9 |
10 | "google.golang.org/grpc"
11 | )
12 |
13 | type logStream struct {
14 | grpc.ServerStream
15 | info *grpc.StreamServerInfo
16 | ctxFn NewContextFunc
17 | }
18 |
19 | func (s logStream) Context() context.Context {
20 | return s.ctxFn(s.ServerStream.Context(), s.info.FullMethod, s.info.IsClientStream, s.info.IsServerStream)
21 | }
22 |
23 | // NewContextFunc is a function that creates a new context for a request.
24 | // The returned context is used for the duration of the request.
25 | type NewContextFunc func(ctx context.Context, fullMethod string, isClientStream, isServerStream bool) context.Context
26 |
27 | // NewUnaryContext returns a UnaryServerInterceptor that calls ctxFn to
28 | // create a new context for each request.
29 | func NewUnaryContext(ctxFn NewContextFunc) grpc.UnaryServerInterceptor {
30 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
31 | ctx = ctxFn(ctx, info.FullMethod, false, false)
32 | return handler(ctx, req)
33 | }
34 | }
35 |
36 | // NewStreamContext returns a StreamServerInterceptor that calls ctxFn to
37 | // create a new context for each request.
38 | func NewStreamContext(ctxFn NewContextFunc) grpc.StreamServerInterceptor {
39 | return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
40 | return handler(srv, logStream{
41 | ServerStream: ss,
42 | info: info,
43 | ctxFn: ctxFn,
44 | })
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/larking/mux_test.go:
--------------------------------------------------------------------------------
1 | package larking
2 |
3 | import (
4 | "testing"
5 |
6 | "google.golang.org/genproto/googleapis/api/annotations"
7 | )
8 |
9 | func TestRuleSelector(t *testing.T) {
10 | rule := &annotations.HttpRule{
11 | Selector: "larking.LarkingService.Get",
12 | Pattern: &annotations.HttpRule_Get{
13 | Get: "/v1/{name=projects/*/instances/*}",
14 | },
15 | }
16 | healthzRule := &annotations.HttpRule{
17 | Selector: "grpc.health.v1.Health.Check",
18 | Pattern: &annotations.HttpRule_Get{
19 | Get: "/healthz",
20 | },
21 | }
22 | wildcardRule := &annotations.HttpRule{
23 | Selector: "wildcard.Service.*",
24 | Pattern: &annotations.HttpRule_Get{
25 | Get: "/wildcard",
26 | },
27 | }
28 |
29 | var hr ruleSelector
30 | hr.setRules([]*annotations.HttpRule{rule, healthzRule, wildcardRule})
31 |
32 | t.Log(&hr)
33 |
34 | rules := hr.getRules("larking.LarkingService.Get")
35 | if rules == nil {
36 | t.Fatal("got nil")
37 | }
38 | got := rules[0]
39 | if got != rule {
40 | t.Fatalf("got %v, want %v", got, rule)
41 | }
42 |
43 | rules = hr.getRules("grpc.health.v1.Health.Check")
44 | if rules == nil {
45 | t.Fatal("got nil")
46 | }
47 | got = rules[0]
48 | if got != healthzRule {
49 | t.Fatalf("got %v, want %v", got, healthzRule)
50 | }
51 |
52 | rules = hr.getRules("wildcard.Service.Get.DeepMethod")
53 | if rules == nil {
54 | t.Fatal("got nil")
55 | }
56 | got = rules[0]
57 | if got != wildcardRule {
58 | t.Fatalf("got %v, want %v", got, wildcardRule)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/larking/negotiate.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 The Go Authors. All rights reserved.
2 | //
3 | // Use of this source code is governed by a BSD-style
4 | // license that can be found in the LICENSE file or at
5 | // https://developers.google.com/open-source/licenses/bsd.
6 |
7 | package larking
8 |
9 | // Source: github.com/golang/gddo/httputil
10 |
11 | import (
12 | "net/http"
13 | "strings"
14 | )
15 |
16 | // Octet types from RFC 2616.
17 | var octetTypes [256]octetType
18 |
19 | type octetType byte
20 |
21 | const (
22 | isToken octetType = 1 << iota
23 | isSpace
24 | )
25 |
26 | func init() {
27 | // OCTET =
28 | // CHAR =
29 | // CTL =
30 | // CR =
31 | // LF =
32 | // SP =
33 | // HT =
34 | // <"> =
35 | // CRLF = CR LF
36 | // LWS = [CRLF] 1*( SP | HT )
37 | // TEXT =
38 | // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
39 | // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
40 | // token = 1*
41 | // qdtext = >
42 |
43 | for c := 0; c < 256; c++ {
44 | var t octetType
45 | isCtl := c <= 31 || c == 127
46 | isChar := 0 <= c && c <= 127
47 | isSeparator := strings.ContainsRune(" \t\"(),/:;<=>?@[]\\{}", rune(c))
48 | if strings.ContainsRune(" \t\r\n", rune(c)) {
49 | t |= isSpace
50 | }
51 | if isChar && !isCtl && !isSeparator {
52 | t |= isToken
53 | }
54 | octetTypes[c] = t
55 | }
56 | }
57 |
58 | func expectQuality(s string) (q float64, rest string) {
59 | switch {
60 | case len(s) == 0:
61 | return -1, ""
62 | case s[0] == '0':
63 | q = 0
64 | case s[0] == '1':
65 | q = 1
66 | default:
67 | return -1, ""
68 | }
69 | s = s[1:]
70 | if !strings.HasPrefix(s, ".") {
71 | return q, s
72 | }
73 | s = s[1:]
74 | i := 0
75 | n := 0
76 | d := 1
77 | for ; i < len(s); i++ {
78 | b := s[i]
79 | if b < '0' || b > '9' {
80 | break
81 | }
82 | n = n*10 + int(b) - '0'
83 | d *= 10
84 | }
85 | return q + float64(n)/float64(d), s[i:]
86 | }
87 |
88 | func skipSpace(s string) (rest string) {
89 | i := 0
90 | for ; i < len(s); i++ {
91 | if octetTypes[s[i]]&isSpace == 0 {
92 | break
93 | }
94 | }
95 | return s[i:]
96 | }
97 |
98 | func expectTokenSlash(s string) (token, rest string) {
99 | i := 0
100 | for ; i < len(s); i++ {
101 | b := s[i]
102 | if (octetTypes[b]&isToken == 0) && b != '/' {
103 | break
104 | }
105 | }
106 | return s[:i], s[i:]
107 | }
108 |
109 | // acceptSpec describes an Accept* header.
110 | type acceptSpec struct {
111 | Value string
112 | Q float64
113 | }
114 |
115 | // parseAccept parses Accept* headers.
116 | func parseAccept(values []string) (specs []acceptSpec) {
117 | loop:
118 | for _, s := range values {
119 | for {
120 | var spec acceptSpec
121 | spec.Value, s = expectTokenSlash(s)
122 | if spec.Value == "" {
123 | continue loop
124 | }
125 | spec.Q = 1.0
126 | s = skipSpace(s)
127 | if strings.HasPrefix(s, ";") {
128 | s = skipSpace(s[1:])
129 | if !strings.HasPrefix(s, "q=") {
130 | continue loop
131 | }
132 | spec.Q, s = expectQuality(s[2:])
133 | if spec.Q < 0.0 {
134 | continue loop
135 | }
136 | }
137 | specs = append(specs, spec)
138 | s = skipSpace(s)
139 | if !strings.HasPrefix(s, ",") {
140 | continue loop
141 | }
142 | s = skipSpace(s[1:])
143 | }
144 | }
145 | return
146 | }
147 |
148 | // negotiateContentEncoding returns the best offered content encoding for the
149 | // request's Accept-Encoding header. If two offers match with equal weight and
150 | // then the offer earlier in the list is preferred. If no offers are
151 | // acceptable, then "" is returned.
152 | func negotiateContentEncoding(header http.Header, offers []string) string {
153 | bestOffer := "identity"
154 | bestQ := -1.0
155 | specs := parseAccept(header["Accept-Encoding"])
156 | for _, offer := range offers {
157 | for _, spec := range specs {
158 | if spec.Q > bestQ &&
159 | (spec.Value == "*" || spec.Value == offer) {
160 | bestQ = spec.Q
161 | bestOffer = offer
162 | }
163 | }
164 | }
165 | if bestQ == 0 {
166 | bestOffer = ""
167 | }
168 | return bestOffer
169 | }
170 |
171 | // negotiateContentType returns the best offered content type for the request's
172 | // Accept header. If two offers match with equal weight, then the more specific
173 | // offer is preferred. For example, text/* trumps */*. If two offers match
174 | // with equal weight and specificity, then the offer earlier in the list is
175 | // preferred. If no offers match, then defaultOffer is returned.
176 | func negotiateContentType(header http.Header, offers []string, defaultOffer string) string {
177 | bestOffer := defaultOffer
178 | bestQ := -1.0
179 | bestWild := 3
180 | specs := parseAccept(header["Accept"])
181 | for _, offer := range offers {
182 | for _, spec := range specs {
183 | switch {
184 | case spec.Q == 0.0:
185 | // ignore
186 | case spec.Q < bestQ:
187 | // better match found
188 | case spec.Value == "*/*":
189 | if spec.Q > bestQ || bestWild > 2 {
190 | bestQ = spec.Q
191 | bestWild = 2
192 | bestOffer = offer
193 | }
194 | case strings.HasSuffix(spec.Value, "/*"):
195 | if strings.HasPrefix(offer, spec.Value[:len(spec.Value)-1]) &&
196 | (spec.Q > bestQ || bestWild > 1) {
197 | bestQ = spec.Q
198 | bestWild = 1
199 | bestOffer = offer
200 | }
201 | default:
202 | if spec.Value == offer &&
203 | (spec.Q > bestQ || bestWild > 0) {
204 | bestQ = spec.Q
205 | bestWild = 0
206 | bestOffer = offer
207 | }
208 | }
209 | }
210 | }
211 | return bestOffer
212 | }
213 |
--------------------------------------------------------------------------------
/larking/negotiate_test.go:
--------------------------------------------------------------------------------
1 | package larking
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 | )
7 |
8 | var negotiateContentEncodingTests = []struct {
9 | s string
10 | offers []string
11 | expect string
12 | }{
13 | {"", []string{"identity", "gzip"}, "identity"},
14 | {"*;q=0", []string{"identity", "gzip"}, ""},
15 | {"gzip", []string{"identity", "gzip"}, "gzip"},
16 | }
17 |
18 | func TestNegotiateContentEnoding(t *testing.T) {
19 | for _, tt := range negotiateContentEncodingTests {
20 | h := http.Header{"Accept-Encoding": {tt.s}}
21 | actual := negotiateContentEncoding(h, tt.offers)
22 | if actual != tt.expect {
23 | t.Errorf("NegotiateContentEncoding(%q, %#v)=%q, want %q", tt.s, tt.offers, actual, tt.expect)
24 | }
25 | }
26 | }
27 |
28 | var negotiateContentTypeTests = []struct {
29 | s string
30 | offers []string
31 | defaultOffer string
32 | expect string
33 | }{
34 | {"application/proto", []string{"application/json", "application/proto"}, "application/json", "application/proto"},
35 | {"text/html, */*;q=0", []string{"x/y"}, "", ""},
36 | {"text/html, */*", []string{"x/y"}, "", "x/y"},
37 | {"text/html, image/png", []string{"text/html", "image/png"}, "", "text/html"},
38 | {"text/html, image/png", []string{"image/png", "text/html"}, "", "image/png"},
39 | {"text/html, image/png; q=0.5", []string{"image/png"}, "", "image/png"},
40 | {"text/html, image/png; q=0.5", []string{"text/html"}, "", "text/html"},
41 | {"text/html, image/png; q=0.5", []string{"foo/bar"}, "", ""},
42 | {"text/html, image/png; q=0.5", []string{"image/png", "text/html"}, "", "text/html"},
43 | {"text/html, image/png; q=0.5", []string{"text/html", "image/png"}, "", "text/html"},
44 | {"text/html;q=0.5, image/png", []string{"image/png"}, "", "image/png"},
45 | {"text/html;q=0.5, image/png", []string{"text/html"}, "", "text/html"},
46 | {"text/html;q=0.5, image/png", []string{"image/png", "text/html"}, "", "image/png"},
47 | {"text/html;q=0.5, image/png", []string{"text/html", "image/png"}, "", "image/png"},
48 | {"image/png, image/*;q=0.5", []string{"image/jpg", "image/png"}, "", "image/png"},
49 | {"image/png, image/*;q=0.5", []string{"image/jpg"}, "", "image/jpg"},
50 | {"image/png, image/*;q=0.5", []string{"image/jpg", "image/gif"}, "", "image/jpg"},
51 | {"image/png, image/*", []string{"image/jpg", "image/gif"}, "", "image/jpg"},
52 | {"image/png, image/*", []string{"image/gif", "image/jpg"}, "", "image/gif"},
53 | {"image/png, image/*", []string{"image/gif", "image/png"}, "", "image/png"},
54 | {"image/png, image/*", []string{"image/png", "image/gif"}, "", "image/png"},
55 | }
56 |
57 | func TestNegotiateContentType(t *testing.T) {
58 | for _, tt := range negotiateContentTypeTests {
59 | h := http.Header{"Accept": {tt.s}}
60 | actual := negotiateContentType(h, tt.offers, tt.defaultOffer)
61 | if actual != tt.expect {
62 | t.Errorf("NegotiateContentType(%q, %#v, %q)=%q, want %q", tt.s, tt.offers, tt.defaultOffer, actual, tt.expect)
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/larking/proxy.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "io"
9 |
10 | "google.golang.org/grpc"
11 | rpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
12 | "google.golang.org/protobuf/proto"
13 | "google.golang.org/protobuf/reflect/protodesc"
14 | "google.golang.org/protobuf/reflect/protoreflect"
15 | "google.golang.org/protobuf/reflect/protoregistry"
16 | )
17 |
18 | // TODO: fetch type on a per stream basis
19 | type serverReflectionServer struct {
20 | rpb.UnimplementedServerReflectionServer
21 | m *Mux
22 | s *grpc.Server
23 | }
24 |
25 | // RegisterReflectionServer registers the server reflection service for multiple
26 | // proxied gRPC servers. Each individual reflection stream is merged to provide
27 | // a consistent view at the point of stream creation.
28 | func (m *Mux) RegisterReflectionServer(s *grpc.Server) {
29 | rpb.RegisterServerReflectionServer(s, &serverReflectionServer{
30 | m: m,
31 | s: s,
32 | })
33 | }
34 |
35 | // does marshalling on it and returns the marshalled result.
36 | //
37 | //nolint:unused // fileDescEncodingByFilename finds the file descriptor for given filename,
38 | func (s *serverReflectionServer) fileDescEncodingByFilename(name string) ([]byte, error) {
39 | fd, err := protoregistry.GlobalFiles.FindFileByPath(name)
40 | if err != nil {
41 | return nil, err
42 | }
43 | return proto.Marshal(protodesc.ToFileDescriptorProto(fd))
44 | }
45 |
46 | // does marshalling on it and returns the marshalled result.
47 | // The given symbol can be a type, a service or a method.
48 | //
49 | //nolint:unused // fileDescEncodingContainingSymbol finds the file descriptor containing the given symbol,
50 | func (s *serverReflectionServer) fileDescEncodingContainingSymbol(name string) ([]byte, error) {
51 | fullname := protoreflect.FullName(name)
52 | d, err := protoregistry.GlobalFiles.FindDescriptorByName(fullname)
53 | if err != nil {
54 | return nil, err
55 | }
56 | fd := d.ParentFile()
57 | return proto.Marshal(protodesc.ToFileDescriptorProto(fd))
58 | }
59 |
60 | // does marshalling on it and returns the marshalled result.
61 | //
62 | //nolint:unused // fileDescEncodingContainingExtension finds the file descriptor containing given extension,
63 | func (s *serverReflectionServer) fileDescEncodingContainingExtension(typeName string, extNum int32) ([]byte, error) {
64 | fullname := protoreflect.FullName(typeName)
65 | fieldnumber := protoreflect.FieldNumber(extNum)
66 | ext, err := protoregistry.GlobalTypes.FindExtensionByNumber(fullname, fieldnumber)
67 | if err != nil {
68 | return nil, err
69 | }
70 |
71 | extd := ext.TypeDescriptor()
72 | d, err := protoregistry.GlobalFiles.FindDescriptorByName(extd.FullName())
73 | if err != nil {
74 | return nil, err
75 | }
76 | fd := d.ParentFile()
77 |
78 | return proto.Marshal(protodesc.ToFileDescriptorProto(fd))
79 | }
80 |
81 | //nolint:unused // allExtensionNumbersForTypeName returns all extension numbers for the given type.
82 | func (s *serverReflectionServer) allExtensionNumbersForTypeName(name string) ([]int32, error) {
83 | fullname := protoreflect.FullName(name)
84 | _, err := protoregistry.GlobalFiles.FindDescriptorByName(fullname)
85 | if err != nil {
86 | return nil, err
87 | }
88 |
89 | n := protoregistry.GlobalTypes.NumExtensionsByMessage(fullname)
90 | if n == 0 {
91 | return nil, nil
92 | }
93 |
94 | extNums := make([]int32, 0, n)
95 | protoregistry.GlobalTypes.RangeExtensionsByMessage(
96 | fullname,
97 | func(et protoreflect.ExtensionType) bool {
98 | ed := et.TypeDescriptor().Descriptor()
99 | extNums = append(extNums, int32(ed.Number()))
100 | return true
101 | },
102 | )
103 | return extNums, nil
104 | }
105 |
106 | // ServerReflectionInfo is the reflection service handler.
107 | func (s *serverReflectionServer) ServerReflectionInfo(stream rpb.ServerReflection_ServerReflectionInfoServer) error {
108 |
109 | //ss := s.m.loadState()
110 |
111 | for {
112 | in, err := stream.Recv()
113 | if err == io.EOF {
114 | return nil
115 | }
116 | if err != nil {
117 | return err
118 | }
119 |
120 | out := &rpb.ServerReflectionResponse{
121 | ValidHost: in.Host,
122 | OriginalRequest: in,
123 | }
124 | /*switch req := in.MessageRequest.(type) {
125 | case *rpb.ServerReflectionRequest_FileByFilename:
126 | b, err := s.fileDescEncodingByFilename(req.FileByFilename)
127 | if err != nil {
128 | out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{
129 | ErrorResponse: &rpb.ErrorResponse{
130 | ErrorCode: int32(codes.NotFound),
131 | ErrorMessage: err.Error(),
132 | },
133 | }
134 | } else {
135 | out.MessageResponse = &rpb.ServerReflectionResponse_FileDescriptorResponse{
136 | FileDescriptorResponse: &rpb.FileDescriptorResponse{FileDescriptorProto: [][]byte{b}},
137 | }
138 | }
139 | case *rpb.ServerReflectionRequest_FileContainingSymbol:
140 | b, err := s.fileDescEncodingContainingSymbol(req.FileContainingSymbol)
141 | if err != nil {
142 | out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{
143 | ErrorResponse: &rpb.ErrorResponse{
144 | ErrorCode: int32(codes.NotFound),
145 | ErrorMessage: err.Error(),
146 | },
147 | }
148 | } else {
149 | out.MessageResponse = &rpb.ServerReflectionResponse_FileDescriptorResponse{
150 | FileDescriptorResponse: &rpb.FileDescriptorResponse{FileDescriptorProto: [][]byte{b}},
151 | }
152 | }
153 | case *rpb.ServerReflectionRequest_FileContainingExtension:
154 | typeName := req.FileContainingExtension.ContainingType
155 | extNum := req.FileContainingExtension.ExtensionNumber
156 | b, err := s.fileDescEncodingContainingExtension(typeName, extNum)
157 | if err != nil {
158 | out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{
159 | ErrorResponse: &rpb.ErrorResponse{
160 | ErrorCode: int32(codes.NotFound),
161 | ErrorMessage: err.Error(),
162 | },
163 | }
164 | } else {
165 | out.MessageResponse = &rpb.ServerReflectionResponse_FileDescriptorResponse{
166 | FileDescriptorResponse: &rpb.FileDescriptorResponse{FileDescriptorProto: [][]byte{b}},
167 | }
168 | }
169 | case *rpb.ServerReflectionRequest_AllExtensionNumbersOfType:
170 | extNums, err := s.allExtensionNumbersForTypeName(req.AllExtensionNumbersOfType)
171 | if err != nil {
172 | out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{
173 | ErrorResponse: &rpb.ErrorResponse{
174 | ErrorCode: int32(codes.NotFound),
175 | ErrorMessage: err.Error(),
176 | },
177 | }
178 | } else {
179 | out.MessageResponse = &rpb.ServerReflectionResponse_AllExtensionNumbersResponse{
180 | AllExtensionNumbersResponse: &rpb.ExtensionNumberResponse{
181 | BaseTypeName: req.AllExtensionNumbersOfType,
182 | ExtensionNumber: extNums,
183 | },
184 | }
185 | }
186 | case *rpb.ServerReflectionRequest_ListServices:
187 | svcInfo := s.s.GetServiceInfo()
188 | serviceResponses := make([]*rpb.ServiceResponse, 0, len(svcInfo))
189 | for svcName := range svcInfo {
190 | serviceResponses = append(serviceResponses, &rpb.ServiceResponse{
191 | Name: svcName,
192 | })
193 | }
194 | sort.Slice(serviceResponses, func(i, j int) bool {
195 | return serviceResponses[i].Name < serviceResponses[j].Name
196 | })
197 | out.MessageResponse = &rpb.ServerReflectionResponse_ListServicesResponse{
198 | ListServicesResponse: &rpb.ListServiceResponse{
199 | Service: serviceResponses,
200 | },
201 | }
202 | default:
203 | return status.Errorf(codes.InvalidArgument, "invalid MessageRequest: %v", in.MessageRequest)
204 | }*/
205 |
206 | if err := stream.Send(out); err != nil {
207 | return err
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/larking/proxy_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "context"
9 | "net"
10 | "net/http"
11 | "testing"
12 |
13 | "github.com/google/go-cmp/cmp"
14 | "google.golang.org/genproto/googleapis/api/httpbody"
15 | "google.golang.org/grpc"
16 | "google.golang.org/grpc/credentials/insecure"
17 | "google.golang.org/grpc/metadata"
18 | "google.golang.org/grpc/reflection"
19 | "google.golang.org/protobuf/proto"
20 | "google.golang.org/protobuf/testing/protocmp"
21 |
22 | "golang.org/x/sync/errgroup"
23 | "larking.io/api/testpb"
24 | )
25 |
26 | func TestGRPCProxy(t *testing.T) {
27 | // Create test server.
28 | ms := &testpb.UnimplementedMessagingServer{}
29 | fs := &testpb.UnimplementedFilesServer{}
30 |
31 | o := &overrides{}
32 | gs := grpc.NewServer(o.streamOption(), o.unaryOption())
33 | testpb.RegisterMessagingServer(gs, ms)
34 | testpb.RegisterFilesServer(gs, fs)
35 | reflection.Register(gs)
36 |
37 | lis, err := net.Listen("tcp", "localhost:0")
38 | if err != nil {
39 | t.Fatalf("failed to listen: %v", err)
40 | }
41 | defer lis.Close()
42 |
43 | var g errgroup.Group
44 | defer func() {
45 | if err := g.Wait(); err != nil {
46 | if err != http.ErrServerClosed {
47 | t.Fatal(err)
48 | }
49 | }
50 | }()
51 |
52 | g.Go(func() error {
53 | return gs.Serve(lis)
54 | })
55 | defer gs.Stop()
56 |
57 | // Create the client.
58 | conn, err := grpc.Dial(
59 | lis.Addr().String(),
60 | grpc.WithTransportCredentials(insecure.NewCredentials()),
61 | )
62 | if err != nil {
63 | t.Fatalf("cannot connect to server: %v", err)
64 | }
65 | defer conn.Close()
66 |
67 | h, err := NewMux()
68 | if err != nil {
69 | t.Fatal(err)
70 | }
71 | if err := h.RegisterConn(context.Background(), conn); err != nil {
72 | t.Fatal(err)
73 | }
74 |
75 | lisProxy, err := net.Listen("tcp", "localhost:0")
76 | if err != nil {
77 | t.Fatalf("failed to listen: %v", err)
78 | }
79 | defer lisProxy.Close()
80 |
81 | ts, err := NewServer(h)
82 | if err != nil {
83 | t.Fatal(err)
84 | }
85 |
86 | g.Go(func() error {
87 | return ts.Serve(lisProxy)
88 | })
89 | defer ts.Close()
90 |
91 | cc, err := grpc.Dial(
92 | lisProxy.Addr().String(),
93 | grpc.WithTransportCredentials(insecure.NewCredentials()),
94 | )
95 | if err != nil {
96 | t.Fatal(err)
97 | }
98 |
99 | cmpOpts := cmp.Options{protocmp.Transform()}
100 |
101 | var unaryStreamDesc = &grpc.StreamDesc{
102 | ClientStreams: false,
103 | ServerStreams: false,
104 | }
105 |
106 | tests := []struct {
107 | name string
108 | desc *grpc.StreamDesc
109 | method string
110 | inouts []interface{}
111 | }{{
112 | name: "unary_message",
113 | desc: unaryStreamDesc,
114 | method: "/larking.testpb.Messaging/GetMessageOne",
115 | inouts: []interface{}{in{
116 | msg: &testpb.GetMessageRequestOne{Name: "proxy"},
117 | }, out{
118 | msg: &testpb.Message{Text: "success"},
119 | }},
120 | }, {
121 | name: "stream_file",
122 | desc: &grpc.StreamDesc{
123 | ClientStreams: true,
124 | ServerStreams: true,
125 | },
126 | method: "/larking.testpb.Files/LargeUploadDownload",
127 | inouts: []interface{}{in{
128 | method: "/larking.testpb.Files/LargeUploadDownload",
129 | msg: &testpb.UploadFileRequest{
130 | Filename: "cat.jpg",
131 | File: &httpbody.HttpBody{
132 | ContentType: "jpg",
133 | Data: []byte("cat"),
134 | },
135 | },
136 | }, in{
137 | msg: &testpb.UploadFileRequest{
138 | File: &httpbody.HttpBody{
139 | Data: []byte("dog"),
140 | },
141 | },
142 | }, out{
143 | msg: &httpbody.HttpBody{
144 | Data: []byte("cat"),
145 | },
146 | }, out{
147 | msg: &httpbody.HttpBody{
148 | Data: []byte("dog"),
149 | },
150 | }},
151 | }}
152 |
153 | for _, tt := range tests {
154 | t.Run(tt.name, func(t *testing.T) {
155 | o.reset(t, "test", tt.inouts)
156 |
157 | ctx := context.Background()
158 | ctx = metadata.AppendToOutgoingContext(ctx, "test", tt.method)
159 |
160 | s, err := cc.NewStream(ctx, tt.desc, tt.method)
161 | if err != nil {
162 | t.Fatal(err)
163 | }
164 |
165 | for i := 0; i < len(tt.inouts); i++ {
166 | switch typ := tt.inouts[i].(type) {
167 | case in:
168 | if err := s.SendMsg(typ.msg); err != nil {
169 | t.Fatal(err)
170 | }
171 | case out:
172 | out := proto.Clone(typ.msg)
173 | if err := s.RecvMsg(out); err != nil {
174 | t.Fatal(err)
175 | }
176 | diff := cmp.Diff(out, typ.msg, cmpOpts...)
177 | if diff != "" {
178 | t.Fatal(diff)
179 | }
180 | }
181 | }
182 | })
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/larking/server.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "context"
9 | "crypto/tls"
10 | "fmt"
11 | "net/http"
12 | "os"
13 | "os/signal"
14 | "strings"
15 | "time"
16 |
17 | "golang.org/x/net/http2"
18 | "golang.org/x/net/http2/h2c"
19 | )
20 |
21 | // NewOSSignalContext tries to gracefully handle OS closure.
22 | func NewOSSignalContext(ctx context.Context) (context.Context, func()) {
23 | // trap Ctrl+C and call cancel on the context
24 | ctx, cancel := context.WithCancel(ctx)
25 | c := make(chan os.Signal, 1)
26 | signal.Notify(c, os.Interrupt)
27 | go func() {
28 | select {
29 | case <-c:
30 | cancel()
31 | case <-ctx.Done():
32 | }
33 | }()
34 |
35 | return ctx, func() {
36 | signal.Stop(c)
37 | cancel()
38 | }
39 | }
40 |
41 | // NewServer creates a new http.Server with http2 support.
42 | // The server is configured with the given options.
43 | // It is a convenience function for creating a new http.Server.
44 | func NewServer(mux *Mux, opts ...ServerOption) (*http.Server, error) {
45 | if mux == nil {
46 | return nil, fmt.Errorf("invalid mux must not be nil")
47 | }
48 |
49 | var svrOpts serverOptions
50 | for _, opt := range opts {
51 | if err := opt(&svrOpts); err != nil {
52 | return nil, err
53 | }
54 | }
55 |
56 | h := svrOpts.serveMux
57 | if h == nil {
58 | h = http.NewServeMux()
59 | }
60 | if len(svrOpts.muxPatterns) == 0 {
61 | svrOpts.muxPatterns = []string{"/"}
62 | }
63 | for _, pattern := range svrOpts.muxPatterns {
64 | prefix := strings.TrimSuffix(pattern, "/")
65 | if len(prefix) > 0 {
66 | h.Handle(prefix+"/", http.StripPrefix(prefix, mux))
67 | } else {
68 | h.Handle("/", mux)
69 | }
70 | }
71 |
72 | h2s := &http2.Server{}
73 | hs := &http.Server{
74 | ReadHeaderTimeout: 10 * time.Second,
75 | MaxHeaderBytes: 1 << 20, // 1 MB
76 | Handler: h2c.NewHandler(h, h2s),
77 | TLSConfig: svrOpts.tlsConfig,
78 | }
79 | if err := http2.ConfigureServer(hs, h2s); err != nil {
80 | return nil, err
81 | }
82 | return hs, nil
83 | }
84 |
85 | type serverOptions struct {
86 | tlsConfig *tls.Config
87 | serveMux *http.ServeMux
88 | muxPatterns []string
89 | }
90 |
91 | // ServerOption is similar to grpc.ServerOption.
92 | type ServerOption func(*serverOptions) error
93 |
94 | func TLSCredsOption(c *tls.Config) ServerOption {
95 | return func(opts *serverOptions) error {
96 | opts.tlsConfig = c
97 | return nil
98 | }
99 | }
100 |
101 | func MuxHandleOption(patterns ...string) ServerOption {
102 | return func(opts *serverOptions) error {
103 | if opts.muxPatterns != nil {
104 | return fmt.Errorf("duplicate mux patterns registered")
105 | }
106 | opts.muxPatterns = patterns
107 | return nil
108 | }
109 | }
110 |
111 | func HTTPHandlerOption(pattern string, handler http.Handler) ServerOption {
112 | return func(opts *serverOptions) error {
113 | if opts.serveMux == nil {
114 | opts.serveMux = http.NewServeMux()
115 | }
116 | opts.serveMux.Handle(pattern, handler)
117 | return nil
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/larking/stats.go:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "time"
9 |
10 | "google.golang.org/grpc/stats"
11 | )
12 |
13 | const (
14 | payloadLen = 1
15 | sizeLen = 4
16 | headerLen = payloadLen + sizeLen
17 | )
18 |
19 | func outPayload(client bool, msg interface{}, payload []byte, t time.Time) *stats.OutPayload {
20 | return &stats.OutPayload{
21 | Client: client,
22 | Payload: msg,
23 | Length: len(payload),
24 | WireLength: len(payload) + headerLen,
25 | SentTime: t,
26 | }
27 | }
28 |
29 | func inPayload(client bool, msg interface{}, payload []byte, t time.Time) *stats.InPayload {
30 | return &stats.InPayload{
31 | Client: true,
32 | RecvTime: t,
33 | Payload: msg,
34 | Length: len(payload),
35 | WireLength: len(payload) + headerLen,
36 | }
37 | }
38 |
39 | // strAddr is a net.Addr backed by either a TCP "ip:port" string, or
40 | // the empty string if unknown.
41 | type strAddr string
42 |
43 | func (a strAddr) Network() string {
44 | if a != "" {
45 | // Per the documentation on net/http.Request.RemoteAddr, if this is
46 | // set, it's set to the IP:port of the peer (hence, TCP):
47 | // https://golang.org/pkg/net/http/#Request
48 | //
49 | // If we want to support Unix sockets later, we can
50 | // add our own grpc-specific convention within the
51 | // grpc codebase to set RemoteAddr to a different
52 | // format, or probably better: we can attach it to the
53 | // context and use that from serverHandlerTransport.RemoteAddr.
54 | return "tcp"
55 | }
56 | return ""
57 | }
58 | func (a strAddr) String() string { return string(a) }
59 |
--------------------------------------------------------------------------------
/larking/web.go:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | // Support for gRPC-web
8 | // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
9 | // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
10 |
11 | import (
12 | "bytes"
13 | "encoding/base64"
14 | "encoding/binary"
15 | "fmt"
16 | "io"
17 | "net/http"
18 | "strings"
19 | )
20 |
21 | const (
22 | grpcBase = "application/grpc"
23 | grpcWeb = "application/grpc-web"
24 | grpcWebText = "application/grpc-web-text"
25 | )
26 |
27 | // isWebRequest checks for gRPC Web headers.
28 | func isWebRequest(r *http.Request) (typ string, enc string, ok bool) {
29 | ct := r.Header.Get("Content-Type")
30 | if !strings.HasPrefix(ct, "application/grpc-web") || r.Method != http.MethodPost {
31 | return typ, enc, false
32 | }
33 | typ, enc, ok = strings.Cut(ct, "+")
34 | if !ok {
35 | enc = "proto"
36 | }
37 | ok = typ == grpcWeb || typ == grpcWebText
38 | return typ, enc, ok
39 | }
40 |
41 | type webWriter struct {
42 | w http.ResponseWriter
43 | resp io.Writer
44 | seenHeaders map[string]bool
45 | typ string // grpcWeb or grpcWebText
46 | enc string // proto or json
47 | wroteHeader bool
48 | wroteResp bool
49 | }
50 |
51 | func newWebWriter(w http.ResponseWriter, typ, enc string) *webWriter {
52 | var resp io.Writer = w
53 | if typ == grpcWebText {
54 | resp = base64.NewEncoder(base64.StdEncoding, resp)
55 |
56 | }
57 |
58 | return &webWriter{
59 | w: w,
60 | typ: typ,
61 | enc: enc,
62 | resp: resp,
63 | }
64 | }
65 |
66 | func (w *webWriter) seeHeaders() {
67 | hdr := w.Header()
68 | hdr.Set("Content-Type", w.typ+"+"+w.enc) // override content-type
69 |
70 | keys := make(map[string]bool, len(hdr))
71 | for k := range hdr {
72 | if strings.HasPrefix(k, http.TrailerPrefix) {
73 | continue
74 | }
75 | keys[k] = true
76 | }
77 | w.seenHeaders = keys
78 | w.wroteHeader = true
79 | }
80 |
81 | func (w *webWriter) Write(b []byte) (int, error) {
82 | if !w.wroteHeader {
83 | w.seeHeaders()
84 | }
85 | return w.resp.Write(b)
86 | }
87 |
88 | func (w *webWriter) Header() http.Header { return w.w.Header() }
89 |
90 | func (w *webWriter) WriteHeader(code int) {
91 | w.seeHeaders()
92 | w.w.WriteHeader(code)
93 | }
94 |
95 | func (w *webWriter) Flush() {
96 | if w.wroteHeader || w.wroteResp {
97 | if f, ok := w.w.(http.Flusher); ok {
98 | f.Flush()
99 | }
100 | }
101 | }
102 |
103 | func (w *webWriter) writeTrailer() error {
104 | hdr := w.Header()
105 |
106 | tr := make(http.Header, len(hdr)-len(w.seenHeaders)+1)
107 | for key, val := range hdr {
108 | if w.seenHeaders[key] {
109 | continue
110 | }
111 | key = strings.TrimPrefix(key, http.TrailerPrefix)
112 | // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
113 | tr[strings.ToLower(key)] = val
114 | }
115 |
116 | var buf bytes.Buffer
117 | if err := tr.Write(&buf); err != nil {
118 | return err
119 | }
120 |
121 | head := []byte{1 << 7, 0, 0, 0, 0} // MSB=1 indicates this is a trailer data frame.
122 | binary.BigEndian.PutUint32(head[1:5], uint32(buf.Len()))
123 | if _, err := w.Write(head); err != nil {
124 | return err
125 | }
126 | if _, err := w.Write(buf.Bytes()); err != nil {
127 | return err
128 | }
129 | return nil
130 | }
131 |
132 | func (w *webWriter) flushWithTrailer() {
133 | // Write trailers only if message has been sent.
134 | if w.wroteHeader || w.wroteResp {
135 | if err := w.writeTrailer(); err != nil {
136 | return // nothing
137 | }
138 | }
139 | w.Flush()
140 | }
141 |
142 | type readCloser struct {
143 | io.Reader
144 | io.Closer
145 | }
146 |
147 | func (m *Mux) serveGRPCWeb(w http.ResponseWriter, r *http.Request) {
148 | typ, enc, ok := isWebRequest(r)
149 | if !ok {
150 | msg := fmt.Sprintf("invalid gRPC-Web content type: %v", r.Header.Get("Content-Type"))
151 | http.Error(w, msg, http.StatusBadRequest)
152 | return
153 | }
154 | // TODO: Check for websocket request and upgrade.
155 | if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
156 | http.Error(w, "unimplemented websocket support", http.StatusInternalServerError)
157 | return
158 | }
159 |
160 | r.ProtoMajor = 2
161 | r.ProtoMinor = 0
162 |
163 | hdr := r.Header
164 | hdr.Del("Content-Length")
165 | hdr.Set("Content-Type", grpcBase+"+"+enc)
166 |
167 | if typ == grpcWebText {
168 | body := base64.NewDecoder(base64.StdEncoding, r.Body)
169 | r.Body = readCloser{body, r.Body}
170 | }
171 |
172 | ww := newWebWriter(w, typ, enc)
173 | m.serveGRPC(ww, r)
174 | ww.flushWithTrailer()
175 | }
176 |
--------------------------------------------------------------------------------
/larking/web_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2022 Edward McFarlane. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package larking
6 |
7 | import (
8 | "bytes"
9 | "encoding/binary"
10 | "io"
11 | "net/http"
12 | "net/http/httptest"
13 | "testing"
14 |
15 | "github.com/google/go-cmp/cmp"
16 | "google.golang.org/genproto/googleapis/rpc/status"
17 | "google.golang.org/protobuf/encoding/protojson"
18 | "google.golang.org/protobuf/proto"
19 | "google.golang.org/protobuf/testing/protocmp"
20 | "larking.io/api/testpb"
21 | )
22 |
23 | func TestWeb(t *testing.T) {
24 |
25 | // Create test server.
26 | ms := &testpb.UnimplementedMessagingServer{}
27 |
28 | o := new(overrides)
29 | m, err := NewMux(
30 | UnaryServerInterceptorOption(o.unary()),
31 | StreamServerInterceptorOption(o.stream()),
32 | )
33 | if err != nil {
34 | t.Fatalf("failed to create mux: %v", err)
35 | }
36 |
37 | testpb.RegisterMessagingServer(m, ms)
38 |
39 | type want struct {
40 | msg proto.Message
41 | statusCode int
42 | }
43 |
44 | frame := func(b []byte, msb uint8) []byte {
45 | head := append([]byte{0 | msb, 0, 0, 0, 0}, b...)
46 | binary.BigEndian.PutUint32(head[1:5], uint32(len(b)))
47 | return head
48 | }
49 | deframe := func(b []byte) ([]byte, []byte) {
50 | if len(b) < 5 {
51 | t.Errorf("invalid deframe")
52 | return nil, nil
53 | }
54 | x := int(binary.BigEndian.Uint32(b[1:5]))
55 | b = b[5:]
56 | return b[:x], b[x:]
57 | }
58 |
59 | // TODO: compare http.Response output
60 | tests := []struct {
61 | in in
62 | out out
63 | want want
64 | req *http.Request
65 | name string
66 | }{{
67 | name: "unary proto request",
68 | req: func() *http.Request {
69 | msg := &testpb.GetMessageRequestOne{Name: "name/hello"}
70 | b, err := proto.Marshal(msg)
71 | if err != nil {
72 | t.Fatal(err)
73 | }
74 |
75 | body := bytes.NewReader(frame(b, 0))
76 | req := httptest.NewRequest(http.MethodPost, "/larking.testpb.Messaging/GetMessageOne", body)
77 | req.Header.Set("Content-Type", grpcWeb+"+proto")
78 | return req
79 | }(),
80 | in: in{
81 | method: "/larking.testpb.Messaging/GetMessageOne",
82 | msg: &testpb.GetMessageRequestOne{Name: "name/hello"},
83 | },
84 | out: out{
85 | msg: &testpb.Message{Text: "hello, world!"},
86 | },
87 | want: want{
88 | statusCode: 200,
89 | msg: &testpb.Message{Text: "hello, world!"},
90 | },
91 | }}
92 |
93 | opts := cmp.Options{protocmp.Transform()}
94 | for _, tt := range tests {
95 | t.Run(tt.name, func(t *testing.T) {
96 | o.reset(t, "test", []interface{}{tt.in, tt.out})
97 |
98 | req := tt.req
99 | req.Header["test"] = []string{tt.in.method}
100 |
101 | w := httptest.NewRecorder()
102 | m.ServeHTTP(w, req)
103 | resp := w.Result()
104 |
105 | t.Log("resp", resp)
106 |
107 | b, err := io.ReadAll(resp.Body)
108 | if err != nil {
109 | t.Fatal(err)
110 | }
111 | t.Logf("resp length: %d", len(b))
112 |
113 | if sc := tt.want.statusCode; sc != resp.StatusCode {
114 | t.Errorf("expected %d got %d", tt.want.statusCode, resp.StatusCode)
115 | var msg status.Status
116 | if err := protojson.Unmarshal(b, &msg); err != nil {
117 | t.Error(err, string(b))
118 | return
119 | }
120 | t.Error("status.code", msg.Code)
121 | t.Error("status.message", msg.Message)
122 | return
123 | }
124 |
125 | //if tt.want.body != nil {
126 | // if !bytes.Equal(b, tt.want.body) {
127 | // t.Errorf("length %d != %d", len(tt.want.body), len(b))
128 | // t.Errorf("body %s != %s", tt.want.body, b)
129 | // }
130 | //}
131 | if tt.want.msg != nil {
132 | b, _ := deframe(b)
133 | msg := proto.Clone(tt.want.msg)
134 | if err := proto.Unmarshal(b, msg); err != nil {
135 | t.Errorf("%v: %X", err, b)
136 | return
137 | }
138 | diff := cmp.Diff(msg, tt.want.msg, opts...)
139 | if diff != "" {
140 | t.Error(diff)
141 | }
142 | }
143 | })
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/larking/websocket.go:
--------------------------------------------------------------------------------
1 | package larking
2 |
3 | import (
4 | "context"
5 | "net"
6 |
7 | "github.com/gobwas/ws"
8 | "github.com/gobwas/ws/wsutil"
9 | "google.golang.org/grpc"
10 | "google.golang.org/grpc/metadata"
11 | "google.golang.org/protobuf/encoding/protojson"
12 | "google.golang.org/protobuf/proto"
13 | )
14 |
15 | const kindWebsocket = "WEBSOCKET"
16 |
17 | type streamWS struct {
18 | ctx context.Context
19 | conn net.Conn
20 | method *method
21 | header metadata.MD
22 | trailer metadata.MD
23 | params params
24 | recvN int
25 | sendN int
26 | sentHeader bool
27 | }
28 |
29 | func (s *streamWS) SetHeader(md metadata.MD) error {
30 | if !s.sentHeader {
31 | s.header = metadata.Join(s.header, md)
32 | }
33 | return nil
34 |
35 | }
36 | func (s *streamWS) SendHeader(md metadata.MD) error {
37 | if s.sentHeader {
38 | return nil // already sent?
39 | }
40 | // TODO: headers?
41 | s.sentHeader = true
42 | return nil
43 | }
44 |
45 | func (s *streamWS) SetTrailer(md metadata.MD) {
46 | s.sentHeader = true
47 | s.trailer = metadata.Join(s.trailer, md)
48 | }
49 |
50 | func (s *streamWS) Context() context.Context {
51 | sts := &serverTransportStream{s, s.method.name}
52 | return grpc.NewContextWithServerTransportStream(s.ctx, sts)
53 | }
54 |
55 | func (s *streamWS) SendMsg(v interface{}) error {
56 | s.sendN += 1
57 | reply := v.(proto.Message)
58 | //ctx := s.ctx
59 |
60 | cur := reply.ProtoReflect()
61 | for _, fd := range s.method.resp {
62 | cur = cur.Mutable(fd).Message()
63 | }
64 | msg := cur.Interface()
65 |
66 | // TODO: contentType check?
67 | b, err := protojson.Marshal(msg)
68 | if err != nil {
69 | return err
70 | }
71 |
72 | if err := wsutil.WriteServerMessage(s.conn, ws.OpText, b); err != nil {
73 | return err
74 | }
75 | return nil
76 | }
77 |
78 | func (s *streamWS) RecvMsg(m interface{}) error {
79 | s.recvN += 1
80 | args := m.(proto.Message)
81 |
82 | if s.method.hasBody {
83 | cur := args.ProtoReflect()
84 | for _, fd := range s.method.body {
85 | cur = cur.Mutable(fd).Message()
86 | }
87 |
88 | msg := cur.Interface()
89 |
90 | b, _, err := wsutil.ReadClientData(s.conn)
91 | if err != nil {
92 | return err
93 | }
94 |
95 | // TODO: contentType check?
96 | // What marshalling options should we support?
97 | if err := protojson.Unmarshal(b, msg); err != nil {
98 | return err
99 | }
100 | }
101 |
102 | if s.recvN == 1 {
103 | if err := s.params.set(args); err != nil {
104 | return err
105 | }
106 | }
107 | return nil
108 | }
109 |
--------------------------------------------------------------------------------
/larking/websocket_test.go:
--------------------------------------------------------------------------------
1 | package larking
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "io"
7 | "net"
8 | "net/http"
9 | "testing"
10 | "time"
11 |
12 | "github.com/gobwas/ws"
13 | "github.com/gobwas/ws/wsutil"
14 | "github.com/google/go-cmp/cmp"
15 | "golang.org/x/sync/errgroup"
16 | "google.golang.org/grpc"
17 | "google.golang.org/protobuf/encoding/protojson"
18 | "google.golang.org/protobuf/proto"
19 | "google.golang.org/protobuf/testing/protocmp"
20 | "larking.io/api/testpb"
21 | )
22 |
23 | type wsHeader http.Header
24 |
25 | func (h wsHeader) WriteTo(w io.Writer) (int64, error) {
26 | var buf bytes.Buffer
27 | if err := http.Header(h).Write(&buf); err != nil {
28 | return 0, err
29 | }
30 | return io.Copy(w, &buf)
31 | }
32 |
33 | func TestWebsocket(t *testing.T) {
34 | // Create test server.
35 | fs := &testpb.UnimplementedChatRoomServer{}
36 | o := &overrides{}
37 |
38 | var g errgroup.Group
39 | defer func() {
40 | if err := g.Wait(); err != nil {
41 | t.Fatal(err)
42 | }
43 | }()
44 | mux, err := NewMux(
45 | UnaryServerInterceptorOption(o.unary()),
46 | StreamServerInterceptorOption(o.stream()),
47 | )
48 | if err != nil {
49 | t.Fatal(err)
50 | }
51 | mux.RegisterService(&testpb.ChatRoom_ServiceDesc, fs)
52 |
53 | s, err := NewServer(mux)
54 | if err != nil {
55 | t.Fatal(err)
56 | }
57 |
58 | lis, err := net.Listen("tcp", "localhost:0")
59 | if err != nil {
60 | t.Fatalf("failed to listen: %v", err)
61 | }
62 | defer lis.Close()
63 |
64 | g.Go(func() (err error) {
65 | if err := s.Serve(lis); err != nil && err != http.ErrServerClosed {
66 | return err
67 | }
68 | return nil
69 | })
70 | defer func() {
71 | if err := s.Shutdown(context.Background()); err != nil {
72 | t.Fatal(err)
73 | }
74 | }()
75 |
76 | cmpOpts := cmp.Options{protocmp.Transform()}
77 | var unaryStreamDesc = &grpc.StreamDesc{
78 | ClientStreams: false,
79 | ServerStreams: false,
80 | }
81 |
82 | tests := []struct {
83 | name string
84 | desc *grpc.StreamDesc
85 | path string
86 | method string
87 | client []interface{}
88 | server []interface{}
89 | }{{
90 | name: "unary",
91 | desc: unaryStreamDesc,
92 | path: "/v1/rooms/chat",
93 | method: "/larking.testpb.ChatRoom/Chat",
94 | client: []interface{}{
95 | in{
96 | msg: &testpb.ChatMessage{
97 | Text: "hello",
98 | },
99 | },
100 | out{
101 | msg: &testpb.ChatMessage{
102 | Text: "world",
103 | },
104 | },
105 | },
106 | server: []interface{}{
107 | in{
108 | msg: &testpb.ChatMessage{
109 | Name: "rooms/chat", // name added from URL path
110 | Text: "hello",
111 | },
112 | },
113 | out{
114 | msg: &testpb.ChatMessage{
115 | Text: "world",
116 | },
117 | },
118 | },
119 | }}
120 |
121 | for _, tt := range tests {
122 | t.Run(tt.name, func(t *testing.T) {
123 | o.reset(t, "test", tt.server)
124 |
125 | ctx, cancel := context.WithTimeout(testContext(t), time.Minute)
126 | defer cancel()
127 |
128 | urlStr := "ws://" + lis.Addr().String() + tt.path
129 |
130 | // TODO: protocols and headers.
131 | conn, _, _, err := ws.Dialer{
132 | Header: wsHeader(
133 | map[string][]string{
134 | "test": {tt.method},
135 | },
136 | ),
137 | }.Dial(ctx, urlStr)
138 | if err != nil {
139 | t.Fatal(err)
140 | }
141 | defer conn.Close()
142 |
143 | for i := 0; i < len(tt.client); i++ {
144 | switch typ := tt.client[i].(type) {
145 | case in:
146 | b, err := protojson.Marshal(typ.msg)
147 | if err != nil {
148 | t.Fatal(err)
149 | }
150 | if err := wsutil.WriteClientMessage(conn, ws.OpText, b); err != nil {
151 | t.Fatal(err)
152 | }
153 | t.Log("write", string(b))
154 |
155 | case out:
156 | b, op, err := wsutil.ReadServerData(conn)
157 | if err != nil {
158 | t.Fatal(op, err)
159 | }
160 | t.Log("read", string(b))
161 |
162 | out := proto.Clone(typ.msg)
163 | if err := protojson.Unmarshal(b, out); err != nil {
164 | t.Fatal(err)
165 | }
166 | diff := cmp.Diff(out, typ.msg, cmpOpts...)
167 | if diff != "" {
168 | t.Fatal(diff)
169 | }
170 | }
171 | }
172 | })
173 | }
174 | }
175 |
--------------------------------------------------------------------------------