diff --git a/.chronus/changes/fix-deprecated-operation-import-2026-6-5-18-8-6.md b/.chronus/changes/fix-deprecated-operation-import-2026-6-5-18-8-6.md new file mode 100644 index 00000000000..c7c3a8d8f2e --- /dev/null +++ b/.chronus/changes/fix-deprecated-operation-import-2026-6-5-18-8-6.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/openapi3" +--- + +Fix import of `deprecated: true` on OpenAPI3 operations to generate `#deprecated "deprecated"` directive in converted TypeSpec output. diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-operation.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-operation.ts index fa704c611f3..1eaf24e3441 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-operation.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-operation.ts @@ -6,19 +6,23 @@ import { } from "../interfaces.js"; import { Context } from "../utils/context.js"; import { generateDocs } from "../utils/docs.js"; -import { generateDecorators } from "./generate-decorators.js"; +import { generateDecorators, generateDirectives } from "./generate-decorators.js"; import { generateOperationReturnType } from "./generate-response-expressions.js"; export function generateOperation(operation: TypeSpecOperation, context: Context): string { - const definitions: string[] = []; + const preamble: string[] = []; + const operationParts: string[] = []; if (operation.doc) { - definitions.push(generateDocs(operation.doc)); + preamble.push(generateDocs(operation.doc)); } - definitions.push(...operation.tags.map((t) => `@tag("${t}")`)); + // Directives (e.g. #deprecated) must each be on their own line before the operation + preamble.push(...generateDirectives(operation.directives)); + + operationParts.push(...operation.tags.map((t) => `@tag("${t}")`)); - definitions.push(generateDecorators(operation.decorators).join(" ")); + operationParts.push(generateDecorators(operation.decorators).join(" ")); // generate parameters const parameters: string[] = [ @@ -29,12 +33,15 @@ export function generateOperation(operation: TypeSpecOperation, context: Context const responses = generateOperationReturnType(operation, context); if (operation.fixmes?.length) { - definitions.push("\n", ...operation.fixmes.map((f) => `// FIXME: ${f}\n`)); + operationParts.push("\n", ...operation.fixmes.map((f) => `// FIXME: ${f}\n`)); } - definitions.push(`op ${operation.name}(${parameters.join(", ")}): ${responses};`); + operationParts.push(`op ${operation.name}(${parameters.join(", ")}): ${responses};`); - return definitions.join(" "); + const preambleStr = preamble.join("\n"); + const operationStr = operationParts.join(" "); + + return preambleStr ? `${preambleStr}\n${operationStr}` : operationStr; } function generateOperationParameter( diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transform-paths.ts b/packages/openapi3/src/cli/actions/convert/transforms/transform-paths.ts index 21fd50ff38d..30e2d0ae0a8 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transform-paths.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transform-paths.ts @@ -9,6 +9,7 @@ import { Refable, } from "../../../../types.js"; import { + TypeSpecDirective, TypeSpecOperation, TypeSpecOperationParameter, TypeSpecRequestBody, @@ -75,6 +76,10 @@ export function transformPaths( const requestBodies = transformRequestBodies(operation.requestBody, context); + const directives: TypeSpecDirective[] = operation.deprecated + ? [{ name: "deprecated", message: "deprecated" }] + : []; + // Check if we need to split the operation due to incompatible content types const splitOperations = splitOperationByContentType( operationId, @@ -85,6 +90,7 @@ export function transformPaths( operationResponses, tags, fixmes, + directives, usedOperationIds, ); @@ -161,6 +167,7 @@ function splitOperationByContentType( responses: any, tags: string[], fixmes: string[], + directives: TypeSpecDirective[], usedOperationIds: Set, ): TypeSpecOperation[] { // If no request bodies or only one content type, no splitting needed @@ -169,6 +176,7 @@ function splitOperationByContentType( { ...getScopeAndName(operationId), decorators, + directives, parameters, doc, operationId, @@ -190,6 +198,7 @@ function splitOperationByContentType( { ...getScopeAndName(operationId), decorators, + directives, parameters, doc, operationId, @@ -248,6 +257,7 @@ function splitOperationByContentType( operations.push({ ...getScopeAndName(newOperationId), decorators: newDecorators, + directives, parameters, doc, operationId: newOperationId, diff --git a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts index 8a7289f0ccf..61c6ca3fdfe 100644 --- a/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts +++ b/packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts @@ -941,6 +941,116 @@ def grade(sample: dict, item: dict) -> float: ), ); }); + + it("should convert deprecated operation without description to #deprecated directive", async () => { + const tsp = await convertOpenAPI3Document({ + openapi: version, + info: { + title: "(title)", + version: "0.0.0", + }, + tags: [], + paths: { + "/foo": { + get: { + operationId: "Foo_get", + deprecated: true, + tags: ["Foo"], + responses: { + "200": { + description: "OK", + }, + }, + }, + }, + }, + } as any); + + strictEqual( + tsp, + await formatTypeSpec( + ` + import "@typespec/http"; + import "@typespec/openapi"; + import "@typespec/openapi3"; + + using Http; + using OpenAPI; + + @service(#{ + title: "(title)", + }) + @info(#{ + version: "0.0.0", + }) + namespace title; + + #deprecated "deprecated" + @tag("Foo") + @route("/foo") + @get + op Foo_get(): OkResponse; + `, + { printWidth: 100, tabWidth: 2 }, + ), + ); + }); + + it("should convert deprecated operation with description to #deprecated directive", async () => { + const tsp = await convertOpenAPI3Document({ + openapi: version, + info: { + title: "(title)", + version: "0.0.0", + }, + tags: [], + paths: { + "/foo": { + get: { + operationId: "Foo_get", + description: "Get foo", + deprecated: true, + tags: ["Foo"], + responses: { + "200": { + description: "OK", + }, + }, + }, + }, + }, + } as any); + + strictEqual( + tsp, + await formatTypeSpec( + ` + import "@typespec/http"; + import "@typespec/openapi"; + import "@typespec/openapi3"; + + using Http; + using OpenAPI; + + @service(#{ + title: "(title)", + }) + @info(#{ + version: "0.0.0", + }) + namespace title; + + /** Get foo */ + #deprecated "deprecated" + @tag("Foo") + @route("/foo") + @get + op Foo_get(): OkResponse; + `, + { printWidth: 100, tabWidth: 2 }, + ), + ); + }); }); describe("convertOpenAPI3Document tag metadata", () => {