Skip to content

Commit 2a20433

Browse files
Copilotbaywettimotheeguerin
authored
feat(openapi): add array form for @tagMetadata to control tag declaration order (#10770)
Adds a new array form for the `@tagMetadata` decorator that lets authors explicitly specify tags and their order in a single decorator call, rather than relying on the bottom-up decorator execution order. ## Changes - **`packages/openapi/lib/decorators.tsp`**: Added `TagMetadataWithName` model; updated `@tagMetadata` signature to accept `string | TagMetadataWithName[]` with the second parameter optional (only used in inline form). - **`packages/openapi/generated-defs/TypeSpec.OpenAPI.ts`**: Regenerated with new `TagMetadataWithName` interface (including `summary` and `kind` fields) and updated `TagMetadataDecorator` type. - **`packages/openapi/src/lib.ts`**: Added `mixed-tag-metadata-form` diagnostic — emitted when mixing the array form and inline form on the same namespace. Added `tag-metadata-array-with-metadata-arg` diagnostic — emitted when the second `tagMetadata` argument is provided alongside the array form. - **`packages/openapi/src/types.ts`**: Added publicly documented `TagMetadataWithName` interface (with `summary` and `kind` fields). - **`packages/openapi/src/index.ts`**: Exported new public type. - **`packages/openapi/src/decorators.ts`**: State storage changed from `{ [name: string]: TagMetadata }` to `TagMetadataWithName[]`; handles both inline and array forms; reports error on mixing or on using the second argument with the array form. - **`packages/openapi3/src/openapi.ts`**: Updated `resolveDocumentTags` to iterate the new array-based state. - **`packages/openapi3/src/cli/actions/convert/generators/generate-tags.ts`**: Updated the OpenAPI→TypeSpec converter to emit `@tagMetadata(#[...])` (array form) instead of multiple inline calls, ensuring import/export order symmetry. Emits `parent`, `summary`, and `kind` fields natively for OpenAPI 3.2.0 tags. - **`packages/openapi3/src/cli/actions/convert/transforms/transform-tags.ts`**: Passes through `parent`, `summary`, and `kind` fields from `OpenAPITag3_2`; reads `summary` and `kind` from `x-oai-summary`/`x-oai-kind` extensions as fallbacks for OpenAPI 3.0/3.1 documents. - **`packages/openapi3/src/cli/actions/convert/interfaces.ts`**: Added `parent`, `summary`, and `kind` fields to `TypeSpecTagMetadata`. - **`packages/openapi/test/decorators.test.ts`**: Updated existing tests for the new array return type; added tests for array form, mixing error, and second-argument-with-array-form error. - **`packages/openapi3/test/tagmetadata.test.ts`**: Added tests for array form ordering, operation-tag insertion behavior, and parent/child tag scenarios. - **`packages/openapi3/test/tsp-openapi3/convert-openapi3-doc.test.ts`**: Added unit tests verifying converter handling of OpenAPI 3.2.0 `parent`, `summary`, and `kind` tag fields, and `x-oai-` extension fallbacks for 3.0/3.1 documents. - **Converter snapshot outputs** (`tag-metadata`, `playground-http-service`, `petstore-swagger`, `tag-metadata-3-2`): Updated to reflect the new array form emitted by the converter. ### Array form example ```typespec @service @tagMetadata(#[ #{ name: "First Tag", description: "First tag description" }, #{ name: "Second Tag", description: "Second tag description" }, #{ name: "Third Tag", description: "Third tag description" }, ]) namespace PetStore {} ``` Tags are emitted in the exact order specified in the array. ### Mixing forms is an error Using both forms on the same namespace reports a `mixed-tag-metadata-form` diagnostic: ```typespec @service @tagMetadata(#[#{ name: "tag1" }]) @tagMetadata("tag2", #{}) // error: cannot mix array and inline form namespace PetStore {} ``` ### Passing the second argument with the array form is an error ```typespec @service @tagMetadata(#[#{ name: "tag1" }], #{description: "not allowed"}) // error: tag-metadata-array-with-metadata-arg namespace PetStore {} ``` ### Converter output The OpenAPI→TypeSpec converter now emits the array form so that tag order in the source OpenAPI document is preserved in the output TypeSpec. OpenAPI 3.2.0 `parent`, `summary`, and `kind` tag fields are emitted natively. For OpenAPI 3.0/3.1 documents, `summary` and `kind` are also read from `x-oai-summary` and `x-oai-kind` extensions: ```typespec @tagMetadata(#[ #{ name: "pet", description: "Everything about your Pets", externalDocs: #{ url: "...", description: "Find out more" } }, #{ name: "store", description: "Access to Petstore orders", externalDocs: #{ url: "...", description: "Find out more about our store" } }, #{ name: "user", description: "Operations about user" }, #{ name: "extensive", description: "A tag with all 3.2 fields", summary: "Short summary", kind: "OperationGroup", parent: "parent-tag" }, ]) namespace SwaggerPetstoreOpenAPI30; ``` ### Tag insertion behavior (unchanged) - Tags used only at the operation level (via `@tag`) and **not** declared with `@tagMetadata` appear first in the output. - Tags declared with `@tagMetadata` follow in their stored order. - A tag used at both the operation level and in `@tagMetadata` is emitted exactly once, at its `@tagMetadata`-declared position with its metadata. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> Co-authored-by: Vincent Biret <vibiret@microsoft.com> Co-authored-by: Timothee Guerin <tiguerin@microsoft.com>
1 parent c030232 commit 2a20433

22 files changed

Lines changed: 629 additions & 115 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/openapi"
5+
- "@typespec/openapi3"
6+
---
7+
8+
Add array form for `@tagMetadata` decorator to allow explicit control of tag declaration order.
9+
10+
```typespec
11+
@service
12+
@tagMetadata(#[
13+
#{ name: "First Tag", description: "First tag description" },
14+
#{ name: "Second Tag", description: "Second tag description" },
15+
])
16+
namespace PetStore {}
17+
```
18+
19+
Using `@tagMetadata(#[...])` and `@tagMetadata("name", #{...})` on the same namespace is a diagnostic error.

packages/openapi/README.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,13 @@ op read(): string;
147147

148148
#### `@tagMetadata`
149149

150-
Specify OpenAPI additional information.
150+
Specify OpenAPI tag metadata. Can be used in two forms:
151+
152+
- Inline form: specify a single tag by name with optional metadata.
153+
- Array form: specify an ordered list of tags with their metadata in a single decorator call.
151154

152155
```typespec
153-
@TypeSpec.OpenAPI.tagMetadata(name: valueof string, tagMetadata: valueof TypeSpec.OpenAPI.TagMetadata)
156+
@TypeSpec.OpenAPI.tagMetadata(name: valueof string | TypeSpec.OpenAPI.TagMetadataWithName[], tagMetadata?: valueof TypeSpec.OpenAPI.TagMetadata)
154157
```
155158

156159
##### Target
@@ -159,13 +162,15 @@ Specify OpenAPI additional information.
159162

160163
##### Parameters
161164

162-
| Name | Type | Description |
163-
| ----------- | ------------------------------------- | ---------------------- |
164-
| name | `valueof string` | tag name |
165-
| tagMetadata | [valueof `TagMetadata`](#tagmetadata) | Additional information |
165+
| Name | Type | Description |
166+
| ----------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
167+
| name | `valueof string \| TypeSpec.OpenAPI.TagMetadataWithName[]` | Tag name (inline form) or array of tags with metadata (array form). |
168+
| tagMetadata | [valueof `TagMetadata`](#tagmetadata) | Additional information for the tag. Only used in inline form. |
166169

167170
##### Examples
168171

172+
###### Inline form
173+
169174
```typespec
170175
@service
171176
@tagMetadata(
@@ -181,3 +186,16 @@ namespace PetStore {
181186
182187
}
183188
```
189+
190+
###### Array form (preserves explicit tag order)
191+
192+
```typespec
193+
@service
194+
@tagMetadata(#[
195+
#{ name: "First Tag", description: "First tag description" },
196+
#{ name: "Second Tag", description: "Second tag description" }
197+
])
198+
namespace PetStore {
199+
200+
}
201+
```

packages/openapi/generated-defs/TypeSpec.OpenAPI.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ export interface AdditionalInfo {
1717
readonly license?: License;
1818
}
1919

20+
export interface TagMetadataWithName {
21+
readonly [key: string]: unknown;
22+
readonly name: string;
23+
readonly description?: string;
24+
readonly externalDocs?: ExternalDocs;
25+
readonly parent?: string;
26+
readonly summary?: string;
27+
readonly kind?: string;
28+
}
29+
2030
export interface TagMetadata {
2131
readonly [key: string]: unknown;
2232
readonly description?: string;
@@ -128,23 +138,34 @@ export type InfoDecorator = (
128138
) => DecoratorValidatorCallbacks | void;
129139

130140
/**
131-
* Specify OpenAPI additional information.
141+
* Specify OpenAPI tag metadata. Can be used in two forms:
142+
* - Inline form: specify a single tag by name with optional metadata.
143+
* - Array form: specify an ordered list of tags with their metadata in a single decorator call.
132144
*
133-
* @param name tag name
134-
* @param tagMetadata Additional information
135-
* @example
145+
* @param name Tag name (inline form) or array of tags with metadata (array form).
146+
* @param tagMetadata Additional information for the tag. Only used in inline form.
147+
* @example Inline form
136148
* ```typespec
137149
* @service()
138150
* @tagMetadata("Tag Name", #{description: "Tag description", externalDocs: #{url: "https://example.com", description: "More info.", `x-custom`: "string"}, `x-custom`: "string"})
139151
* @tagMetadata("Child Tag", #{description: "Child tag description", parent: "Tag Name"})
140152
* namespace PetStore {}
141153
* ```
154+
* @example Array form (preserves explicit tag order)
155+
* ```typespec
156+
* @service()
157+
* @tagMetadata(#[
158+
* #{ name: "First Tag", description: "First tag description" },
159+
* #{ name: "Second Tag", description: "Second tag description" },
160+
* ])
161+
* namespace PetStore {}
162+
* ```
142163
*/
143164
export type TagMetadataDecorator = (
144165
context: DecoratorContext,
145166
target: Namespace,
146-
name: string,
147-
tagMetadata: TagMetadata,
167+
name: string | readonly TagMetadataWithName[],
168+
tagMetadata?: TagMetadata,
148169
) => DecoratorValidatorCallbacks | void;
149170

150171
export type TypeSpecOpenAPIDecorators = {

packages/openapi/lib/decorators.tsp

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ extern dec info(target: Namespace, additionalInfo: valueof AdditionalInfo);
118118

119119
/** Metadata to a single tag that is used by operations. */
120120
model TagMetadata {
121-
/** A description of the API. */
121+
/** A description of the tag. */
122122
description?: string;
123123

124-
/** An external Docs information of the API. */
124+
/** External documentation information for the tag. */
125125
externalDocs?: ExternalDocs;
126126

127127
/** 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`. */
@@ -137,6 +137,14 @@ model TagMetadata {
137137
...Record<unknown>;
138138
}
139139

140+
/** Metadata for a tag that includes the name of the tag. Used with the array form of `@tagMetadata`. */
141+
model TagMetadataWithName {
142+
/** The name of the tag. */
143+
name: string;
144+
145+
...TagMetadata;
146+
}
147+
140148
/** External Docs information. */
141149
model ExternalDocs {
142150
/** Documentation url */
@@ -150,16 +158,33 @@ model ExternalDocs {
150158
}
151159

152160
/**
153-
* Specify OpenAPI additional information.
154-
* @param name tag name
155-
* @param tagMetadata Additional information
161+
* Specify OpenAPI tag metadata. Can be used in two forms:
162+
* - Inline form: specify a single tag by name with optional metadata.
163+
* - Array form: specify an ordered list of tags with their metadata in a single decorator call.
156164
*
157-
* @example
165+
* @param name Tag name (inline form) or array of tags with metadata (array form).
166+
* @param tagMetadata Additional information for the tag. Only used in inline form.
167+
*
168+
* @example Inline form
158169
* ```typespec
159170
* @service()
160171
* @tagMetadata("Tag Name", #{description: "Tag description", externalDocs: #{url: "https://example.com", description: "More info.", `x-custom`: "string"}, `x-custom`: "string"})
161172
* @tagMetadata("Child Tag", #{description: "Child tag description", parent: "Tag Name"})
162173
* namespace PetStore {}
163174
* ```
175+
*
176+
* @example Array form (preserves explicit tag order)
177+
* ```typespec
178+
* @service()
179+
* @tagMetadata(#[
180+
* #{ name: "First Tag", description: "First tag description" },
181+
* #{ name: "Second Tag", description: "Second tag description" },
182+
* ])
183+
* namespace PetStore {}
184+
* ```
164185
*/
165-
extern dec tagMetadata(target: Namespace, name: valueof string, tagMetadata: valueof TagMetadata);
186+
extern dec tagMetadata(
187+
target: Namespace,
188+
name: valueof string | TagMetadataWithName[],
189+
tagMetadata?: valueof TagMetadata
190+
);

0 commit comments

Comments
 (0)