Skip to content

Commit

Permalink
Update protoplugin example
Browse files Browse the repository at this point in the history
  • Loading branch information
timostamm committed Sep 25, 2023
1 parent 0abb4b3 commit 1cfd8fe
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18.15.0
v18.18.0
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ test-protobuf: $(BUILD)/protobuf-test packages/protobuf-test/jest.config.js
test-protoplugin: $(BUILD)/protoplugin-test packages/protoplugin-test/jest.config.js
npm run -w packages/protoplugin-test test

.PHONY: test-protoplugin-example
test-protoplugin-example: $(BUILD)/protoplugin-example
npm run -w packages/protoplugin-example test

.PHONY: test-conformance
test-conformance: $(BIN)/conformance_test_runner $(BUILD)/protobuf-conformance
cd packages/protobuf-conformance \
Expand Down
7 changes: 5 additions & 2 deletions docs/writing_plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,9 +535,12 @@ const enumVal: FooEnum | undefined = findCustomEnumOption(descMessage, 50001);
## Testing
There is no specific formula for how to test an individual plugin. The official [protoc-gen-es](../packages/protoc-gen-es) plugin is extensively tested and could provide some guidance. In addition, there are examples of testing the framework in the [protoplugin-test package](../packages/protoplugin-test).
We recommend to test generated code just like handwritten code. Identify a
representative protobuf file for your use case, generate code, and then simply
run tests against the generated code.
A helpful suggestion is to generate specific use cases that are expected for your plugin and then test that the output is what is expected. It is a bit difficult to test discrete functionality so verifying the output is valid is the recommended approach. To test the transpilation process specifically, it may be helpful to generate your own JavaScript and declaration files and then verify that they match transpilation.
If you implement your own generator functions for the `js` and `dts` targets,
we recommend to run all tests against both.
## Examples
Expand Down
54 changes: 34 additions & 20 deletions packages/protoplugin-example/README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
# Protoplugin Example

This directory contains an example plugin, which shows how to work with the
plugin framework. It also contains a separate webpage which shows the generated files working with a remote server.
This example shows how to write a custom plugin. We generate [Twirp](https://twitchtv.github.io/twirp/docs/spec_v7.html)
clients from service definitions in Protobuf files.

The code generation logic for the actual plugin is located in [`protoc-gen-twirp-es.ts`](src/protoc-gen-twirp-es.ts).

The sample plugin generates a [Twirp](https://twitchtv.github.io/twirp/docs/spec_v7.html) client from service
definitions in Protobuf files. The Twirp client uses base types generated from
[`@bufbuild/protobuf-es`](https://www.npmjs.com/package/@bufbuild/protoc-gen-es).
## Run the example

From the project root, first install and build all required packages:
You will need [Node](https://nodejs.org/en/download/) in version 18.17.0 or later installed.
Download the example project and install its dependencies:

```shell
npm install -w packages/protoplugin-example
npm run -w packages/protobuf build
npm run -w packages/protoplugin build
npm run -w packages/protoc-gen-es build
curl -L https://github.com/bufbuild/protobuf-es/archive/refs/heads/main.zip > protobuf-es-main.zip
unzip protobuf-es-main.zip 'protobuf-es-main/packages/protoplugin-example/*'

cd protobuf-es-main/packages/protoplugin-example
npm install
```

To see the client in action:

```shell
npm start
```

Open http://127.0.0.1:3000/ in your browser.


To re-generate code:

```shell
npx buf generate buf.build/connectrpc/eliza
```

Next, `cd` into the example directory and build:
This will generate the [Eliza module](https://buf.build/connectrpc/eliza) from the Buf Schema Registry (BSR).
You can change this path to generate additional files locally or from the BSR.

Test the generated code:

```shell
cd packages/protoplugin-example
npm run build
npm test
```

To run the plugin (i.e. generate files), use the following command. This will generate files based on the
[Eliza module](https://buf.build/connectrpc/eliza) in the Buf Schema Registry (BSR). You can change this path to generate
additional files locally or from the BSR.
## About this example

`npx buf generate buf.build/connectrpc/eliza`
This example is a starting point - we encourage you to try it out and experiment.

To run the example webpage and see the generated code in action:
Take a look at the code generation logic in [protoc-gen-twirp-es.ts](./src/protoc-gen-twirp-es.ts),
and at [buf.gen.yaml](./buf.gen.yaml) for how it is invoked.

`npm run start`
4 changes: 3 additions & 1 deletion packages/protoplugin-example/buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ plugins:
opt: target=ts
out: src/gen
- plugin: twirp-es
path: ./src/protoc-gen-twirp-es.ts
# Override the path to the plugin binary.
# See https://buf.build/docs/configuration/v1/buf-gen-yaml#path
path: ["tsx", "./src/protoc-gen-twirp-es.ts"]
opt: target=ts
out: src/gen
7 changes: 2 additions & 5 deletions packages/protoplugin-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@
"scripts": {
"clean": "rm -rf src/gen",
"build": "../../node_modules/typescript/bin/tsc --noEmit",
"start": "npx esbuild src/index.ts --serve=localhost:3000 --servedir=www --outdir=www --bundle --global-name=eliza"
"start": "npx esbuild src/index.ts --serve=localhost:3000 --servedir=www --outdir=www --bundle --global-name=eliza",
"test": "node --loader tsx --test test/*.ts"
},
"author": "Buf",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
},
"dependencies": {
"@bufbuild/buf": "^1.25.0",
"@bufbuild/protobuf": "^1.3.1",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021-2023 Buf Technologies, Inc.
// Copyright 2022-2023 The Connect Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down
67 changes: 67 additions & 0 deletions packages/protoplugin-example/test/generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2021-2023 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as assert from "node:assert/strict";
import { describe, it, mock } from "node:test";
import { ElizaServiceClient } from "../src/gen/connectrpc/eliza/v1/eliza_twirp";
import { SayRequest } from "../src/gen/connectrpc/eliza/v1/eliza_pb";

describe("custom plugin", async () => {
it("should generate client class", () => {
assert.equal(typeof ElizaServiceClient, "function");
const client = new ElizaServiceClient("https://example.com");
assert.ok(client !== undefined);
});
describe("generated client", () => {
it("should should take argument in constructor", () => {
const client = new ElizaServiceClient("https://example.com");
assert.ok(client !== undefined);
assert.equal(
(client as unknown as Record<string, unknown>).baseUrl,
"https://example.com",
);
});
it("should have method for unary RPC", () => {
const client = new ElizaServiceClient("https://example.com");
assert.equal(typeof client.say, "function");
});
it("should use fetch", async (t) => {
let fetch = mock.fn<typeof globalThis.fetch>(globalThis.fetch);
globalThis.fetch = fetch;
t.after(() => fetch.mock.restore());
fetch.mock.mockImplementationOnce(
async () =>
new Response('{"sentence":"ho"}', {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);
const client = new ElizaServiceClient("https://example.com");
const res = await client.say(new SayRequest({ sentence: "hi" }));
assert.equal(res.sentence, "ho");
assert.equal(fetch.mock.callCount(), 1);
const [argInput, argInit] = fetch.mock.calls[0].arguments;
assert.strictEqual(
argInput,
"https://example.com/connectrpc.eliza.v1.ElizaService/Say",
);
assert.equal(argInit?.method, "POST");
assert.equal(
new Headers(argInit?.headers).get("Content-Type"),
"application/json",
);
assert.equal(argInit?.body, '{"sentence":"hi"}');
});
});
});
7 changes: 5 additions & 2 deletions packages/protoplugin-example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"files": ["src/protoc-gen-twirp-es.ts", "src/index.ts"],
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts", "test/*.ts"],
"compilerOptions": {
"target": "es2017",
"esModuleInterop": false,
"forceConsistentCasingInFileNames": true,
"strict": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true
}
Expand Down

0 comments on commit 1cfd8fe

Please sign in to comment.