@tetsujs/openapi 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,20 +76,57 @@ the hash, to be prefixed with `sha384-`.
76
76
  | `documentSelf` | `false` | include `/openapi.json` and `/docs` in the document |
77
77
  | `onWarning` | `console.warn` | receives what could not be described |
78
78
  | `errors` | the framework's envelope | an error format of your own — see [below](#an-error-format-of-your-own) |
79
+ | `tags` | — | what each tag is, in the order the sidebar lists them — see [below](#tags) |
79
80
 
80
- ## What goes into the document
81
+ ## How a route becomes an operation
81
82
 
82
- | From the route | Becomes |
83
- | --- | --- |
84
- | `path` | the path: `/users/:id` → `/users/{id}` |
85
- | `schema.params`, `query`, `headers`, `cookies` | parameters, required as the schema says |
86
- | `schema.body` and `bodyType` | the request body and its media types |
87
- | `schema.response` | responses — one, or one per status of a map; `null` is a status without a body |
88
- | `docs` | `summary`, `description`, `tags`, `deprecated` |
89
- | the controller's name and the field | `operationId`: `setAvatar` in `controller("Users", …)` → `usersSetAvatar` |
83
+ Every part of an operation comes from somewhere the application already
84
+ wrote, by a rule:
85
+
86
+ | In the document | Comes from | Rule |
87
+ | --- | --- | --- |
88
+ | the path | the route's `path`, under its groups' prefixes | `/users/:id` → `/users/{id}` |
89
+ | `operationId` | `docs.operationId`, or the controller's name and the field | `setAvatar` in `controller("Users", …)` → `usersSetAvatar` — see [Operation ids](#operation-ids) |
90
+ | `summary`, `description`, `tags`, `deprecated` | the route's `docs` | as written |
91
+ | parameters | `schema.params`, `query`, `headers`, `cookies` | required as the schema says |
92
+ | the request body | `schema.body` and `bodyType` | media types from `bodyType`; required unless `text` or `stream` |
93
+ | `security` | `secured()` hooks, of every level the route runs under | every hook required together; `anyOf` inside one hook is the alternatives |
94
+ | the statuses | the route's `schema.response`, the hooks' `documented()` and `secured()`, the framework | one schema is `200`, a map its statuses, `null` a status without a body; with no `2xx` declared, `200`; the framework's `422`, `400`, `413`, `500` where they can happen — see [Responses the framework adds](#responses-the-framework-adds) |
95
+ | a status's body | everything answering with that status | one flat `anyOf`, the route's own first, the same body once |
96
+ | an error envelope | any of them | one definition per status and code in `components`, named after the code; the route's own kept |
97
+ | `discriminator` | the envelopes of a status | on `error`, or the format's field, when every alternative is an envelope |
98
+ | a status's description | the schemas' `description`, the hooks', the framework's | one is the description, several a list led by code, none the reason phrase |
99
+ | a status's headers | the hooks' `documented()` headers | possible, not required |
100
+ | the document's `tags` | the `tags` option | in its order, then the tags routes use and it leaves out |
90
101
 
91
102
  `docs: { hidden: true }` leaves a route out of the document; it is served
92
- as before.
103
+ as before. A route with no `schema.response` is documented as `200` — its
104
+ handler may still answer `204` by returning nothing, which only a declared
105
+ `204: null` says.
106
+
107
+ ## Tags
108
+
109
+ A route names its tags in `docs: { tags }`; the document says what each
110
+ one is:
111
+
112
+ ```ts
113
+ docs({
114
+ info,
115
+ tags: {
116
+ "sign-in": "Signing in with a code sent by email, and signing out",
117
+ me: "The signed-in user",
118
+ },
119
+ });
120
+ ```
121
+
122
+ The order of the keys is the order of the sections a renderer lists. A tag
123
+ the routes use and `tags` leaves out is listed after them, in the order
124
+ the routes first use it, and reported — it is usually one tag spelled two
125
+ ways, `sign_in` next to `sign-in`. So is a tag described and used by no
126
+ operation. Without `tags` the document lists none, and nothing is
127
+ reported. An application of several surfaces, each with its own `docs()`,
128
+ gives each one only the tags of its own routes: given all of them, every
129
+ `docs()` reports the tags of the others as unused.
93
130
 
94
131
  ## Operation ids
95
132
 
@@ -325,6 +362,50 @@ await Bun.write("openapi.json", JSON.stringify(document, null, 2));
325
362
 
326
363
  `docsPage()` renders the page on its own, for serving it elsewhere.
327
364
 
365
+ ## Testing against the document
366
+
367
+ `@tetsujs/openapi/testing` checks a response a test provoked against the
368
+ operation the document describes:
369
+
370
+ ```ts
371
+ import { assertDescribed } from "@tetsujs/openapi/testing";
372
+
373
+ const { document } = openapi(app, { info });
374
+
375
+ test("a wrong code is what the document says", async () => {
376
+ const res = await request("/session", { method: "POST", body });
377
+
378
+ await assertDescribed(document, "POST /session", res);
379
+ });
380
+ ```
381
+
382
+ It throws unless the status is declared and the body fits one of the
383
+ alternatives its status describes, listing every problem at once:
384
+
385
+ ```
386
+ POST /session answered 403, which the document does not describe:
387
+ - error "CAPTCHA_FAILED", which its 403 does not list: ACCOUNT_DISABLED, CSRF_HEADER_REQUIRED
388
+ ```
389
+
390
+ The operation is named as the test called it, a method and the path it
391
+ requested, matched against the document's templates. Without a validator
392
+ only the top level of the body is compared — required fields, and fields
393
+ that are a `const`, which is where an envelope keeps its code. `validate`
394
+ checks the whole body with the JSON Schema validator of your choice, given
395
+ a schema that stands on its own; `headers` names headers the status must
396
+ declare when the response carries them:
397
+
398
+ ```ts
399
+ await assertDescribed(document, "POST /session", res, {
400
+ validate: (schema, body) => ajv.validate(schema, body) || ajv.errorsText(),
401
+ headers: ["retry-after"],
402
+ });
403
+ ```
404
+
405
+ The core does not check a thrown error against the route's response map:
406
+ a guard's refusal would be reported on every route it runs on. A test asks
407
+ about the one response it provoked.
408
+
328
409
  ## Warnings
329
410
 
330
411
  A route is still documented when part of it cannot be described, and the
@@ -29,6 +29,12 @@ export interface DocsOptions<Path extends string, UiPath extends string, Ui exte
29
29
  * envelope — see `errors` on {@link openapi}.
30
30
  */
31
31
  readonly errors?: ErrorFormat;
32
+ /**
33
+ * What each tag is — see `tags` on {@link openapi}. The controller's own
34
+ * tag, `docs`, is described for you when its routes are in the document
35
+ * (`documentSelf`) and you did not describe it.
36
+ */
37
+ readonly tags?: Readonly<Record<string, string>>;
32
38
  /** Where the document is served. Defaults to `/openapi.json`. */
33
39
  readonly path?: Path & ValidatePath<Path>;
34
40
  /** Where the page is served. Defaults to `/docs`. */
@@ -79,6 +85,12 @@ export declare class DocsController<Path extends string, UiPath extends string,
79
85
  private document;
80
86
  private page;
81
87
  constructor(options: DocsOptions<Path, UiPath>, documentPath: string, uiPath: string);
88
+ /**
89
+ * The tags to describe: the application's, and this controller's own when
90
+ * its routes are in the document — without it, a described set of tags
91
+ * would report `docs` as a tag nobody described.
92
+ */
93
+ private tagsOf;
82
94
  /**
83
95
  * Generates the document of the application this was mounted in, and
84
96
  * the page that renders it.
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/controller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/controller.ts"],"names":[],"mappings":"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"}
@@ -67,12 +67,29 @@ export interface OperationObject {
67
67
  }
68
68
  /** Every operation declared on one path template. */
69
69
  export type PathItemObject = Record<string, OperationObject>;
70
+ /**
71
+ * A tag the document describes: its name and what its operations are.
72
+ *
73
+ * Open to the rest of the specification's fields — `externalDocs` — as
74
+ * `info` is: the generator fills in two, and a document a caller builds
75
+ * or extends by hand may carry more.
76
+ */
77
+ export interface TagObject {
78
+ readonly name: string;
79
+ readonly description?: string;
80
+ readonly [key: string]: unknown;
81
+ }
70
82
  /** The generated document. */
71
83
  export interface OpenApiDocument {
72
84
  readonly openapi: "3.1.0";
73
85
  readonly info: DocumentInfo;
74
86
  readonly servers?: readonly DocumentServer[];
75
87
  readonly paths: Record<string, PathItemObject>;
88
+ /**
89
+ * The tags the operations are grouped under, in the order a renderer
90
+ * lists them. Present only when the generator was given descriptions.
91
+ */
92
+ readonly tags?: readonly TagObject[];
76
93
  /**
77
94
  * What the operations refer to rather than repeat: the security schemes
78
95
  * the guards registered, and the failure envelopes the framework fills
@@ -1 +1 @@
1
- {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/document.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,uDAAuD;AACvD,MAAM,MAAM,UAAU,GAAG,MAAM,CAC7B,MAAM,EACN;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAC9C,CAAC;AAEF,mCAAmC;AACnC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B;AAED,oCAAoC;AACpC,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAChD,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC;CAC/B;AAED,wCAAwC;AACxC,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC3B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;KAC9B,CAAC;IACF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;CACpD;AAED,qDAAqD;AACrD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAE7D,8BAA8B;AAC9B,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAE/C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE;QACpB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC5C,CAAC;IAEF,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC"}
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/document.ts"],"names":[],"mappings":"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"}
@@ -25,7 +25,7 @@ export type { DocumentedHeader, DocumentedResponse, HookContributions, HookDocs,
25
25
  export { documented, secured } from "./annotations.ts";
26
26
  export type { DocsOptions } from "./controller.ts";
27
27
  export { DocsController, docs } from "./controller.ts";
28
- export type { ContentMap, DocumentInfo, DocumentServer, HeaderObject, OpenApiDocument, OperationObject, ParameterObject, PathItemObject, ResponseObject, } from "./document.ts";
28
+ export type { ContentMap, DocumentInfo, DocumentServer, HeaderObject, OpenApiDocument, OperationObject, ParameterObject, PathItemObject, ResponseObject, TagObject, } from "./document.ts";
29
29
  export type { DocumentedFailure, ErrorFormat } from "./errors.ts";
30
30
  export type { JsonSchema, JsonSchemaKeywords, JsonSchemaType, } from "./json-schema.ts";
31
31
  export type { DocsAssets, DocsPageOptions, DocsUi } from "./page.ts";
@@ -64,6 +64,28 @@ export interface OpenApiOptions {
64
64
  * ```
65
65
  */
66
66
  readonly errors?: ErrorFormat;
67
+ /**
68
+ * What each tag is, by name — the sections a renderer's sidebar shows,
69
+ * in the order given here.
70
+ *
71
+ * A tag the routes use and this leaves out is listed after these, in the
72
+ * order the routes first use it, without a description; so is reported,
73
+ * as a likely misspelling — `sign_in` for `sign-in` makes a section of its
74
+ * own. So is a tag described here that no operation uses. Without
75
+ * `tags`, the document lists none and nothing is reported.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * openapi(app, {
80
+ * info,
81
+ * tags: {
82
+ * "sign-in": "Signing in with a code sent by email, and signing out",
83
+ * me: "The signed-in user",
84
+ * },
85
+ * });
86
+ * ```
87
+ */
88
+ readonly tags?: Readonly<Record<string, string>>;
67
89
  }
68
90
  /**
69
91
  * What the generator could not describe faithfully.
@@ -74,6 +96,10 @@ export interface OpenApiOptions {
74
96
  * these are returned rather than swallowed.
75
97
  */
76
98
  export interface GeneratorWarning {
99
+ /**
100
+ * The route the warning is about, as `GET /path` — empty for a warning
101
+ * about the document as a whole, such as a described tag nothing uses.
102
+ */
77
103
  readonly route: string;
78
104
  readonly message: string;
79
105
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,eAAe,CAAC;AAErE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,uCAAuC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAE9C;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe,CA8F1E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAA6B,MAAM,eAAe,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,eAAe,EACf,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,SAAS,GACV,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,uCAAuC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAE9C;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAE9B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAClD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAChD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe,CA2G1E"}
package/dist/src/index.js CHANGED
@@ -847,11 +847,18 @@ class DocsController {
847
847
  this.json = { ...json, path: documentPath };
848
848
  this.ui = options.ui === false ? undefined : { ...ui, path: uiPath };
849
849
  }
850
+ tagsOf(tags) {
851
+ if (!this.options.documentSelf || Object.hasOwn(tags, "docs")) {
852
+ return tags;
853
+ }
854
+ return { ...tags, docs: "This documentation" };
855
+ }
850
856
  [onMount](app) {
851
857
  const generated = openapi(app, {
852
858
  info: this.options.info,
853
859
  ...this.options.servers ? { servers: this.options.servers } : {},
854
- ...this.options.errors ? { errors: this.options.errors } : {}
860
+ ...this.options.errors ? { errors: this.options.errors } : {},
861
+ ...this.options.tags ? { tags: this.tagsOf(this.options.tags) } : {}
855
862
  });
856
863
  this.document = generated.document;
857
864
  const ui = this.options.ui ?? "scalar";
@@ -875,7 +882,7 @@ function docs(options) {
875
882
  return new DocsController(options, options.path ?? "/openapi.json", options.uiPath ?? "/docs");
876
883
  }
877
884
  function warn(warning) {
878
- console.warn(`[openapi] ${warning.route}: ${warning.message}`);
885
+ console.warn(warning.route === "" ? `[openapi] ${warning.message}` : `[openapi] ${warning.route}: ${warning.message}`);
879
886
  }
880
887
 
881
888
  // packages/openapi/src/index.ts
@@ -891,6 +898,7 @@ function openapi(app, options) {
891
898
  declareEnvelopes(entry, envelopes2);
892
899
  }
893
900
  const occupied = new Map;
901
+ const used = new Map;
894
902
  for (const entry of described) {
895
903
  const template = toTemplate(entry.path);
896
904
  const route = `${entry.method} ${entry.path}`;
@@ -919,22 +927,51 @@ function openapi(app, options) {
919
927
  });
920
928
  }
921
929
  occupied.set(slot, route);
930
+ for (const tag of operation.tags ?? []) {
931
+ if (!used.has(tag)) {
932
+ used.set(tag, route);
933
+ }
934
+ }
922
935
  paths[template] = {
923
936
  ...paths[template],
924
937
  [entry.method.toLowerCase()]: operation
925
938
  };
926
939
  }
940
+ const tags = options.tags ? tagsOf(options.tags, used, warnings) : undefined;
927
941
  return {
928
942
  document: {
929
943
  openapi: "3.1.0",
930
944
  info: options.info,
931
945
  ...options.servers ? { servers: options.servers } : {},
946
+ ...tags ? { tags } : {},
932
947
  paths,
933
948
  ...componentsOf(schemes, components.schemas)
934
949
  },
935
950
  warnings
936
951
  };
937
952
  }
953
+ function tagsOf(described, used, warnings) {
954
+ const tags = Object.entries(described).map(([name, description]) => ({ name, description }));
955
+ for (const [name, route] of used) {
956
+ if (Object.hasOwn(described, name)) {
957
+ continue;
958
+ }
959
+ tags.push({ name });
960
+ warnings.push({
961
+ route,
962
+ message: `the tag "${name}" is not in tags; it is listed after them, without a description`
963
+ });
964
+ }
965
+ for (const name of Object.keys(described)) {
966
+ if (!used.has(name)) {
967
+ warnings.push({
968
+ route: "",
969
+ message: `the tag "${name}" is described in tags, but no operation uses it`
970
+ });
971
+ }
972
+ }
973
+ return tags;
974
+ }
938
975
  function componentsOf(securitySchemes, schemas) {
939
976
  const components = {
940
977
  ...Object.keys(securitySchemes).length > 0 ? { securitySchemes } : {},
@@ -951,5 +988,5 @@ export {
951
988
  secured
952
989
  };
953
990
 
954
- //# debugId=4A68DE756C1883B864756E2164756E21
991
+ //# debugId=3A3B60517CA111D464756E2164756E21
955
992
  //# sourceMappingURL=index.js.map