Skip to content

Commit

Permalink
Autorest emit all files programatically (#972)
Browse files Browse the repository at this point in the history
Expose an API like we have in openapi3 emitter to emit all services at
all version programmatically
  • Loading branch information
timotheeguerin committed Jun 7, 2024
1 parent a8f0337 commit d179b2a
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-autorest"
---

Add API to programmatically get all the OpenAPI2 documents for all services at all versions in a spec
191 changes: 137 additions & 54 deletions packages/typespec-autorest/src/emit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SdkContext, createSdkContext } from "@azure-tools/typespec-client-generator-core";
import { createSdkContext } from "@azure-tools/typespec-client-generator-core";
import {
EmitContext,
Namespace,
Expand All @@ -21,6 +21,11 @@ import {
getOpenAPIForService,
sortOpenAPIDocument,
} from "./openapi.js";
import type {
AutorestEmitterResult,
AutorestServiceRecord,
AutorestVersionedServiceRecord,
} from "./types.js";
import { AutorestEmitterContext } from "./utils.js";

/**
Expand Down Expand Up @@ -48,17 +53,34 @@ const defaultOptions = {
} as const;

export async function $onEmit(context: EmitContext<AutorestEmitterOptions>) {
const resolvedOptions = { ...defaultOptions, ...context.options };
const tracer = getTracer(context.program);

const options = resolveAutorestOptions(
context.program,
context.emitterOutputDir,
context.options
);
tracer.trace("options", JSON.stringify(options, null, 2));

await emitAllServiceAtAllVersions(context.program, options);
}

export function resolveAutorestOptions(
program: Program,
emitterOutputDir: string,
options: AutorestEmitterOptions
): ResolvedAutorestEmitterOptions {
const resolvedOptions = { ...defaultOptions, ...options };
const armTypesDir = interpolatePath(
resolvedOptions["arm-types-dir"] ?? "{project-root}/../../common-types/resource-management",
{
"project-root": context.program.projectRoot,
"emitter-output-dir": context.emitterOutputDir,
"project-root": program.projectRoot,
"emitter-output-dir": emitterOutputDir,
}
);
const options: ResolvedAutorestEmitterOptions = {
return {
outputFile: resolvedOptions["output-file"],
outputDir: context.emitterOutputDir,
outputDir: emitterOutputDir,
azureResourceProviderFolder: resolvedOptions["azure-resource-provider-folder"],
examplesDirectory: resolvedOptions["examples-directory"],
version: resolvedOptions["version"],
Expand All @@ -69,37 +91,37 @@ export async function $onEmit(context: EmitContext<AutorestEmitterOptions>) {
armTypesDir,
useReadOnlyStatusSchema: resolvedOptions["use-read-only-status-schema"],
};
const tracer = getTracer(context.program);
tracer.trace("options", JSON.stringify(options, null, 2));

const tcgcSdkContext = createSdkContext(context, "@azure-tools/typespec-autorest", {
versionStrategy: "ignore",
});

await emitAllServiceAtAllVersions(context.program, tcgcSdkContext, options);
}

export async function emitAllServiceAtAllVersions(
export async function getAllServicesAtAllVersions(
program: Program,
tcgcSdkContext: SdkContext,
options: ResolvedAutorestEmitterOptions
) {
): Promise<AutorestServiceRecord[]> {
const tcgcSdkContext = createSdkContext(
{ program, options: {} } as any,
"@azure-tools/typespec-autorest",
{
versionStrategy: "ignore",
}
);

const services = listServices(program);
if (services.length === 0) {
services.push({ type: program.getGlobalNamespaceType() });
}

const serviceRecords: AutorestServiceRecord[] = [];
for (const service of services) {
const originalProgram = program;
const versions = buildVersionProjections(program, service.type).filter(
(v) => !options.version || options.version === v.version
);
for (const record of versions) {

if (versions.length === 1 && versions[0].version === undefined) {
let projectedProgram;
if (record.projections.length > 0) {
projectedProgram = program = projectProgram(originalProgram, record.projections);
if (versions[0].projections.length > 0) {
projectedProgram = program = projectProgram(originalProgram, versions[0].projections);
}

const projectedServiceNs: Namespace = projectedProgram
? (projectedProgram.projector.projectedTypes.get(service.type) as Namespace)
: service.type;
Expand All @@ -109,50 +131,111 @@ export async function emitAllServiceAtAllVersions(
: getService(program, projectedServiceNs)!;
const context: AutorestEmitterContext = {
program,
outputFile: resolveOutputFile(
program,
projectedService,
services.length > 1,
options,
record.version
),
outputFile: resolveOutputFile(program, service, services.length > 1, options),
service: projectedService,
version: record.version,
tcgcSdkContext,
};

const result = await getOpenAPIForService(context, options);
if (!program.compilerOptions.noEmit && !program.hasError()) {
// Sort the document
const sortedDocument = sortOpenAPIDocument(result.document);

// Write out the OpenAPI document to the output path
await emitFile(program, {
path: context.outputFile,
content: prettierOutput(JSON.stringify(sortedDocument, null, 2)),
newLine: options.newLine,
serviceRecords.push({
service,
versioned: false,
...result,
});
} else {
const serviceRecord: AutorestVersionedServiceRecord = {
service,
versioned: true,
versions: [],
};
serviceRecords.push(serviceRecord);

for (const record of versions) {
const projectedProgram = (program = projectProgram(originalProgram, record.projections));

const projectedServiceNs: Namespace = projectedProgram
? (projectedProgram.projector.projectedTypes.get(service.type) as Namespace)
: service.type;
const projectedService =
projectedServiceNs === program.getGlobalNamespaceType()
? { type: program.getGlobalNamespaceType() }
: getService(program, projectedServiceNs)!;
const context: AutorestEmitterContext = {
program,
outputFile: resolveOutputFile(
program,
projectedService,
services.length > 1,
options,
record.version
),
service: projectedService,
version: record.version,
tcgcSdkContext,
};
const result = await getOpenAPIForService(context, options);
serviceRecord.versions.push({
...result,
service: projectedService,
version: record.version!,
});
}
}
}

// Copy examples to the output directory
if (options.examplesDirectory && result.operationExamples.length > 0) {
const examplesPath = resolvePath(getDirectoryPath(context.outputFile), "examples");
await program.host.mkdirp(examplesPath);
for (const { examples } of result.operationExamples) {
if (examples) {
for (const { relativePath, file } of Object.values(examples)) {
await emitFile(program, {
path: resolvePath(examplesPath, relativePath),
content: file.text,
newLine: options.newLine,
});
}
}
}
}
return serviceRecords;
}

async function emitAllServiceAtAllVersions(
program: Program,
options: ResolvedAutorestEmitterOptions
) {
const services = await getAllServicesAtAllVersions(program, options);
if (program.compilerOptions.noEmit || program.hasError()) {
return;
}
for (const serviceRecord of services) {
if (serviceRecord.versioned) {
for (const documentRecord of serviceRecord.versions) {
await emitOutput(program, documentRecord, options);
}
} else {
await emitOutput(program, serviceRecord, options);
}
}
}

async function emitOutput(
program: Program,
result: AutorestEmitterResult,
options: ResolvedAutorestEmitterOptions
) {
const sortedDocument = sortOpenAPIDocument(result.document);

// Write out the OpenAPI document to the output path
await emitFile(program, {
path: result.outputFile,
content: prettierOutput(JSON.stringify(sortedDocument, null, 2)),
newLine: options.newLine,
});

// Copy examples to the output directory
if (options.examplesDirectory && result.operationExamples.length > 0) {
const examplesPath = resolvePath(getDirectoryPath(result.outputFile), "examples");
await program.host.mkdirp(examplesPath);
for (const { examples } of result.operationExamples) {
if (examples) {
for (const { relativePath, file } of Object.values(examples)) {
await emitFile(program, {
path: resolvePath(examplesPath, relativePath),
content: file.text,
newLine: options.newLine,
});
}
}
}
}
}
function prettierOutput(output: string) {
return output + "\n";
}
Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-autorest/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "./decorators.js";
export { $onEmit } from "./emit.js";
export { $onEmit, getAllServicesAtAllVersions, resolveAutorestOptions } from "./emit.js";
export { $lib, AutorestEmitterOptions } from "./lib.js";
export {
getOpenAPIForService,
Expand Down
18 changes: 2 additions & 16 deletions packages/typespec-autorest/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
Operation,
Program,
Scalar,
SourceFile,
StringLiteral,
StringTemplate,
SyntaxKind,
Expand Down Expand Up @@ -146,6 +145,7 @@ import {
PrimitiveItems,
Refable,
} from "./openapi2-document.js";
import type { AutorestEmitterResult, LoadedExample } from "./types.js";
import { AutorestEmitterContext, getClientName, resolveOperationId } from "./utils.js";

interface SchemaContext {
Expand Down Expand Up @@ -229,16 +229,6 @@ interface ProcessedSchema extends PendingSchema {
schema: OpenAPI2Schema | undefined;
}

export interface OperationExamples {
readonly operationId: string;
readonly examples: LoadedExample[];
}

export interface AutorestEmitterResult {
readonly document: OpenAPI2Document;
readonly operationExamples: OperationExamples[];
}

export async function getOpenAPIForService(
context: AutorestEmitterContext,
options: AutorestDocumentEmitterOptions
Expand Down Expand Up @@ -360,6 +350,7 @@ export async function getOpenAPIForService(
}
})
.filter((x) => x) as any,
outputFile: context.outputFile,
};

function resolveHost(
Expand Down Expand Up @@ -2302,11 +2293,6 @@ export function sortOpenAPIDocument(doc: OpenAPI2Document): OpenAPI2Document {
return sorted;
}

interface LoadedExample {
readonly relativePath: string;
readonly file: SourceFile;
readonly data: any;
}
async function loadExamples(
host: CompilerHost,
options: AutorestDocumentEmitterOptions,
Expand Down
62 changes: 62 additions & 0 deletions packages/typespec-autorest/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Service, SourceFile } from "@typespec/compiler";
import type { OpenAPI2Document } from "./openapi2-document.js";

/**
* A record containing the the OpenAPI 3 documents corresponding to
* a particular service definition.
*/
export type AutorestServiceRecord =
| AutorestUnversionedServiceRecord
| AutorestVersionedServiceRecord;

export interface AutorestUnversionedServiceRecord extends AutorestEmitterResult {
/** The service that generated this OpenAPI document */
readonly service: Service;

/** Whether the service is versioned */
readonly versioned: false;
}

export interface AutorestVersionedServiceRecord {
/** The service that generated this OpenAPI document */
readonly service: Service;

/** Whether the service is versioned */
readonly versioned: true;

/** The OpenAPI 3 document records for each version of this service */
readonly versions: AutorestVersionedDocumentRecord[];
}

/**
* A record containing an unversioned OpenAPI document and associated metadata.
*/
export interface AutorestVersionedDocumentRecord extends AutorestEmitterResult {
/** The service that generated this OpenAPI document. */
readonly service: Service;

/** The version of the service. Absent if the service is unversioned. */
readonly version: string;
}

export interface OperationExamples {
readonly operationId: string;
readonly examples: LoadedExample[];
}

export interface AutorestEmitterResult {
/** The OpenAPI document*/
readonly document: OpenAPI2Document;

/** The examples */
readonly operationExamples: OperationExamples[];

/** Output file used */
readonly outputFile: string;
}

export interface LoadedExample {
readonly relativePath: string;
readonly file: SourceFile;
readonly data: any;
}

0 comments on commit d179b2a

Please sign in to comment.