diff --git a/packages/openapi/README.md b/packages/openapi/README.md index 26f9b3e42d6..7e848c6c622 100644 --- a/packages/openapi/README.md +++ b/packages/openapi/README.md @@ -176,6 +176,7 @@ Specify OpenAPI additional information. `x-custom`: "string", } ) +@tagMetadata("Child Tag", #{ description: "Child tag description", parent: "Tag Name" }) namespace PetStore { } diff --git a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts index b04fb4dec2b..48b894737b3 100644 --- a/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts +++ b/packages/openapi/generated-defs/TypeSpec.OpenAPI.ts @@ -21,6 +21,7 @@ export interface TagMetadata { readonly [key: string]: unknown; readonly description?: string; readonly externalDocs?: ExternalDocs; + readonly parent?: string; } export interface Contact { @@ -133,6 +134,7 @@ export type InfoDecorator = ( * ```typespec * @service() * @tagMetadata("Tag Name", #{description: "Tag description", externalDocs: #{url: "https://example.com", description: "More info.", `x-custom`: "string"}, `x-custom`: "string"}) + * @tagMetadata("Child Tag", #{description: "Child tag description", parent: "Tag Name"}) * namespace PetStore {} * ``` */ diff --git a/packages/openapi/lib/decorators.tsp b/packages/openapi/lib/decorators.tsp index c1ddfefd7ed..e00bf2d1126 100644 --- a/packages/openapi/lib/decorators.tsp +++ b/packages/openapi/lib/decorators.tsp @@ -124,6 +124,9 @@ model TagMetadata { /** An external Docs information of the API. */ externalDocs?: ExternalDocs; + /** 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; + /** Attach some custom data, The extension key must start with `x-`. */ ...Record; } @@ -149,6 +152,7 @@ model ExternalDocs { * ```typespec * @service() * @tagMetadata("Tag Name", #{description: "Tag description", externalDocs: #{url: "https://example.com", description: "More info.", `x-custom`: "string"}, `x-custom`: "string"}) + * @tagMetadata("Child Tag", #{description: "Child tag description", parent: "Tag Name"}) * namespace PetStore {} * ``` */ diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 5aa2f1da30e..a3d27481723 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1782,7 +1782,12 @@ function createOAPIEmitter( } for (const [name, tag] of Object.entries(metadata || {})) { - tags.push({ name: name, ...tag }); + const tagData: OpenAPI3Tag = { name: name, ...tag }; + // For OpenAPI 3.0 and 3.1, drop the 'parent' field (only supported in 3.2) + if (specVersion !== "3.2.0" && tag.parent) { + delete (tagData as { parent?: string }).parent; + } + tags.push(tagData); } return tags; diff --git a/packages/openapi3/test/tagmetadata.test.ts b/packages/openapi3/test/tagmetadata.test.ts index 5c04db43d11..946a0d5a744 100644 --- a/packages/openapi3/test/tagmetadata.test.ts +++ b/packages/openapi3/test/tagmetadata.test.ts @@ -1,6 +1,6 @@ import { deepStrictEqual } from "assert"; -import { expect, it } from "vitest"; -import { supportedVersions, worksFor } from "./works-for.js"; +import { describe, expect, it } from "vitest"; +import { OpenAPISpecHelpers, supportedVersions, worksFor } from "./works-for.js"; worksFor(supportedVersions, ({ openApiFor, openapisFor }) => { const testCases: [string, string, string, any][] = [ @@ -150,3 +150,79 @@ worksFor(supportedVersions, ({ openApiFor, openapisFor }) => { ]); }); }); + +// Test for parent field - version specific behavior +describe("tag metadata with parent field", () => { + it("OpenAPI 3.2 should emit parent field as-is", async () => { + const res = await OpenAPISpecHelpers["3.2.0"].openApiFor( + ` + @service + @tagMetadata("ParentTag", #{description: "Parent tag"}) + @tagMetadata("ChildTag", #{description: "Child tag", parent: "ParentTag"}) + namespace PetStore { + @tag("ChildTag") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "ChildTag", + description: "Child tag", + parent: "ParentTag", + }, + { + name: "ParentTag", + description: "Parent tag", + }, + ]); + }); + + it("OpenAPI 3.1 should drop parent field", async () => { + const res = await OpenAPISpecHelpers["3.1.0"].openApiFor( + ` + @service + @tagMetadata("ParentTag", #{description: "Parent tag"}) + @tagMetadata("ChildTag", #{description: "Child tag", parent: "ParentTag"}) + namespace PetStore { + @tag("ChildTag") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "ChildTag", + description: "Child tag", + }, + { + name: "ParentTag", + description: "Parent tag", + }, + ]); + }); + + it("OpenAPI 3.0 should drop parent field", async () => { + const res = await OpenAPISpecHelpers["3.0.0"].openApiFor( + ` + @service + @tagMetadata("ParentTag", #{description: "Parent tag"}) + @tagMetadata("ChildTag", #{description: "Child tag", parent: "ParentTag"}) + namespace PetStore { + @tag("ChildTag") op test(): string; + } + `, + ); + + deepStrictEqual(res.tags, [ + { + name: "ChildTag", + description: "Child tag", + }, + { + name: "ParentTag", + description: "Parent tag", + }, + ]); + }); +}); 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 cb03c688b29..b6e89310e0c 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,8 +85,9 @@ 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. | -| | `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`. | +| | `unknown` | Additional properties | diff --git a/website/src/content/docs/docs/libraries/openapi/reference/decorators.md b/website/src/content/docs/docs/libraries/openapi/reference/decorators.md index cd223411b0d..23f9a426b8c 100644 --- a/website/src/content/docs/docs/libraries/openapi/reference/decorators.md +++ b/website/src/content/docs/docs/libraries/openapi/reference/decorators.md @@ -165,6 +165,7 @@ Specify OpenAPI additional information. `x-custom`: "string", } ) +@tagMetadata("Child Tag", #{ description: "Child tag description", parent: "Tag Name" }) namespace PetStore { }