Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/protobuf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/protobuf/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DefaultFieldOptions>;
type: ProtoType;
index: number;
Expand Down
14 changes: 14 additions & 0 deletions packages/protobuf/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProtobufEmitterOptions> = {
Expand All @@ -44,6 +52,12 @@ const EmitterOptionsSchema: JSONSchemaType<ProtobufEmitterOptions> = {
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: [],
};
Expand Down
5 changes: 5 additions & 0 deletions packages/protobuf/src/transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/protobuf/src/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ function writeVariant(decl: ProtoEnumVariantDeclaration, indentLevel: number): I
}

function writeField(decl: ProtoFieldDeclaration, indentLevel: number): Iterable<string> {
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"emit-optional": false
}
Original file line number Diff line number Diff line change
@@ -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);
}
20 changes: 20 additions & 0 deletions packages/protobuf/test/scenarios/optional/input/main.tsp
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions packages/protobuf/test/scenarios/optional/options.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"emit-optional": true
}
Original file line number Diff line number Diff line change
@@ -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);
}
Loading