diff --git a/.chronus/changes/add-tag-metadata-summary-kind-2026-5-22-13-53-44.md b/.chronus/changes/add-tag-metadata-summary-kind-2026-5-22-13-53-44.md new file mode 100644 index 00000000000..453708fb781 --- /dev/null +++ b/.chronus/changes/add-tag-metadata-summary-kind-2026-5-22-13-53-44.md @@ -0,0 +1,14 @@ +--- +changeKind: feature +packages: + - "@typespec/openapi" + - "@typespec/openapi3" +--- + +Add `summary` and `kind` fields to `@tagMetadata` decorator. + +For OpenAPI 3.2, these fields are emitted as native tag object fields. For OpenAPI 3.0/3.1, they are emitted as `x-oai-summary` and `x-oai-kind` extensions. The OpenAPI converter also supports importing `x-oai-summary`, `x-oai-kind` (from 3.0/3.1) and native `summary`, `kind` (from 3.2) back to TypeSpec. + +```typespec +@tagMetadata("foo", #{ summary: "all operations that allow doing Foo", kind: "FooGroup" }) +``` diff --git a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts index 48b894737b3..93e555b29df 100644 --- a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts +++ b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts @@ -22,6 +22,8 @@ export interface TagMetadata { readonly description?: string; readonly externalDocs?: ExternalDocs; readonly parent?: string; + readonly summary?: string; + readonly kind?: string; } export interface Contact { diff --git a/packages/openapi/lib/decorators.tsp b/packages/openapi/lib/decorators.tsp index e00bf2d1126..547707cec51 100644 --- a/packages/openapi/lib/decorators.tsp +++ b/packages/openapi/lib/decorators.tsp @@ -127,6 +127,12 @@ model TagMetadata { /** The name of a tag that this tag is nested under. Only supported in OpenAPI 3.2. For 3.0 and 3.1, this will be converted to `x-parent`. */ parent?: string; + /** A short summary of the tag, used for display purposes. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-summary`. */ + summary?: string; + + /** A machine-readable string to categorize what sort of tag it is. Any string value can be used. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-kind`. */ + kind?: string; + /** Attach some custom data, The extension key must start with `x-`. */ ...Record; } diff --git a/packages/openapi3/src/cli/actions/convert/generators/generate-tags.ts b/packages/openapi3/src/cli/actions/convert/generators/generate-tags.ts index b759356a167..1080d3c7e36 100644 --- a/packages/openapi3/src/cli/actions/convert/generators/generate-tags.ts +++ b/packages/openapi3/src/cli/actions/convert/generators/generate-tags.ts @@ -21,9 +21,11 @@ export function generateTags(tags: TypeSpecTagMetadata[]): string { const definitions = tags.map((tag) => { const description = tag.description ? `description: "${tag.description}"` : ""; const externalDocs = generateExternalDocs(tag.externalDocs); + const summary = tag.summary ? `summary: "${tag.summary}"` : ""; + const kind = tag.kind ? `kind: "${tag.kind}"` : ""; const tagMetadata = - description || externalDocs - ? `, #{${[description, externalDocs].filter((x) => !!x).join(", ")}}` + description || externalDocs || summary || kind + ? `, #{${[description, externalDocs, summary, kind].filter((x) => !!x).join(", ")}}` : ""; return `@tagMetadata("${tag.name}"${tagMetadata})`; }); diff --git a/packages/openapi3/src/cli/actions/convert/interfaces.ts b/packages/openapi3/src/cli/actions/convert/interfaces.ts index 82efbe8db62..c1decacbe54 100644 --- a/packages/openapi3/src/cli/actions/convert/interfaces.ts +++ b/packages/openapi3/src/cli/actions/convert/interfaces.ts @@ -32,6 +32,8 @@ export interface TypeSpecTagMetadata { name: string; description?: string; externalDocs?: TypeSpecExternalDocs; + summary?: string; + kind?: string; } export interface TypeSpecExternalDocs { diff --git a/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts b/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts index 84de81cbd98..0550790d009 100644 --- a/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts +++ b/packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts @@ -1,16 +1,26 @@ -import { OpenAPI3Tag } from "../../../../types.js"; +import { OpenAPI3Tag, OpenAPITag3_2 } from "../../../../types.js"; import { TypeSpecTagMetadata } from "../interfaces.js"; export function transformTags(tags: OpenAPI3Tag[]): TypeSpecTagMetadata[] { - return tags.map((tag) => ({ - name: tag.name, - description: tag.description, - externalDocs: - tag.externalDocs?.url || tag.externalDocs?.description - ? { - url: tag.externalDocs.url, - description: tag.externalDocs.description, - } - : undefined, - })); + return tags.map((tag) => { + const tag32 = tag as OpenAPITag3_2; + // Support both native 3.2 fields and x-oai- prefixed extensions for 3.0/3.1 + const summary: string | undefined = + tag32.summary ?? (tag["x-oai-summary"] as string | undefined); + const kind: string | undefined = tag32.kind ?? (tag["x-oai-kind"] as string | undefined); + + return { + name: tag.name, + description: tag.description, + externalDocs: + tag.externalDocs?.url || tag.externalDocs?.description + ? { + url: tag.externalDocs.url, + description: tag.externalDocs.description, + } + : undefined, + summary, + kind, + }; + }); } diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 8425d7ad001..9c67c48b867 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1831,6 +1831,17 @@ function createOAPIEmitter( if (specVersion !== "3.2.0" && tag.parent) { delete (tagData as { parent?: string }).parent; } + // For OpenAPI 3.0 and 3.1, convert 'summary' and 'kind' to x-oai- prefixed extensions + if (specVersion !== "3.2.0") { + if (tag.summary) { + (tagData as unknown as Record)["x-oai-summary"] = tag.summary; + delete (tagData as { summary?: string }).summary; + } + if (tag.kind) { + (tagData as unknown as Record)["x-oai-kind"] = tag.kind; + delete (tagData as { kind?: string }).kind; + } + } tags.push(tagData); } diff --git a/packages/openapi3/test/tagmetadata.test.ts b/packages/openapi3/test/tagmetadata.test.ts index 946a0d5a744..5dcf6417be2 100644 --- a/packages/openapi3/test/tagmetadata.test.ts +++ b/packages/openapi3/test/tagmetadata.test.ts @@ -226,3 +226,104 @@ describe("tag metadata with parent field", () => { ]); }); }); + +// Tests for summary and kind fields - version specific behavior +describe("tag metadata with summary and kind fields", () => { + it("OpenAPI 3.2 should emit summary and kind as native fields", async () => { + const res = await OpenAPISpecHelpers["3.2.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{summary: "all operations that allow doing Foo", kind: "FooGroup"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + summary: "all operations that allow doing Foo", + kind: "FooGroup", + }, + ]); + }); + + it("OpenAPI 3.1 should emit summary as x-oai-summary and kind as x-oai-kind", async () => { + const res = await OpenAPISpecHelpers["3.1.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{summary: "all operations that allow doing Foo", kind: "FooGroup"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + "x-oai-summary": "all operations that allow doing Foo", + "x-oai-kind": "FooGroup", + }, + ]); + }); + + it("OpenAPI 3.0 should emit summary as x-oai-summary and kind as x-oai-kind", async () => { + const res = await OpenAPISpecHelpers["3.0.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{summary: "all operations that allow doing Foo", kind: "FooGroup"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + "x-oai-summary": "all operations that allow doing Foo", + "x-oai-kind": "FooGroup", + }, + ]); + }); + + it("OpenAPI 3.2 should emit summary only if kind is absent", async () => { + const res = await OpenAPISpecHelpers["3.2.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{summary: "all operations that allow doing Foo"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + summary: "all operations that allow doing Foo", + }, + ]); + }); + + it("OpenAPI 3.2 should emit kind only if summary is absent", async () => { + const res = await OpenAPISpecHelpers["3.2.0"].openApiFor( + ` + @service + @tagMetadata("foo", #{kind: "FooGroup"}) + namespace PetStore { + @tag("foo") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "foo", + kind: "FooGroup", + }, + ]); + }); +}); diff --git a/packages/openapi3/test/tsp-openapi3/output/tag-metadata-3-2/main.tsp b/packages/openapi3/test/tsp-openapi3/output/tag-metadata-3-2/main.tsp new file mode 100644 index 00000000000..3c3fcb700c2 --- /dev/null +++ b/packages/openapi3/test/tsp-openapi3/output/tag-metadata-3-2/main.tsp @@ -0,0 +1,27 @@ +import "@typespec/http"; +import "@typespec/openapi"; +import "@typespec/openapi3"; + +using Http; +using OpenAPI; + +@service(#{ title: "(title)" }) +@info(#{ version: "0.0.0" }) +@tagMetadata( + "extensive", + #{ + description: "An extensive operation", + summary: "A summary of the extensive tag", + kind: "OperationGroup", + } +) +namespace title; + +model Pet { + name: string; +} + +@tag("extensive") +@route("/") +@get +op extensive(): Pet; diff --git a/packages/openapi3/test/tsp-openapi3/output/tag-metadata/main.tsp b/packages/openapi3/test/tsp-openapi3/output/tag-metadata/main.tsp index 18d5ad53cee..94a0a1c8499 100644 --- a/packages/openapi3/test/tsp-openapi3/output/tag-metadata/main.tsp +++ b/packages/openapi3/test/tsp-openapi3/output/tag-metadata/main.tsp @@ -7,7 +7,14 @@ using OpenAPI; @service(#{ title: "(title)" }) @info(#{ version: "0.0.0" }) -@tagMetadata("extensive", #{ description: "An extensive operation" }) +@tagMetadata( + "extensive", + #{ + description: "An extensive operation", + summary: "A summary of the extensive tag", + kind: "OperationGroup", + } +) namespace title; model Pet { diff --git a/packages/openapi3/test/tsp-openapi3/specs/tag-metadata-3-2/service.yml b/packages/openapi3/test/tsp-openapi3/specs/tag-metadata-3-2/service.yml new file mode 100644 index 00000000000..7ce1af0c390 --- /dev/null +++ b/packages/openapi3/test/tsp-openapi3/specs/tag-metadata-3-2/service.yml @@ -0,0 +1,33 @@ +openapi: 3.2.0 +info: + title: (title) + version: 0.0.0 +tags: + - name: extensive + description: An extensive operation + summary: A summary of the extensive tag + kind: OperationGroup + parent: root +paths: + /: + get: + operationId: extensive + parameters: [] + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + tags: + - extensive +components: + schemas: + Pet: + type: object + required: + - name + properties: + name: + type: string diff --git a/packages/openapi3/test/tsp-openapi3/specs/tag-metadata/service.yml b/packages/openapi3/test/tsp-openapi3/specs/tag-metadata/service.yml index 1e89668fa1b..39883beea4a 100644 --- a/packages/openapi3/test/tsp-openapi3/specs/tag-metadata/service.yml +++ b/packages/openapi3/test/tsp-openapi3/specs/tag-metadata/service.yml @@ -5,6 +5,8 @@ info: tags: - name: extensive description: An extensive operation + x-oai-summary: A summary of the extensive tag + x-oai-kind: OperationGroup paths: /: get: diff --git a/website/src/content/docs/docs/libraries/openapi/reference/data-types.md b/website/src/content/docs/docs/libraries/openapi/reference/data-types.md index b6e89310e0c..2595d35015c 100644 --- a/website/src/content/docs/docs/libraries/openapi/reference/data-types.md +++ b/website/src/content/docs/docs/libraries/openapi/reference/data-types.md @@ -85,9 +85,11 @@ model TypeSpec.OpenAPI.TagMetadata #### Properties -| Name | Type | Description | -| ------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| description? | `string` | A description of the API. | -| externalDocs? | [`ExternalDocs`](./data-types.md#TypeSpec.OpenAPI.ExternalDocs) | An external Docs information of the API. | -| parent? | `string` | The name of a tag that this tag is nested under. Only supported in OpenAPI 3.2. For 3.0 and 3.1, this will be converted to `x-parent`. | -| | `unknown` | Additional properties | +| Name | Type | Description | +| ------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| description? | `string` | A description of the API. | +| externalDocs? | [`ExternalDocs`](./data-types.md#TypeSpec.OpenAPI.ExternalDocs) | An external Docs information of the API. | +| parent? | `string` | The name of a tag that this tag is nested under. Only supported in OpenAPI 3.2. For 3.0 and 3.1, this will be converted to `x-parent`. | +| summary? | `string` | A short summary of the tag, used for display purposes. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-summary`. | +| kind? | `string` | A machine-readable string to categorize what sort of tag it is. Any string value can be used. Only supported natively in OpenAPI 3.2. For 3.0 and 3.1, this will be emitted as `x-oai-kind`. | +| | `unknown` | Additional properties |