diff --git a/packages/protobuf/README.md b/packages/protobuf/README.md index 01afd6f837b..442fcee93f7 100644 --- a/packages/protobuf/README.md +++ b/packages/protobuf/README.md @@ -54,6 +54,39 @@ If set to `true`, this emitter will not write any files. It will still validate By default, the emitter will create `message` declarations for any models in a namespace decorated with `@package` that have an `@field` decorator on every property. If this option is set to true, this behavior will be disabled, and only messages that are explicitly decorated with `@message` or that are reachable from a service operation will be emitted. +### `emit-optional` + +**Type:** `boolean` + +**Default:** `false` + +When enabled, fields marked as optional in TypeSpec (using `?` syntax or `@optional` decorator) will be emitted with the `optional` keyword in proto3. By default, optional fields are not marked as such in the output. + +Example: + +```typespec +model Message { + @field(1) requiredField: string; + @field(2) optionalField?: int32; +} +``` + +With `emit-optional: false` (default): +```protobuf +message Message { + string requiredField = 1; + int32 optionalField = 2; +} +``` + +With `emit-optional: true`: +```protobuf +message Message { + string requiredField = 1; + optional int32 optionalField = 2; +} +``` + ## Decorators ### TypeSpec.Protobuf diff --git a/packages/protobuf/src/ast.ts b/packages/protobuf/src/ast.ts index 068a62de8e8..2d22dc06060 100644 --- a/packages/protobuf/src/ast.ts +++ b/packages/protobuf/src/ast.ts @@ -262,6 +262,10 @@ export interface ProtoFieldDeclaration extends ProtoDeclarationCommon { * Whether or not the field is repeated (i.e. an array). */ repeated?: boolean; + /** + * Whether or not the field is optional. + */ + optional?: boolean; options?: Partial; type: ProtoType; index: number; diff --git a/packages/protobuf/src/lib.ts b/packages/protobuf/src/lib.ts index 611140ab6aa..cfbf751cc8e 100644 --- a/packages/protobuf/src/lib.ts +++ b/packages/protobuf/src/lib.ts @@ -26,6 +26,14 @@ export interface ProtobufEmitterOptions { * in an interface decoarated with `@service` will be emitted. */ "omit-unreachable-types"?: boolean; + + /** + * Emit optional fields. + * + * When enabled, fields marked as optional in TypeSpec (using `?` or `@optional`) will be emitted with the + * `optional` keyword in proto3. By default, optional fields are not marked as such in the output. + */ + "emit-optional"?: boolean; } const EmitterOptionsSchema: JSONSchemaType = { @@ -44,6 +52,12 @@ const EmitterOptionsSchema: JSONSchemaType = { description: "By default, the emitter will create `message` declarations for any models in a namespace decorated with `@package` that have an `@field` decorator on every property. If this option is set to true, this behavior will be disabled, and only messages that are explicitly decorated with `@message` or that are reachable from a service operation will be emitted.", }, + "emit-optional": { + type: "boolean", + nullable: true, + description: + "When enabled, fields marked as optional in TypeSpec (using `?` or `@optional`) will be emitted with the `optional` keyword in proto3. By default, optional fields are not marked as such in the output.", + }, }, required: [], }; diff --git a/packages/protobuf/src/transform/index.ts b/packages/protobuf/src/transform/index.ts index 9c8402f0383..770cbcff292 100644 --- a/packages/protobuf/src/transform/index.ts +++ b/packages/protobuf/src/transform/index.ts @@ -833,6 +833,11 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P // Determine if the property type is an array if (isArray(property.type)) field.repeated = true; + // Determine if the property is optional (when emit-optional option is enabled) + if (emitterOptions["emit-optional"] && property.optional) { + field.optional = true; + } + return field; } diff --git a/packages/protobuf/src/write.ts b/packages/protobuf/src/write.ts index 3e58853d057..c08dc69ba82 100644 --- a/packages/protobuf/src/write.ts +++ b/packages/protobuf/src/write.ts @@ -178,7 +178,7 @@ function writeVariant(decl: ProtoEnumVariantDeclaration, indentLevel: number): I } function writeField(decl: ProtoFieldDeclaration, indentLevel: number): Iterable { - const prefix = decl.repeated ? "repeated " : ""; + const prefix = decl.repeated ? "repeated " : decl.optional ? "optional " : ""; const output = prefix + `${writeType(decl.type)} ${decl.name} = ${decl.index};`; return writeDocumentationCommentFlexible(decl, output, indentLevel); diff --git a/packages/protobuf/test/scenarios/optional-disabled/input/main.tsp b/packages/protobuf/test/scenarios/optional-disabled/input/main.tsp new file mode 100644 index 00000000000..60499c813cf --- /dev/null +++ b/packages/protobuf/test/scenarios/optional-disabled/input/main.tsp @@ -0,0 +1,20 @@ +import "@typespec/protobuf"; + +using Protobuf; + +@package() +namespace Test; + +@Protobuf.service +interface Service { + foo(...Input): Output; +} + +model Input { + @field(1) testInputField: string; +} + +model Output { + @field(1) testOutputField?: int32; + @field(2) secondField?: string; +} diff --git a/packages/protobuf/test/scenarios/optional-disabled/options.json b/packages/protobuf/test/scenarios/optional-disabled/options.json new file mode 100644 index 00000000000..872390b6de0 --- /dev/null +++ b/packages/protobuf/test/scenarios/optional-disabled/options.json @@ -0,0 +1,3 @@ +{ + "emit-optional": false +} diff --git a/packages/protobuf/test/scenarios/optional-disabled/output/@typespec/protobuf/main.proto b/packages/protobuf/test/scenarios/optional-disabled/output/@typespec/protobuf/main.proto new file mode 100644 index 00000000000..87bafb7f6ae --- /dev/null +++ b/packages/protobuf/test/scenarios/optional-disabled/output/@typespec/protobuf/main.proto @@ -0,0 +1,16 @@ +// Generated by Microsoft TypeSpec + +syntax = "proto3"; + +message Input { + string testInputField = 1; +} + +message Output { + int32 testOutputField = 1; + string secondField = 2; +} + +service Service { + rpc Foo(Input) returns (Output); +} diff --git a/packages/protobuf/test/scenarios/optional/input/main.tsp b/packages/protobuf/test/scenarios/optional/input/main.tsp new file mode 100644 index 00000000000..60499c813cf --- /dev/null +++ b/packages/protobuf/test/scenarios/optional/input/main.tsp @@ -0,0 +1,20 @@ +import "@typespec/protobuf"; + +using Protobuf; + +@package() +namespace Test; + +@Protobuf.service +interface Service { + foo(...Input): Output; +} + +model Input { + @field(1) testInputField: string; +} + +model Output { + @field(1) testOutputField?: int32; + @field(2) secondField?: string; +} diff --git a/packages/protobuf/test/scenarios/optional/options.json b/packages/protobuf/test/scenarios/optional/options.json new file mode 100644 index 00000000000..8c567601213 --- /dev/null +++ b/packages/protobuf/test/scenarios/optional/options.json @@ -0,0 +1,3 @@ +{ + "emit-optional": true +} diff --git a/packages/protobuf/test/scenarios/optional/output/@typespec/protobuf/main.proto b/packages/protobuf/test/scenarios/optional/output/@typespec/protobuf/main.proto new file mode 100644 index 00000000000..9422965c0b2 --- /dev/null +++ b/packages/protobuf/test/scenarios/optional/output/@typespec/protobuf/main.proto @@ -0,0 +1,16 @@ +// Generated by Microsoft TypeSpec + +syntax = "proto3"; + +message Input { + string testInputField = 1; +} + +message Output { + optional int32 testOutputField = 1; + optional string secondField = 2; +} + +service Service { + rpc Foo(Input) returns (Output); +}