diff --git a/docs/writing_plugins.md b/docs/writing_plugins.md index 969e2d76c..5d15fddce 100644 --- a/docs/writing_plugins.md +++ b/docs/writing_plugins.md @@ -21,10 +21,7 @@ Code generator plugins can be created using the npm packages [@bufbuild/protoplu - [Why use `f.import()`?](#why-use-fimport) - [Exporting](#exporting) - [Parsing plugin options](#parsing-plugin-options) - - [Reading custom options](#reading-custom-options) - - [Scalar options](#scalar-options) - - [Message options](#message-options) - - [Enum options](#enum-options) + - [Using custom Protobuf options](#using-custom-protobuf-options) - [Testing](#testing) - [Examples](#examples) @@ -376,163 +373,81 @@ parseOption(key: string, value: string | undefined): void; This function will be invoked by the framework, passing in any key/value pairs that it does not recognize from its pre-defined list. -### Reading custom options +### Using custom Protobuf options -Protobuf-ES does not yet provide support for extensions, neither in general as pertaining to proto2 nor with custom options in proto3. However, in the interim, there are convenience functions for retrieving any custom options specified in your .proto files. These are provided as a temporary utility until full extension support is implemented. There are three functions depending on the structure of the custom option desired (scalar, message, or enum): +Your plugin can support custom Protobuf options to modify the code it generates. -#### Scalar Options +As an example, let's use a custom service option to provide a default host. +Here is how this option would be used: -Custom options of a scalar type (`boolean`, `string`, `int32`, etc.) can be retrieved via the `findCustomScalarOption` function. It returns a type corresponding to the given `scalarType` parameter. For example, if `ScalarType.STRING` is passed, the return type will be a `string`. If the option is not found, it returns `undefined`. +```protobuf +syntax = "proto3"; +import "customoptions/default_host.proto"; +package connectrpc.eliza.v1; -```ts -function findCustomScalarOption( - desc: AnyDesc, - extensionNumber: number, - scalarType: T -): ScalarValue | undefined; -``` - -`AnyDesc` represents any of the `DescXXX` objects such as `DescFile`, `DescEnum`, `DescMessage`, etc. The `extensionNumber` parameter represents the extension number of the custom options field definition. +service MyService { -The `scalarType` parameter is the type of the custom option you are searching for. `ScalarType` is an enum that represents all possible scalar types in the Protobuf grammar + // Set the default host for this service with our custom option. + option (customoptions.default_host) = "https://demo.connectrpc.com/"; -For example, given the following: - -```proto -extend google.protobuf.MessageOptions { - optional int32 foo_message_option = 50001; -} -extend google.protobuf.FieldOptions { - optional string foo_field_option = 50002; + // ... } - -message FooMessage { - option (foo_message_option) = 1234; - - int32 foo = 1 [(foo_field_option) = "test"]; -} -``` - -The values of these options can be retrieved as follows: - -```ts -const msgVal = findCustomScalarOption(descMessage, 50001, ScalarType.INT32); // 1234 - -const fieldVal = findCustomScalarOption(descField, 50002, ScalarType.STRING); // "test" ``` -#### Message Options - -Custom options of a more complex message type can be retrieved via the `findCustomMessageOption` function. It returns a concrete type with fields populated corresponding to the values set in the proto file. - -```ts -export function findCustomMessageOption>( - desc: AnyDesc, - extensionNumber: number, - msgType: MessageType -): T | undefined { -``` +Custom options are extensions to one of the options messages defined in +`google/protobuf/descriptor.proto`. Here is how we can define the option we are +using above: -The `msgType` parameter represents the type of the message you are searching for. +```protobuf +// customoptions/default_host.proto +syntax = "proto3"; +import "google/protobuf/descriptor.proto"; +package customoptions; -For example, given the following proto files: - -```proto -// custom_options.proto - -extend google.protobuf.MethodOptions { - optional ServiceOptions service_method_option = 50007; -} - -message ServiceOptions { - int32 foo = 1; - string bar = 2; - oneof qux { - string quux = 3; - } - repeated string many = 4; - map mapping = 5; +extend google.protobuf.ServiceOptions { + // We extend the ServiceOptions message, so that other proto files can import + // this file, and set the option on a service declaration. + optional string default_host = 1001; } ``` -```proto -// service.proto +You can learn more about custom options in the [language guide](https://protobuf.dev/programming-guides/proto3/#customoptions). -import "custom_options.proto"; - -service FooService { - rpc Get(GetRequest) returns (GetResponse) { - option (service_method_option) = { - foo: 567, - bar: "Some string", - quux: "Oneof string", - many: ["a", "b", "c"], - mapping: [{key: "testKey", value: "testVal"}] - }; - } -} -``` - -You can retrieve the options using a generated type by first generating the file which defines the custom option type. Then, import and pass this type to the `findCustomMessageOption` function. +First, we need to generate code for our custom option. This will generate a file +`customoptions/default_host_pb.ts` with a new export - our extension: ```ts -import { ServiceOptions } from "./gen/proto/custom_options_pb.js"; - -const option = findCustomMessageOption(method, 50007, ServiceOptions) - -console.log(option); -/* - * { - * foo: 567, - * bar: "Some string", - * quux: "Oneof string", - * many: ["a", "b", "c"], - * mapping: [{key: "testKey", value: "testVal"}] - * } - */ - ``` +import { ServiceOptions, Extension } from "@bufbuild/protobuf"; -Note that `repeated` and `map` values are only supported within a custom message option. They are not supported as option types independently. If you have need to use a custom option that is `repeated` or is of type `map`, it is recommended to use a message option as a wrapper. - - -#### Enum Options - -Custom options of an enum type can be retrieved via the `findCustomEnumOption` function. It returns a `number` corresponding to the `enum` value set in the option. - -```ts -export function findCustomEnumOption( - desc: AnyDesc, - extensionNumber: number -): number | undefined { +export const default_host: Extension = ... ``` -The returned number can then be coerced into the concrete `enum` type. The `enum` type just needs to be generated ahead of time much like the example in `findCustomMessageOption`. +Now we can utilize this extension to read custom options in our plugin: -For example, given the following: - -```proto -extend google.protobuf.MessageOptions { - optional FooEnum foo_enum_option = 50001; -} - -enum FooEnum { - UNDEFINED = 0; - ACTIVE = 1; - INACTIVE = 2; -} - -message FooMessage { - option (foo_enum_option) = ACTIVE; - - string name = 1; +```ts +import type { GeneratedFile } from "@bufbuild/protoplugin/ecmascript"; +import { ServiceOptions, ServiceDesc, hasExtension, getExtension } from "@bufbuild/protobuf"; +import { default_host } from "./customoptions/default_host_pb.js"; + +function generateService(desc: ServiceDesc, f: GeneratedFile) { + // The protobuf message google.protobuf.ServiceOptions contains our custom + // option. + const serviceOptions: ServiceOptions | undefined = service.proto.options; + + // Let's see if our option was set: + if (serviceOptions && hasExtension(serviceOption, default_host)) { + const value = getExtension(serviceOption, default_host); // "https://demo.connectrpc.com/" + // Our option was set, we can use it here. + } } ``` -The value of this option can be retrieved as follows: +Custom options can be set on any Protobuf element. They can be simple singular +string fields as the one above, but also repeated fields, message fields, etc. + +Take a look at our [plugin example](https://github.com/bufbuild/protobuf-es/tree/main/packages/protoplugin-example) +to see the custom option above in action, and run the code yourself. -```ts -const enumVal: FooEnum | undefined = findCustomEnumOption(descMessage, 50001); // FooEnum.ACTIVE -``` ## Testing diff --git a/packages/protoplugin-example/package.json b/packages/protoplugin-example/package.json index c09069688..c3db76dae 100644 --- a/packages/protoplugin-example/package.json +++ b/packages/protoplugin-example/package.json @@ -6,8 +6,7 @@ "build": "../../node_modules/typescript/bin/tsc --noEmit", "start": "npx esbuild src/index.ts --serve=localhost:3000 --servedir=www --outdir=www --bundle --global-name=eliza", "test": "tsx --test test/*.ts", - "pregenerate": "rm -rf src/gen", - "generate": "buf generate buf.build/connectrpc/eliza" + "generate": "buf generate proto" }, "license": "Apache-2.0", "dependencies": { diff --git a/packages/protoplugin-example/proto/connectrpc/eliza.proto b/packages/protoplugin-example/proto/connectrpc/eliza.proto new file mode 100644 index 000000000..e370dcffc --- /dev/null +++ b/packages/protoplugin-example/proto/connectrpc/eliza.proto @@ -0,0 +1,41 @@ +// Copyright 2021-2024 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. + +syntax = "proto3"; + +import "customoptions/default_host.proto"; + +package connectrpc.eliza.v1; + +// This is a modified copy of ElizaService from https://buf.build/connectrpc/eliza +service ElizaService { + + // Set the default host for this service with a custom option. + // Comment this line out and run `npm run generate` to see how the generated + // code changes. + option (customoptions.default_host) = "https://demo.connectrpc.com/"; + + // Say is a unary RPC. Eliza responds to the prompt with a single sentence. + rpc Say(SayRequest) returns (SayResponse) { + option idempotency_level = NO_SIDE_EFFECTS; + } +} +// SayRequest is a single-sentence request. +message SayRequest { + string sentence = 1; +} +// SayResponse is a single-sentence response. +message SayResponse { + string sentence = 1; +} diff --git a/packages/protoplugin-example/proto/customoptions/default_host.proto b/packages/protoplugin-example/proto/customoptions/default_host.proto new file mode 100644 index 000000000..410a494fb --- /dev/null +++ b/packages/protoplugin-example/proto/customoptions/default_host.proto @@ -0,0 +1,30 @@ +// Copyright 2021-2024 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. + +syntax = "proto3"; + +import "google/protobuf/descriptor.proto"; + +package customoptions; + +extend google.protobuf.ServiceOptions { + + // An example for a custom option. Custom options are extensions to one of + // the options messages defined in descriptor.proto. + // + // We extend the ServiceOptions message, so that other proto files can import + // this file, and set the option on a service declaration. + optional string default_host = 1001; + +} diff --git a/packages/protoplugin-example/src/gen/connectrpc/eliza/v1/eliza_pb.ts b/packages/protoplugin-example/src/gen/connectrpc/eliza/v1/eliza_pb.ts deleted file mode 100644 index bda5095f6..000000000 --- a/packages/protoplugin-example/src/gen/connectrpc/eliza/v1/eliza_pb.ts +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2021-2024 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. - -// @generated by protoc-gen-es v1.6.0 with parameter "target=ts" -// @generated from file connectrpc/eliza/v1/eliza.proto (package connectrpc.eliza.v1, syntax proto3) -/* eslint-disable */ -// @ts-nocheck - -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; -import { Message, proto3 } from "@bufbuild/protobuf"; - -/** - * SayRequest is a single-sentence request. - * - * @generated from message connectrpc.eliza.v1.SayRequest - */ -export class SayRequest extends Message { - /** - * @generated from field: string sentence = 1; - */ - sentence = ""; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "connectrpc.eliza.v1.SayRequest"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "sentence", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): SayRequest { - return new SayRequest().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): SayRequest { - return new SayRequest().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): SayRequest { - return new SayRequest().fromJsonString(jsonString, options); - } - - static equals(a: SayRequest | PlainMessage | undefined, b: SayRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(SayRequest, a, b); - } -} - -/** - * SayResponse is a single-sentence response. - * - * @generated from message connectrpc.eliza.v1.SayResponse - */ -export class SayResponse extends Message { - /** - * @generated from field: string sentence = 1; - */ - sentence = ""; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "connectrpc.eliza.v1.SayResponse"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "sentence", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): SayResponse { - return new SayResponse().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): SayResponse { - return new SayResponse().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): SayResponse { - return new SayResponse().fromJsonString(jsonString, options); - } - - static equals(a: SayResponse | PlainMessage | undefined, b: SayResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(SayResponse, a, b); - } -} - -/** - * ConverseRequest is a single sentence request sent as part of a - * back-and-forth conversation. - * - * @generated from message connectrpc.eliza.v1.ConverseRequest - */ -export class ConverseRequest extends Message { - /** - * @generated from field: string sentence = 1; - */ - sentence = ""; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "connectrpc.eliza.v1.ConverseRequest"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "sentence", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): ConverseRequest { - return new ConverseRequest().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): ConverseRequest { - return new ConverseRequest().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): ConverseRequest { - return new ConverseRequest().fromJsonString(jsonString, options); - } - - static equals(a: ConverseRequest | PlainMessage | undefined, b: ConverseRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(ConverseRequest, a, b); - } -} - -/** - * ConverseResponse is a single sentence response sent in answer to a - * ConverseRequest. - * - * @generated from message connectrpc.eliza.v1.ConverseResponse - */ -export class ConverseResponse extends Message { - /** - * @generated from field: string sentence = 1; - */ - sentence = ""; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "connectrpc.eliza.v1.ConverseResponse"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "sentence", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): ConverseResponse { - return new ConverseResponse().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): ConverseResponse { - return new ConverseResponse().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): ConverseResponse { - return new ConverseResponse().fromJsonString(jsonString, options); - } - - static equals(a: ConverseResponse | PlainMessage | undefined, b: ConverseResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(ConverseResponse, a, b); - } -} - -/** - * IntroduceRequest asks Eliza to introduce itself to the named user. - * - * @generated from message connectrpc.eliza.v1.IntroduceRequest - */ -export class IntroduceRequest extends Message { - /** - * @generated from field: string name = 1; - */ - name = ""; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "connectrpc.eliza.v1.IntroduceRequest"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): IntroduceRequest { - return new IntroduceRequest().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): IntroduceRequest { - return new IntroduceRequest().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): IntroduceRequest { - return new IntroduceRequest().fromJsonString(jsonString, options); - } - - static equals(a: IntroduceRequest | PlainMessage | undefined, b: IntroduceRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(IntroduceRequest, a, b); - } -} - -/** - * IntroduceResponse is one sentence of Eliza's introductory monologue. - * - * @generated from message connectrpc.eliza.v1.IntroduceResponse - */ -export class IntroduceResponse extends Message { - /** - * @generated from field: string sentence = 1; - */ - sentence = ""; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "connectrpc.eliza.v1.IntroduceResponse"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "sentence", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): IntroduceResponse { - return new IntroduceResponse().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): IntroduceResponse { - return new IntroduceResponse().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): IntroduceResponse { - return new IntroduceResponse().fromJsonString(jsonString, options); - } - - static equals(a: IntroduceResponse | PlainMessage | undefined, b: IntroduceResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(IntroduceResponse, a, b); - } -} - diff --git a/packages/protoplugin-example/src/gen/connectrpc/eliza_pb.ts b/packages/protoplugin-example/src/gen/connectrpc/eliza_pb.ts new file mode 100644 index 000000000..1b775dba4 --- /dev/null +++ b/packages/protoplugin-example/src/gen/connectrpc/eliza_pb.ts @@ -0,0 +1,100 @@ +// Copyright 2021-2024 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. + +// @generated by protoc-gen-es v1.6.0 with parameter "target=ts" +// @generated from file connectrpc/eliza.proto (package connectrpc.eliza.v1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; +import { Message, proto3 } from "@bufbuild/protobuf"; + +/** + * SayRequest is a single-sentence request. + * + * @generated from message connectrpc.eliza.v1.SayRequest + */ +export class SayRequest extends Message { + /** + * @generated from field: string sentence = 1; + */ + sentence = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "connectrpc.eliza.v1.SayRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "sentence", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SayRequest { + return new SayRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SayRequest { + return new SayRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SayRequest { + return new SayRequest().fromJsonString(jsonString, options); + } + + static equals(a: SayRequest | PlainMessage | undefined, b: SayRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(SayRequest, a, b); + } +} + +/** + * SayResponse is a single-sentence response. + * + * @generated from message connectrpc.eliza.v1.SayResponse + */ +export class SayResponse extends Message { + /** + * @generated from field: string sentence = 1; + */ + sentence = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "connectrpc.eliza.v1.SayResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "sentence", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): SayResponse { + return new SayResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): SayResponse { + return new SayResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): SayResponse { + return new SayResponse().fromJsonString(jsonString, options); + } + + static equals(a: SayResponse | PlainMessage | undefined, b: SayResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(SayResponse, a, b); + } +} + diff --git a/packages/protoplugin-example/src/gen/connectrpc/eliza/v1/eliza_twirp.ts b/packages/protoplugin-example/src/gen/connectrpc/eliza_twirp.ts similarity index 79% rename from packages/protoplugin-example/src/gen/connectrpc/eliza/v1/eliza_twirp.ts rename to packages/protoplugin-example/src/gen/connectrpc/eliza_twirp.ts index 57fd07726..e068552e7 100644 --- a/packages/protoplugin-example/src/gen/connectrpc/eliza/v1/eliza_twirp.ts +++ b/packages/protoplugin-example/src/gen/connectrpc/eliza_twirp.ts @@ -13,7 +13,7 @@ // limitations under the License. // @generated by protoc-gen-twirp-es v1.6.0 with parameter "target=ts" -// @generated from file connectrpc/eliza/v1/eliza.proto (package connectrpc.eliza.v1, syntax proto3) +// @generated from file connectrpc/eliza.proto (package connectrpc.eliza.v1, syntax proto3) /* eslint-disable */ // @ts-nocheck @@ -21,20 +21,13 @@ import type { JsonValue, Message } from "@bufbuild/protobuf"; import { SayRequest, SayResponse } from "./eliza_pb.js"; /** - * ElizaService provides a way to talk to Eliza, a port of the DOCTOR script - * for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at - * the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the - * superficiality of human-computer communication. DOCTOR simulates a - * psychotherapist, and is commonly found as an Easter egg in emacs - * distributions. + * This is a modified copy of ElizaService from https://buf.build/connectrpc/eliza * * @generated from service connectrpc.eliza.v1.ElizaService */ export class ElizaServiceClient { - private baseUrl: string = ''; - constructor(url: string) { - this.baseUrl = url; + constructor(private readonly baseUrl = "https://demo.connectrpc.com/") { } async request>( diff --git a/packages/protoplugin-example/src/gen/customoptions/default_host_pb.ts b/packages/protoplugin-example/src/gen/customoptions/default_host_pb.ts new file mode 100644 index 000000000..c2229378f --- /dev/null +++ b/packages/protoplugin-example/src/gen/customoptions/default_host_pb.ts @@ -0,0 +1,36 @@ +// Copyright 2021-2024 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. + +// @generated by protoc-gen-es v1.6.0 with parameter "target=ts" +// @generated from file customoptions/default_host.proto (package customoptions, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { proto3, ServiceOptions } from "@bufbuild/protobuf"; + +/** + * An example for a custom option. Custom options are extensions to one of + * the options messages defined in descriptor.proto. + * + * We extend the ServiceOptions message, so that other proto files can import + * this file, and set the option on a service declaration. + * + * @generated from extension: optional string default_host = 1001; + */ +export const default_host = proto3.makeExtension( + "customoptions.default_host", + ServiceOptions, + { no: 1001, kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, +); + diff --git a/packages/protoplugin-example/src/index.ts b/packages/protoplugin-example/src/index.ts index 4ba26f7f5..e6a617d62 100644 --- a/packages/protoplugin-example/src/index.ts +++ b/packages/protoplugin-example/src/index.ts @@ -12,62 +12,35 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ElizaServiceClient } from "./gen/connectrpc/eliza/v1/eliza_twirp.js"; -import { SayRequest } from "./gen/connectrpc/eliza/v1/eliza_pb.js"; +import { ElizaServiceClient } from "./gen/connectrpc/eliza_twirp"; +import { SayRequest } from "./gen/connectrpc/eliza_pb"; -let introFinished = false; +const client = new ElizaServiceClient(); +const input = document.querySelector("input")!; +const conversationContainer = document.querySelector( + `#conversation-container`, +)!; -const client = new ElizaServiceClient("https://demo.connectrpc.com"); - -// Query for the common elements and cache them. -const containerEl = document.getElementById( - "conversation-container", -) as HTMLDivElement; -const inputEl = document.getElementById("user-input") as HTMLInputElement; - -// Add an event listener to the input so that the user can hit enter and click the Send button -document.getElementById("user-input")?.addEventListener("keyup", (event) => { +document.querySelector("form")!.addEventListener("submit", (event) => { event.preventDefault(); - if (event.key === "Enter") { - document.getElementById("send-button")?.click(); - } + void send(input.value); + input.value = ""; }); -// Adds a node to the DOM representing the conversation with Eliza -function addNode(text: string, sender: string): void { - const divEl = document.createElement("div"); - const pEl = document.createElement("p"); - - const respContainerEl = containerEl.appendChild(divEl); - respContainerEl.className = `${sender}-resp-container`; - - const respTextEl = respContainerEl.appendChild(pEl); - respTextEl.className = "resp-text"; - respTextEl.innerText = text; -} - -async function send() { - const sentence = inputEl.value; - - addNode(sentence, "user"); - - inputEl.value = ""; - - if (introFinished) { - const request = new SayRequest({ - sentence, - }); - - const response = await client.say(request); - - addNode(response.sentence, "eliza"); - } else { - addNode(`Streaming is not supported but we can still chat. OK?`, "eliza"); - - introFinished = true; - } +// Send a sentence to the Eliza service +async function send(sentence: string) { + addConversationPill(sentence, "user"); + const request = new SayRequest({ sentence }); + const response = await client.say(request); + addConversationPill(response.sentence, "eliza"); } -export function handleSend() { - void send(); +// Adds a node to the DOM representing the conversation with Eliza +function addConversationPill(text: string, sender: string): void { + const div = document.createElement("div"); + div.className = `${sender}-resp-container`; + const p = div.appendChild(document.createElement("p")); + p.className = "resp-text"; + p.innerText = text; + conversationContainer.appendChild(div); } diff --git a/packages/protoplugin-example/src/protoc-gen-twirp-es.ts b/packages/protoplugin-example/src/protoc-gen-twirp-es.ts index 1f0340967..f0ee0ef4b 100755 --- a/packages/protoplugin-example/src/protoc-gen-twirp-es.ts +++ b/packages/protoplugin-example/src/protoc-gen-twirp-es.ts @@ -16,9 +16,9 @@ import { createEcmaScriptPlugin, runNodeJs } from "@bufbuild/protoplugin"; import { version } from "../package.json"; -import type { Schema } from "@bufbuild/protoplugin/ecmascript"; -import { localName } from "@bufbuild/protoplugin/ecmascript"; -import { MethodKind } from "@bufbuild/protobuf"; +import { localName, type Schema } from "@bufbuild/protoplugin/ecmascript"; +import { getExtension, hasExtension, MethodKind } from "@bufbuild/protobuf"; +import { default_host } from "./gen/customoptions/default_host_pb.js"; const protocGenTwirpEs = createEcmaScriptPlugin({ name: "protoc-gen-twirp-es", @@ -38,11 +38,22 @@ function generateTs(schema: Schema) { for (const service of file.services) { f.print(f.jsDoc(service)); f.print(f.exportDecl("class", localName(service) + "Client"), " {"); - f.print(" private baseUrl: string = '';"); f.print(); - f.print(" constructor(url: string) {"); - f.print(" this.baseUrl = url;"); - f.print(" }"); + + // To support the custom option we defined in customoptions/default_host.proto, + // we need to generate code for this proto file first. This will generate the + // file customoptions/default_host_pb.ts, which contains the generated extension + // `default_host`. + // Then we use the functions hasExtension() and getExtension() to see whether + // the option is set, and set the value as the default for the constructor argument. + if (service.proto.options && hasExtension(service.proto.options, default_host)) { + const defaultHost = getExtension(service.proto.options, default_host); + f.print(" constructor(private readonly baseUrl = ", f.string(defaultHost), ") {"); + f.print(" }"); + } else { + f.print(" constructor(private readonly baseUrl: string) {"); + f.print(" }"); + } f.print(); f.print(" async request>("); f.print(" service: string,"); @@ -70,19 +81,19 @@ function generateTs(schema: Schema) { f.print(" }"); for (const method of service.methods) { if (method.methodKind === MethodKind.Unary) { - f.print(); - f.print(f.jsDoc(method, " ")); - f.print(" async ", localName(method), "(request: ", method.input, "): Promise<", method.output, "> {"); - f.print(" const promise = this.request("); - f.print(" ", f.string(service.typeName), ","); - f.print(" ", f.string(method.name), ","); - f.print(' "application/json",'); - f.print(" request"); - f.print(" );"); - f.print(" return promise.then(async (data) =>"); - f.print(" ", method.output, ".fromJson(data as ", JsonValue, ")"); - f.print(" );"); - f.print(" }"); + f.print(); + f.print(f.jsDoc(method, " ")); + f.print(" async ", localName(method), "(request: ", method.input, "): Promise<", method.output, "> {"); + f.print(" const promise = this.request("); + f.print(" ", f.string(service.typeName), ","); + f.print(" ", f.string(method.name), ","); + f.print(' "application/json",'); + f.print(" request"); + f.print(" );"); + f.print(" return promise.then(async (data) =>"); + f.print(" ", method.output, ".fromJson(data as ", JsonValue, ")"); + f.print(" );"); + f.print(" }"); } } f.print("}"); diff --git a/packages/protoplugin-example/test/generated.ts b/packages/protoplugin-example/test/generated.ts index 737f3c02d..77ead770a 100644 --- a/packages/protoplugin-example/test/generated.ts +++ b/packages/protoplugin-example/test/generated.ts @@ -14,8 +14,8 @@ import * as assert from "node:assert/strict"; import { describe, it, mock } from "node:test"; -import { ElizaServiceClient } from "../src/gen/connectrpc/eliza/v1/eliza_twirp.js"; -import { SayRequest } from "../src/gen/connectrpc/eliza/v1/eliza_pb.js"; +import { ElizaServiceClient } from "../src/gen/connectrpc/eliza_twirp"; +import { SayRequest } from "../src/gen/connectrpc/eliza_pb"; describe("custom plugin", async () => { it("should generate client class", () => { diff --git a/packages/protoplugin-example/www/index.html b/packages/protoplugin-example/www/index.html index edc3e1521..fe5b8bbfc 100644 --- a/packages/protoplugin-example/www/index.html +++ b/packages/protoplugin-example/www/index.html @@ -18,12 +18,14 @@

Eliza

- - +
+ + +
diff --git a/packages/protoplugin-test/src/custom-options.test.ts b/packages/protoplugin-test/src/custom-options.test.ts index e5d51e559..1e045b0e6 100644 --- a/packages/protoplugin-test/src/custom-options.test.ts +++ b/packages/protoplugin-test/src/custom-options.test.ts @@ -17,7 +17,7 @@ import { createDescriptorSet, getExtension } from "@bufbuild/protobuf"; import { UpstreamProtobuf } from "upstream-protobuf"; import { readFileSync } from "node:fs"; import assert from "node:assert"; -import { uint32_option } from "./gen/file-option_pb"; +import { uint32_option } from "./gen/file-option_pb.js"; describe("custom options", () => { test("can be read via extension", async () => { diff --git a/packages/protoplugin/src/ecmascript/custom-options.ts b/packages/protoplugin/src/ecmascript/custom-options.ts index d82b79eee..d52d56d7a 100644 --- a/packages/protoplugin/src/ecmascript/custom-options.ts +++ b/packages/protoplugin/src/ecmascript/custom-options.ts @@ -22,6 +22,8 @@ import { } from "@bufbuild/protobuf"; /** + * @deprecated Please use extensions instead. + * * Returns the value of a custom option with a scalar type. * * If no option is found, returns undefined. @@ -73,6 +75,8 @@ export function findCustomScalarOption( } /** + * @deprecated Please use extensions instead. + * * Returns the value of a custom message option for the given descriptor and * extension number. * The msgType param is then used to deserialize the message for returning to @@ -102,6 +106,8 @@ export function findCustomMessageOption>( } /** + * @deprecated Please use extensions instead. + * * Returns the value of a custom enum option for the given descriptor and * extension number. * @@ -116,6 +122,8 @@ export function findCustomEnumOption( // prettier-ignore /** + * @deprecated Please use extensions instead. + * * ScalarValue is a conditional type that pairs a ScalarType value with its concrete type. */ type ScalarValue = T extends ScalarType.STRING