Skip to content

Commit

Permalink
[tcgc] support emit code model (#1912)
Browse files Browse the repository at this point in the history
fix: #1892

There are three ways to get the code model after TCGC conversion:
1. Use TCGC as an emitter to emit the code model: tsp compile .
--emit=@azure-tools/typespec-client-generator-core
2. Output the TCGC code model when used by language’s emitter: set
`exportTCGCoutput` to true when `createSdkContext`
3. In Azure playground, select
@azure-tools/typespec-client-generator-core emitter
  • Loading branch information
tadelesh authored Dec 4, 2024
1 parent 41806ca commit 8b98b5a
Show file tree
Hide file tree
Showing 22 changed files with 4,989 additions and 287 deletions.
7 changes: 7 additions & 0 deletions .chronus/changes/tcgc_output-2024-10-26-15-38-13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

support emit code model
1 change: 1 addition & 0 deletions packages/typespec-azure-playground-website/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ await renderReactPlayground({
emitterViewers: {
"@typespec/openapi3": [SwaggerUIViewer],
"@azure-tools/typespec-autorest": [SwaggerUIViewer],
"@azure-tools/typespec-client-generator-core": [SwaggerUIViewer],
},
importConfig: {
useShim: true,
Expand Down
59 changes: 59 additions & 0 deletions packages/typespec-client-generator-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,65 @@ TypeSpec Data Plane Generation library
npm install @azure-tools/typespec-client-generator-core
```

## Usage

1. Via the command line

```bash
tsp compile . --emit=@azure-tools/typespec-client-generator-core
```

2. Via the config

```yaml
emit:
- "@azure-tools/typespec-client-generator-core"
```
The config can be extended with options as follows:
```yaml
emit:
- "@azure-tools/typespec-client-generator-core"
options:
"@azure-tools/typespec-client-generator-core":
option: value
```
## Emitter options
### `generate-protocol-methods`

**Type:** `boolean`

### `generate-convenience-methods`

**Type:** `boolean`

### `package-name`

**Type:** `string`

### `flatten-union-as-enum`

**Type:** `boolean`

### `api-version`

**Type:** `string`

### `examples-directory`

**Type:** `string`

### `examples-dir`

**Type:** `string`

### `emitter-name`

**Type:** `string`

## Decorators

### Azure.ClientGenerator.Core
Expand Down
3 changes: 2 additions & 1 deletion packages/typespec-client-generator-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
],
"dependencies": {
"change-case": "~5.4.4",
"pluralize": "^8.0.0"
"pluralize": "^8.0.0",
"yaml": "~2.5.1"
},
"peerDependencies": {
"@azure-tools/typespec-azure-core": "workspace:~",
Expand Down
122 changes: 122 additions & 0 deletions packages/typespec-client-generator-core/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
createDiagnosticCollector,
EmitContext,
emitFile,
Program,
resolvePath,
} from "@typespec/compiler";
import { stringify } from "yaml";
import { defaultDecoratorsAllowList } from "./configs.js";
import { handleClientExamples } from "./example.js";
import {
SdkContext,
SdkEmitterOptions,
SdkHttpOperation,
SdkServiceOperation,
TCGCContext,
} from "./interfaces.js";
import { parseEmitterName } from "./internal-utils.js";
import { getSdkPackage } from "./package.js";

export function createTCGCContext(program: Program, emitterName?: string): TCGCContext {
const diagnostics = createDiagnosticCollector();
return {
program,
emitterName: diagnostics.pipe(
parseEmitterName(program, emitterName ?? program.emitters[0]?.metadata?.name),
),
diagnostics: diagnostics.diagnostics,
originalProgram: program,
__clientToParameters: new Map(),
__tspTypeToApiVersions: new Map(),
__clientToApiVersionClientDefaultValue: new Map(),
previewStringRegex: /-preview$/,
disableUsageAccessPropagationToBase: false,
__pagedResultSet: new Set(),
};
}

interface VersioningStrategy {
readonly strategy?: "ignore";
readonly previewStringRegex?: RegExp; // regex to match preview versions
}

export interface CreateSdkContextOptions {
readonly versioning?: VersioningStrategy;
additionalDecorators?: string[];
disableUsageAccessPropagationToBase?: boolean; // this flag is for some languages that has no need to generate base model, but generate model with composition
exportTCGCoutput?: boolean; // this flag is for emitter to export TCGC output as yaml file
}

export async function createSdkContext<
TOptions extends Record<string, any> = SdkEmitterOptions,
TServiceOperation extends SdkServiceOperation = SdkHttpOperation,
>(
context: EmitContext<TOptions>,
emitterName?: string,
options?: CreateSdkContextOptions,
): Promise<SdkContext<TOptions, TServiceOperation>> {
const diagnostics = createDiagnosticCollector();
const protocolOptions = true; // context.program.getLibraryOptions("generate-protocol-methods");
const convenienceOptions = true; // context.program.getLibraryOptions("generate-convenience-methods");
const generateProtocolMethods = context.options["generate-protocol-methods"] ?? protocolOptions;
const generateConvenienceMethods =
context.options["generate-convenience-methods"] ?? convenienceOptions;
const tcgcContext = createTCGCContext(
context.program,
emitterName ?? context.options["emitter-name"],
);
const sdkContext: SdkContext<TOptions, TServiceOperation> = {
...tcgcContext,
emitContext: context,
sdkPackage: undefined!,
generateProtocolMethods: generateProtocolMethods,
generateConvenienceMethods: generateConvenienceMethods,
packageName: context.options["package-name"],
flattenUnionAsEnum: context.options["flatten-union-as-enum"] ?? true,
apiVersion: options?.versioning?.strategy === "ignore" ? "all" : context.options["api-version"],
examplesDir: context.options["examples-dir"] ?? context.options["examples-directory"],
decoratorsAllowList: [...defaultDecoratorsAllowList, ...(options?.additionalDecorators ?? [])],
previewStringRegex: options?.versioning?.previewStringRegex || tcgcContext.previewStringRegex,
disableUsageAccessPropagationToBase: options?.disableUsageAccessPropagationToBase ?? false,
};
sdkContext.sdkPackage = diagnostics.pipe(getSdkPackage(sdkContext));
for (const client of sdkContext.sdkPackage.clients) {
diagnostics.pipe(await handleClientExamples(sdkContext, client));
}
sdkContext.diagnostics = sdkContext.diagnostics.concat(diagnostics.diagnostics);

if (options?.exportTCGCoutput) {
await exportTCGCOutput(sdkContext);
}
return sdkContext;
}

async function exportTCGCOutput(context: SdkContext) {
await emitFile(context.program, {
path: resolvePath(context.emitContext.emitterOutputDir, "tcgc-output.yaml"),
content: stringify(
context.sdkPackage,
(k, v) => {
if (typeof k === "string" && k.startsWith("__")) {
return undefined; // skip keys starting with "__" from the output
}
if (k === "scheme") {
return undefined; // remove credential schema
}
if (k === "rawExample") {
return undefined; // remove raw example
}
return v;
},
{ lineWidth: 0 },
),
});
}

export async function $onEmit(context: EmitContext<SdkEmitterOptions>) {
if (!context.program.compilerOptions.noEmit) {
const sdkContext = await createSdkContext(context);
await exportTCGCOutput(sdkContext);
}
}
77 changes: 0 additions & 77 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
DecoratorContext,
DecoratorExpressionNode,
DecoratorFunction,
EmitContext,
Enum,
EnumMember,
Interface,
Expand All @@ -17,7 +16,6 @@ import {
SyntaxKind,
Type,
Union,
createDiagnosticCollector,
getDiscriminator,
getNamespaceFullName,
getProjectedName,
Expand All @@ -42,20 +40,14 @@ import {
ProtocolAPIDecorator,
UsageDecorator,
} from "../generated-defs/Azure.ClientGenerator.Core.js";
import { defaultDecoratorsAllowList } from "./configs.js";
import { handleClientExamples } from "./example.js";
import {
AccessFlags,
LanguageScopes,
SdkClient,
SdkContext,
SdkEmitterOptions,
SdkHttpOperation,
SdkInitializationType,
SdkMethodParameter,
SdkModelPropertyType,
SdkOperationGroup,
SdkServiceOperation,
TCGCContext,
UsageFlags,
} from "./interfaces.js";
Expand All @@ -66,10 +58,8 @@ import {
getValidApiVersion,
isAzureCoreTspModel,
negationScopesKey,
parseEmitterName,
} from "./internal-utils.js";
import { createStateSymbol, reportDiagnostic } from "./lib.js";
import { getSdkPackage } from "./package.js";
import { getLibraryName } from "./public-utils.js";
import { getSdkEnum, getSdkModel, getSdkUnion } from "./types.js";

Expand Down Expand Up @@ -655,73 +645,6 @@ export function listOperationsInOperationGroup(
return operations;
}

export function createTCGCContext(program: Program, emitterName: string): TCGCContext {
const diagnostics = createDiagnosticCollector();
return {
program,
emitterName: diagnostics.pipe(parseEmitterName(program, emitterName)),
diagnostics: diagnostics.diagnostics,
originalProgram: program,
__clientToParameters: new Map(),
__tspTypeToApiVersions: new Map(),
__clientToApiVersionClientDefaultValue: new Map(),
previewStringRegex: /-preview$/,
disableUsageAccessPropagationToBase: false,
__pagedResultSet: new Set(),
};
}

interface VersioningStrategy {
readonly strategy?: "ignore";
readonly previewStringRegex?: RegExp; // regex to match preview versions
}

export interface CreateSdkContextOptions {
readonly versioning?: VersioningStrategy;
additionalDecorators?: string[];
disableUsageAccessPropagationToBase?: boolean; // this flag is for some languages that has no need to generate base model, but generate model with composition
}

export async function createSdkContext<
TOptions extends Record<string, any> = SdkEmitterOptions,
TServiceOperation extends SdkServiceOperation = SdkHttpOperation,
>(
context: EmitContext<TOptions>,
emitterName?: string,
options?: CreateSdkContextOptions,
): Promise<SdkContext<TOptions, TServiceOperation>> {
const diagnostics = createDiagnosticCollector();
const protocolOptions = true; // context.program.getLibraryOptions("generate-protocol-methods");
const convenienceOptions = true; // context.program.getLibraryOptions("generate-convenience-methods");
const generateProtocolMethods = context.options["generate-protocol-methods"] ?? protocolOptions;
const generateConvenienceMethods =
context.options["generate-convenience-methods"] ?? convenienceOptions;
const tcgcContext = createTCGCContext(
context.program,
(emitterName ?? context.program.emitters[0]?.metadata?.name)!,
);
const sdkContext: SdkContext<TOptions, TServiceOperation> = {
...tcgcContext,
emitContext: context,
sdkPackage: undefined!,
generateProtocolMethods: generateProtocolMethods,
generateConvenienceMethods: generateConvenienceMethods,
packageName: context.options["package-name"],
flattenUnionAsEnum: context.options["flatten-union-as-enum"] ?? true,
apiVersion: options?.versioning?.strategy === "ignore" ? "all" : context.options["api-version"],
examplesDir: context.options["examples-dir"] ?? context.options["examples-directory"],
decoratorsAllowList: [...defaultDecoratorsAllowList, ...(options?.additionalDecorators ?? [])],
previewStringRegex: options?.versioning?.previewStringRegex || tcgcContext.previewStringRegex,
disableUsageAccessPropagationToBase: options?.disableUsageAccessPropagationToBase ?? false,
};
sdkContext.sdkPackage = diagnostics.pipe(getSdkPackage(sdkContext));
for (const client of sdkContext.sdkPackage.clients) {
diagnostics.pipe(await handleClientExamples(sdkContext, client));
}
sdkContext.diagnostics = sdkContext.diagnostics.concat(diagnostics.diagnostics);
return sdkContext;
}

const protocolAPIKey = createStateSymbol("protocolAPI");

export const $protocolAPI: ProtocolAPIDecorator = (
Expand Down
1 change: 1 addition & 0 deletions packages/typespec-client-generator-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./context.js";
export * from "./decorators.js";
export * from "./interfaces.js";
export * from "./lib.js";
Expand Down
1 change: 1 addition & 0 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface SdkEmitterOptions {
*/
"examples-directory"?: string;
"examples-dir"?: string;
"emitter-name"?: string;
}

export interface SdkClient {
Expand Down
21 changes: 20 additions & 1 deletion packages/typespec-client-generator-core/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler";
import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler";
import { SdkEmitterOptions } from "./interfaces.js";

const EmitterOptionsSchema: JSONSchemaType<SdkEmitterOptions> = {
type: "object",
additionalProperties: true,
properties: {
"generate-protocol-methods": { type: "boolean", nullable: true },
"generate-convenience-methods": { type: "boolean", nullable: true },
"package-name": { type: "string", nullable: true },
"flatten-union-as-enum": { type: "boolean", nullable: true },
"api-version": { type: "string", nullable: true },
"examples-directory": { type: "string", nullable: true },
"examples-dir": { type: "string", nullable: true },
"emitter-name": { type: "string", nullable: true },
},
};

export const $lib = createTypeSpecLibrary({
name: "@azure-tools/typespec-client-generator-core",
Expand Down Expand Up @@ -229,6 +245,9 @@ export const $lib = createTypeSpecLibrary({
},
},
},
emitter: {
options: EmitterOptionsSchema as JSONSchemaType<SdkEmitterOptions>,
},
});

const { reportDiagnostic, createDiagnostic, createStateSymbol } = $lib;
Expand Down
Loading

0 comments on commit 8b98b5a

Please sign in to comment.