diff --git a/.chronus/changes/autorest-emitt-all-programmatically-2024-5-7-9-28-38.md b/.chronus/changes/autorest-emitt-all-programmatically-2024-5-7-9-28-38.md new file mode 100644 index 0000000000..9f73b1e8f7 --- /dev/null +++ b/.chronus/changes/autorest-emitt-all-programmatically-2024-5-7-9-28-38.md @@ -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 \ No newline at end of file diff --git a/packages/typespec-autorest/src/emit.ts b/packages/typespec-autorest/src/emit.ts index 7287fb9183..565d6dcab9 100644 --- a/packages/typespec-autorest/src/emit.ts +++ b/packages/typespec-autorest/src/emit.ts @@ -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, @@ -21,6 +21,11 @@ import { getOpenAPIForService, sortOpenAPIDocument, } from "./openapi.js"; +import type { + AutorestEmitterResult, + AutorestServiceRecord, + AutorestVersionedServiceRecord, +} from "./types.js"; import { AutorestEmitterContext } from "./utils.js"; /** @@ -48,17 +53,34 @@ const defaultOptions = { } as const; export async function $onEmit(context: EmitContext) { - 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"], @@ -69,37 +91,37 @@ export async function $onEmit(context: EmitContext) { 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 { + 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; @@ -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"; } diff --git a/packages/typespec-autorest/src/index.ts b/packages/typespec-autorest/src/index.ts index df5549aa52..fe82d89f4d 100644 --- a/packages/typespec-autorest/src/index.ts +++ b/packages/typespec-autorest/src/index.ts @@ -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, diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index 11b7de2a2f..8c46f7e7f8 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -31,7 +31,6 @@ import { Operation, Program, Scalar, - SourceFile, StringLiteral, StringTemplate, SyntaxKind, @@ -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 { @@ -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 @@ -360,6 +350,7 @@ export async function getOpenAPIForService( } }) .filter((x) => x) as any, + outputFile: context.outputFile, }; function resolveHost( @@ -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, diff --git a/packages/typespec-autorest/src/types.ts b/packages/typespec-autorest/src/types.ts new file mode 100644 index 0000000000..0b06c87441 --- /dev/null +++ b/packages/typespec-autorest/src/types.ts @@ -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; +}