Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autorest emit all files programatically #972

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
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
195 changes: 132 additions & 63 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,90 +91,137 @@ 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) {
let projectedProgram;
if (record.projections.length > 0) {
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)!;
if (versions.length === 1 && versions[0].version === undefined) {
const context: AutorestEmitterContext = {
program,
outputFile: resolveOutputFile(
program,
projectedService,
services.length > 1,
options,
record.version
),
service: projectedService,
version: record.version,
outputFile: resolveOutputFile(program, service, services.length > 1, options),
service: service,
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);
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;
}
Loading