Skip to content

Commit

Permalink
[protobuf] Add support for expanding template names. (#2861)
Browse files Browse the repository at this point in the history
There is a bug in the protobuf emitter currently where we use the base
name of a template when emitting corresponding models. This patch adds
support for extracting an instance name, using friendlyName if it is
available or otherwise constructing a name by prepending the name of the
argument types to the template's name.

Closes #2857

---------

Co-authored-by: Will Temple <will@wtemple.net>
  • Loading branch information
witemple-msft and willmtemple committed Feb 1, 2024
1 parent 32050cd commit a3d6acf
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/purple-jokes-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typespec/protobuf": minor
---

Added support for template name expansion to the protobuf emitter.
55 changes: 53 additions & 2 deletions packages/protobuf/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler";
import {
createTypeSpecLibrary,
DiagnosticTarget,
JSONSchemaType,
paramMessage,
Program,
} from "@typespec/compiler";

/**
* Options that the Protobuf emitter accepts.
Expand Down Expand Up @@ -149,6 +155,12 @@ export const TypeSpecProtobufLibrary = createTypeSpecLibrary({
default: "anonymous models cannot be used in Protobuf messages",
},
},
"unspeakable-template-argument": {
severity: "error",
messages: {
default: paramMessage`template ${"name"} cannot be converted to a Protobuf message because it has an unspeakable argument (try using the '@friendlyName' decorator on the template)`,
},
},
package: {
severity: "error",
messages: {
Expand All @@ -159,7 +171,46 @@ export const TypeSpecProtobufLibrary = createTypeSpecLibrary({
emitter: { options: EmitterOptionsSchema },
});

export const { reportDiagnostic } = TypeSpecProtobufLibrary;
export type Diagnostic = Parameters<typeof TypeSpecProtobufLibrary.reportDiagnostic>[1];

const __DIAGNOSTIC_CACHE = new WeakMap<Program, Map<DiagnosticTarget, Set<Diagnostic["code"]>>>();

function getDiagnosticCache(program: Program) {
let cache = __DIAGNOSTIC_CACHE.get(program);
if (!cache) {
cache = new Map();
__DIAGNOSTIC_CACHE.set(program, cache);
}
return cache;
}

function getAppliedCodesForTarget(program: Program, target: DiagnosticTarget) {
const cache = getDiagnosticCache(program);
let codes = cache.get(target);
if (!codes) {
codes = new Set();
cache.set(target, codes);
}
return codes;
}

export const reportDiagnostic = Object.assign(TypeSpecProtobufLibrary.reportDiagnostic, {
/**
* Report a TypeSpec protobuf diagnostic, but only once per target per diagnostic code.
*
* This is useful in situations where a function that reports a recoverable diagnostic may be called multiple times.
*/
once: function (program: Program, diagnostic: Diagnostic & { target: DiagnosticTarget }) {
const codes = getAppliedCodesForTarget(program, diagnostic.target);

if (codes.has(diagnostic.code)) {
return;
}

codes.add(diagnostic.code);
TypeSpecProtobufLibrary.reportDiagnostic(program, diagnostic);
},
});

export type TypeSpecProtobufLibrary = typeof TypeSpecProtobufLibrary;

Expand Down
42 changes: 38 additions & 4 deletions packages/protobuf/src/transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
formatDiagnostic,
getDoc,
getEffectiveModelType,
getFriendlyName,
getTypeName,
Interface,
IntrinsicType,
isDeclaredInNamespace,
isTemplateInstance,
Model,
ModelProperty,
Namespace,
Expand Down Expand Up @@ -346,7 +348,7 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P
return ref(extern[1]);
}

return ref(model.name);
return ref(getModelName(model));
}

/**
Expand Down Expand Up @@ -460,7 +462,7 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P

const effectiveModel = computeEffectiveModel(m, capitalize(operation.name) + "Response");
if (effectiveModel) {
return ref(effectiveModel.name);
return ref(getModelName(effectiveModel));
}

reportDiagnostic(program, {
Expand Down Expand Up @@ -507,8 +509,10 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P
});
return unreachable("anonymous model");
}

visitModel(t, relativeSource);
return ref(t.name);

return ref(getModelName(t));
case "Enum":
visitEnum(t);
return ref(t.name);
Expand Down Expand Up @@ -683,13 +687,43 @@ function tspToProto(program: Program, emitterOptions: ProtobufEmitterOptions): P
function toMessage(model: Model): ProtoMessageDeclaration {
return {
kind: "message",
name: model.name,
name: getModelName(model),
reservations: program.stateMap(state.reserve).get(model),
declarations: [...model.properties.values()].map((f) => toMessageBodyDeclaration(f, model)),
doc: getDoc(program, model),
};
}

function getModelName(model: Model): string {
const friendlyName = getFriendlyName(program, model);

if (friendlyName) return capitalize(friendlyName);

const templateArguments = isTemplateInstance(model) ? model.templateMapper!.args : [];

const prefix = templateArguments
.map(function getTypePrefixName(arg, idx) {
if ("name" in arg && typeof arg.name === "string" && arg.name !== "")
return capitalize(arg.name!);
else {
reportDiagnostic.once(program, {
code: "unspeakable-template-argument",
// TODO-WILL - I'd rather attach the diagnostic to the template argument, but it's the best I can do for
// now to attach it to the model itself.
target: model,
format: {
name: model.name,
},
});

return `T${idx}`;
}
})
.join("");

return prefix + capitalize(model.name);
}

/**
* @param property - the ModelProperty to convert
* @returns a corresponding declaration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/test/main.tsp:15:7 - error @typespec/protobuf/unspeakable-template-argument: template Templated cannot be converted to a Protobuf message because it has an unspeakable argument (try using the '@friendlyName' decorator on the template)
/test/main.tsp:12:40 - error @typespec/protobuf/anonymous-model: anonymous models cannot be used in Protobuf messages
18 changes: 18 additions & 0 deletions packages/protobuf/test/scenarios/templates-error/input/main.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import "@typespec/protobuf";

using TypeSpec.Protobuf;

@package({
name: "com.azure.Test",
})
namespace Test;

@Protobuf.service
interface Service {
foo(...Templated<string>): Templated<{}>;
}

model Templated<T> {
@field(1) value: T;
@field(2) other: int32;
}
27 changes: 27 additions & 0 deletions packages/protobuf/test/scenarios/templates/input/main.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import "@typespec/protobuf";

using TypeSpec.Protobuf;

@package({
name: "com.azure.Test",
})
namespace Test;

@Protobuf.service
interface Service {
foo(...Templated<string>): Templated<uint32>;

listOperation(...List<string>): List<uint32>;

sameTemplate(...Templated<string>): void;
}

@friendlyName("{name}List", T)
model List<T> {
@field(1) values: T[];
}

model Templated<T> {
@field(1) value: T;
@field(2) other: int32;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Generated by Microsoft TypeSpec

syntax = "proto3";

package com.azure.Test;

import "google/protobuf/empty.proto";

message StringTemplated {
string value = 1;
int32 other = 2;
}

message Uint32Templated {
uint32 value = 1;
int32 other = 2;
}

message StringList {
repeated string values = 1;
}

message Uint32List {
repeated uint32 values = 1;
}

service Service {
rpc Foo(StringTemplated) returns (Uint32Templated);
rpc ListOperation(StringList) returns (Uint32List);
rpc SameTemplate(StringTemplated) returns (google.protobuf.Empty);
}

0 comments on commit a3d6acf

Please sign in to comment.