Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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" })
```
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 @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions packages/openapi/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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})`;
});
Expand Down
2 changes: 2 additions & 0 deletions packages/openapi3/src/cli/actions/convert/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface TypeSpecTagMetadata {
name: string;
description?: string;
externalDocs?: TypeSpecExternalDocs;
summary?: string;
kind?: string;
}

export interface TypeSpecExternalDocs {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
};
});
}
11 changes: 11 additions & 0 deletions packages/openapi3/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>)["x-oai-summary"] = tag.summary;
delete (tagData as { summary?: string }).summary;
}
if (tag.kind) {
(tagData as unknown as Record<string, unknown>)["x-oai-kind"] = tag.kind;
delete (tagData as { kind?: string }).kind;
}
}
tags.push(tagData);
}

Expand Down
101 changes: 101 additions & 0 deletions packages/openapi3/test/tagmetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Loading