Skip to content
1 change: 1 addition & 0 deletions packages/openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Specify OpenAPI additional information.
`x-custom`: "string",
}
)
@tagMetadata("Child Tag", #{ description: "Child tag description", parent: "Tag Name" })
namespace PetStore {

}
Expand Down
2 changes: 2 additions & 0 deletions packages/openapi/generated-defs/TypeSpec.OpenAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface TagMetadata {
readonly [key: string]: unknown;
readonly description?: string;
readonly externalDocs?: ExternalDocs;
readonly parent?: string;
}

export interface Contact {
Expand Down Expand Up @@ -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 {}
* ```
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/openapi/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;
}
Expand All @@ -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 {}
* ```
*/
Expand Down
7 changes: 6 additions & 1 deletion packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
80 changes: 78 additions & 2 deletions packages/openapi3/test/tagmetadata.test.ts
Original file line number Diff line number Diff line change
@@ -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][] = [
Expand Down Expand Up @@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this have X-Parent?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided not too, and just dropped and would add it if someone asked with options.

},
{
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",
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this have X-Parent?

{
name: "ParentTag",
description: "Parent tag",
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ Specify OpenAPI additional information.
`x-custom`: "string",
}
)
@tagMetadata("Child Tag", #{ description: "Child tag description", parent: "Tag Name" })
namespace PetStore {

}
Expand Down
Loading