Skip to content

Commit 1990a3f

Browse files
authored
Support templated strings in APIView (#7940)
* Initial work to support StringTemplates. * Closes #7930 * Bump version and add changelog.
1 parent c52fb20 commit 1990a3f

6 files changed

Lines changed: 164 additions & 29 deletions

File tree

tools/apiview/emitters/typespec-apiview/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Release History
22

3+
## Version 0.4.7 (03-22-2024)
4+
Support TypeSpec string templates.
5+
Fix display issue with templated aliases.
6+
Ensure alias statements end with semicolon.
7+
38
## Version 0.4.6 (03-08-2024)
49
Support CrossLanguagePackageId.
510

tools/apiview/emitters/typespec-apiview/package-lock.json

Lines changed: 2 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/apiview/emitters/typespec-apiview/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@azure-tools/typespec-apiview",
3-
"version": "0.4.6",
3+
"version": "0.4.7",
44
"author": "Microsoft Corporation",
55
"description": "Library for emitting APIView token files from TypeSpec",
66
"homepage": "https://github.com/Azure/azure-sdk-tools",

tools/apiview/emitters/typespec-apiview/src/apiview.ts

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,29 @@ import {
88
EnumMemberNode,
99
EnumSpreadMemberNode,
1010
EnumStatementNode,
11+
Expression,
1112
getNamespaceFullName,
1213
getSourceLocation,
1314
IdentifierNode,
1415
InterfaceStatementNode,
1516
IntersectionExpressionNode,
16-
listServices,
1717
MemberExpressionNode,
1818
ModelExpressionNode,
1919
ModelPropertyNode,
2020
ModelSpreadPropertyNode,
2121
ModelStatementNode,
2222
Namespace,
2323
navigateProgram,
24-
NoTarget,
2524
NumericLiteralNode,
2625
OperationSignatureDeclarationNode,
2726
OperationSignatureReferenceNode,
2827
OperationStatementNode,
2928
Program,
3029
ScalarStatementNode,
3130
StringLiteralNode,
31+
StringTemplateExpressionNode,
32+
StringTemplateHeadNode,
33+
StringTemplateSpanNode,
3234
SyntaxKind,
3335
TemplateArgumentNode,
3436
TemplateParameterDeclarationNode,
@@ -43,7 +45,6 @@ import { ApiViewDiagnostic, ApiViewDiagnosticLevel } from "./diagnostic.js";
4345
import { ApiViewNavigation } from "./navigation.js";
4446
import { generateId, NamespaceModel } from "./namespace-model.js";
4547
import { LIB_VERSION } from "./version.js";
46-
import { reportDiagnostic } from "./lib.js";
4748

4849
const WHITESPACE = " ";
4950

@@ -383,6 +384,7 @@ export class ApiView {
383384
this.namespaceStack.push(obj.id.sv);
384385
this.keyword("alias", false, true);
385386
this.typeDeclaration(obj.id.sv, this.namespaceStack.value(), true);
387+
this.tokenizeTemplateParameters(obj.templateParameters);
386388
this.punctuation("=", true, true);
387389
this.tokenize(obj.value);
388390
this.namespaceStack.pop();
@@ -633,10 +635,83 @@ export class ApiView {
633635
this.tokenize(obj.argument);
634636
}
635637
break;
638+
case SyntaxKind.StringTemplateExpression:
639+
obj = node as StringTemplateExpressionNode;
640+
const stringValue = this.buildTemplateString(obj);
641+
const multiLine = stringValue.includes("\n");
642+
// single line case
643+
if (!multiLine) {
644+
this.stringLiteral(stringValue);
645+
break;
646+
}
647+
// otherwise multiline case
648+
const lines = stringValue.split("\n");
649+
this.punctuation(`"""`);
650+
this.newline();
651+
this.indent();
652+
for (const line of lines) {
653+
this.literal(line);
654+
this.newline();
655+
}
656+
this.deindent();
657+
this.punctuation(`"""`);
658+
break;
659+
case SyntaxKind.StringTemplateSpan:
660+
obj = node as StringTemplateSpanNode;
661+
this.punctuation("${", false, false);
662+
this.tokenize(obj.expression);
663+
this.punctuation("}", false, false);
664+
this.tokenize(obj.literal);
665+
break;
666+
case SyntaxKind.StringTemplateHead:
667+
case SyntaxKind.StringTemplateMiddle:
668+
case SyntaxKind.StringTemplateTail:
669+
obj = node as StringTemplateHeadNode;
670+
this.literal(obj.value);
671+
break;
672+
default:
673+
// All Projection* cases should fail here...
674+
throw new Error(`Case "${SyntaxKind[node.kind].toString()}" not implemented`);
675+
}
676+
}
677+
678+
private buildExpressionString(node: Expression) {
679+
switch (node.kind) {
680+
case SyntaxKind.StringLiteral:
681+
return `"${(node as StringLiteralNode).value}"`;
682+
case SyntaxKind.NumericLiteral:
683+
return (node as NumericLiteralNode).value.toString();
684+
case SyntaxKind.BooleanLiteral:
685+
return (node as BooleanLiteralNode).value.toString();
686+
case SyntaxKind.StringTemplateExpression:
687+
return this.buildTemplateString(node as StringTemplateExpressionNode);
688+
case SyntaxKind.VoidKeyword:
689+
return "void";
690+
case SyntaxKind.NeverKeyword:
691+
return "never";
692+
case SyntaxKind.TypeReference:
693+
const obj = node as TypeReferenceNode;
694+
switch (obj.target.kind) {
695+
case SyntaxKind.Identifier:
696+
return (obj.target as IdentifierNode).sv;
697+
case SyntaxKind.MemberExpression:
698+
return this.getFullyQualifiedIdentifier(obj.target as MemberExpressionNode);
699+
}
700+
break;
636701
default:
637-
// All Projection* cases should fall in here...
638-
throw new Error(`Case "${node.kind.toString()}" not implemented`);
702+
throw new Error(`Unsupported expression kind: ${SyntaxKind[node.kind]}`);
703+
//unsupported ArrayExpressionNode | MemberExpressionNode | ModelExpressionNode | TupleExpressionNode | UnionExpressionNode | IntersectionExpressionNode | TypeReferenceNode | ValueOfExpressionNode | AnyKeywordNode;
704+
}
705+
}
706+
707+
/** Constructs a single string with template markers. */
708+
private buildTemplateString(node: StringTemplateExpressionNode): string {
709+
let result = node.head.value;
710+
for (const span of node.spans) {
711+
result += "${" + this.buildExpressionString(span.expression) + "}";
712+
result += span.literal.value;
639713
}
714+
return result;
640715
}
641716

642717
private tokenizeModelStatement(node: ModelStatementNode) {
@@ -891,6 +966,7 @@ export class ApiView {
891966
}
892967
for (const node of model.aliases.values()) {
893968
this.tokenize(node);
969+
this.punctuation(";");
894970
this.blankLines(1);
895971
}
896972
this.endGroup();
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const LIB_VERSION = "0.4.6";
1+
export const LIB_VERSION = "0.4.7";

tools/apiview/emitters/typespec-apiview/test/apiview.test.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,41 @@ describe("apiview: tests", () => {
243243
species: string;
244244
}
245245
246-
alias Creature = Animal
246+
alias Creature = Animal;
247247
}
248248
`;
249249
const apiview = await apiViewFor(input, {});
250250
const actual = apiViewText(apiview);
251251
compare(expect, actual, 9);
252252
validateDefinitionIds(apiview);
253253
});
254-
});
254+
255+
it("templated alias", async () => {
256+
const input = `
257+
@TypeSpec.service( { title: "Test", version: "1" } )
258+
namespace Azure.Test {
259+
model Animal {
260+
species: string;
261+
}
262+
263+
alias Template<T extends valueof string> = "Foo \${T} bar";
264+
}
265+
`;
266+
const expect = `
267+
namespace Azure.Test {
268+
model Animal {
269+
species: string;
270+
}
271+
272+
alias Template<T extends valueof string> = "Foo \${T} bar";
273+
}
274+
`;
275+
const apiview = await apiViewFor(input, {});
276+
const actual = apiViewText(apiview);
277+
compare(expect, actual, 9);
278+
validateDefinitionIds(apiview);
279+
});
280+
});
255281

256282
describe("augment decorators", () => {
257283
it("simple augment", async () => {
@@ -693,4 +719,50 @@ describe("apiview: tests", () => {
693719
validateDefinitionIds(apiview);
694720
});
695721
});
722+
723+
describe("string templates", () => {
724+
it("templates", async () => {
725+
const input = `
726+
@TypeSpec.service( { title: "Test", version: "1" } )
727+
namespace Azure.Test {
728+
alias myconst = "foobar";
729+
model Person {
730+
simple: "Simple \${123} end";
731+
multiline: """
732+
Multi
733+
\${123}
734+
\${true}
735+
line
736+
""";
737+
ref: "Ref this alias \${myconst} end";
738+
template: Template<"custom">;
739+
}
740+
alias Template<T extends valueof string> = "Foo \${T} bar";
741+
}`;
742+
743+
const expect = `
744+
namespace Azure.Test {
745+
model Person {
746+
simple: "Simple \${123} end";
747+
multiline: """
748+
Multi
749+
\${123}
750+
\${true}
751+
line
752+
""";
753+
ref: "Ref this alias \${myconst} end";
754+
template: Template<"custom">;
755+
}
756+
757+
alias myconst = "foobar";
758+
759+
alias Template<T extends valueof string> = "Foo \${T} bar";
760+
}
761+
`;
762+
const apiview = await apiViewFor(input, {});
763+
const lines = apiViewText(apiview);
764+
compare(expect, lines, 9);
765+
validateDefinitionIds(apiview);
766+
});
767+
});
696768
});

0 commit comments

Comments
 (0)