@tetsujs/openapi 0.5.0 → 0.5.2

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
 
@@ -152,6 +189,23 @@ When every alternative of a status is an envelope, the union carries a
152
189
  `discriminator` on `error`, with the mapping from each code to its
153
190
  definition, and a generated client narrows on the code.
154
191
 
192
+ A status is described by what answers with it. The route's part is the
193
+ `description` of the schema it declares — `.describe()` in Zod and
194
+ ArkType, `v.description()` in Valibot, `{ description }` in TypeBox — for
195
+ any status, a `200` as much as a `404`; a union described as a whole is
196
+ one description, one described branch by branch is one per branch. A
197
+ hook's part is its `documented()` description, the framework's its own.
198
+ One description is the status's description; several are a list, each
199
+ led by its code:
200
+
201
+ ```md
202
+ - `ACCOUNT_DISABLED`: this account is disabled
203
+ - `CAPTCHA_FAILED`: the captcha token is missing or did not pass
204
+ ```
205
+
206
+ A status nothing describes keeps its reason phrase — `Not Found`,
207
+ `Successful response`.
208
+
155
209
  ## An error format of your own
156
210
 
157
211
  Every failure reaches the application's `onError` hooks — a thrown
@@ -308,6 +362,50 @@ await Bun.write("openapi.json", JSON.stringify(document, null, 2));
308
362
 
309
363
  `docsPage()` renders the page on its own, for serving it elsewhere.
310
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
+
311
409
  ## Warnings
312
410
 
313
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":"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"}
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/document.ts"],"names":[],"mappings":"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"}
@@ -15,6 +15,7 @@ export interface JsonSchemaObject {
15
15
  required?: unknown;
16
16
  anyOf?: unknown;
17
17
  oneOf?: unknown;
18
+ description?: unknown;
18
19
  [keyword: string]: unknown;
19
20
  }
20
21
  /** Which shape of a schema to describe: what arrives, or what leaves. */
@@ -1 +1 @@
1
- {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/emit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5B;AAED,yEAAyE;AACzE,KAAK,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEpC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CACrB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EAC/B,SAAS,GAAE,SAAoB,GAC9B,gBAAgB,GAAG,SAAS,CAkB9B"}
1
+ {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/emit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5B;AAED,yEAAyE;AACzE,KAAK,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEpC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CACrB,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,EAC/B,SAAS,GAAE,SAAoB,GAC9B,gBAAgB,GAAG,SAAS,CAkB9B"}
@@ -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
@@ -511,17 +511,13 @@ function responses(entry, options, contributed, components, envelopes, warn) {
511
511
  for (const [status, schema] of declaredResponses(entry)) {
512
512
  const described = schema === null ? undefined : emitted(schema, "a response", warn);
513
513
  add(status, {
514
- description: describeStatus(status),
515
- placeholder: true,
516
- schemas: described ? branchesFor(described, Number(status), envelopes, warn) : []
514
+ placeholder: describeStatus(status),
515
+ said: described ? wordsOf(described, envelopes) : [],
516
+ schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : []
517
517
  });
518
518
  }
519
519
  if (![...answers.keys()].some((status) => status.startsWith("2"))) {
520
- add("200", {
521
- description: "Successful response",
522
- placeholder: true,
523
- schemas: []
524
- });
520
+ add("200", { placeholder: "Successful response", said: [], schemas: [] });
525
521
  }
526
522
  for (const [status, answer] of failures(entry, options, contributed, components, envelopes, warn)) {
527
523
  add(String(status), answer);
@@ -549,7 +545,12 @@ function failures(entry, options, contributed, components, envelopes, warn) {
549
545
  found.push([
550
546
  options.validationStatus,
551
547
  {
552
- description: "Request failed schema validation",
548
+ said: [
549
+ {
550
+ code: "VALIDATION_FAILED",
551
+ text: "Request failed schema validation"
552
+ }
553
+ ],
553
554
  schemas: [
554
555
  envelopes.ref(options.validationStatus, "VALIDATION_FAILED", envelopes.format.describe({
555
556
  status: options.validationStatus,
@@ -566,7 +567,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
566
567
  found.push([
567
568
  status,
568
569
  {
569
- description: requirement.description ?? `Request did not satisfy ${requirement.name}`,
570
+ said: [
571
+ said(requirement.error, requirement.description ?? `Request did not satisfy ${requirement.name}`)
572
+ ],
570
573
  schemas: [envelopeRef(status, requirement.error, requirement.message)]
571
574
  }
572
575
  ]);
@@ -576,7 +579,7 @@ function failures(entry, options, contributed, components, envelopes, warn) {
576
579
  found.push([
577
580
  response.status,
578
581
  {
579
- description: response.description,
582
+ said: [said(response.error, response.description)],
580
583
  schemas: described ? branchesFor(described, response.status, envelopes, warn) : [
581
584
  envelopeRef(response.status, response.error, response.message, response.fields)
582
585
  ],
@@ -590,7 +593,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
590
593
  found.push([
591
594
  400,
592
595
  {
593
- description: "Body could not be parsed in the declared shape",
596
+ said: [
597
+ said(unparsable.error, "Body could not be parsed in the declared shape")
598
+ ],
594
599
  schemas: [envelopeRef(400, unparsable.error, unparsable.message)]
595
600
  }
596
601
  ]);
@@ -598,7 +603,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
598
603
  found.push([
599
604
  413,
600
605
  {
601
- description: "Body exceeded the configured size limit",
606
+ said: [
607
+ said("BODY_TOO_LARGE", "Body exceeded the configured size limit")
608
+ ],
602
609
  schemas: [
603
610
  envelopeRef(413, "BODY_TOO_LARGE", "Body exceeds the configured limit")
604
611
  ]
@@ -608,7 +615,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
608
615
  found.push([
609
616
  500,
610
617
  {
611
- description: "The request failed and nothing mapped the failure",
618
+ said: [
619
+ said("INTERNAL_SERVER_ERROR", "The request failed and nothing mapped the failure")
620
+ ],
612
621
  schemas: [
613
622
  envelopeRef(500, "INTERNAL_SERVER_ERROR", "Internal Server Error")
614
623
  ]
@@ -616,9 +625,24 @@ function failures(entry, options, contributed, components, envelopes, warn) {
616
625
  ]);
617
626
  return found;
618
627
  }
628
+ function said(code, text) {
629
+ return code === undefined ? { text } : { code, text };
630
+ }
631
+ function wordsOf(schema, envelopes) {
632
+ if (typeof schema.description === "string") {
633
+ return [said(envelopes.format.code(schema), schema.description)];
634
+ }
635
+ return branchesOf(unionOnly(schema), envelopes.format.code).flatMap((branch) => typeof branch.description === "string" ? [said(envelopes.format.code(branch), branch.description)] : []);
636
+ }
637
+ function unionOnly(schema) {
638
+ if (typeof schema.description !== "string" || !Array.isArray(schema.anyOf) && !Array.isArray(schema.oneOf)) {
639
+ return schema;
640
+ }
641
+ const { description: _, ...union } = schema;
642
+ return union;
643
+ }
619
644
  function merge(list, envelopes) {
620
645
  const schemas = distinct(list.flatMap((answer) => answer.schemas));
621
- const worded = list.filter((answer) => !answer.placeholder);
622
646
  const headers = {};
623
647
  for (const answer of list) {
624
648
  for (const [name, header] of Object.entries(answer.headers ?? {})) {
@@ -626,7 +650,7 @@ function merge(list, envelopes) {
626
650
  }
627
651
  }
628
652
  return {
629
- description: (worded.length > 0 ? worded : list).map((answer) => answer.description).join("; "),
653
+ description: wording(list),
630
654
  ...Object.keys(headers).length > 0 ? { headers } : {},
631
655
  ...schemas.length > 0 ? {
632
656
  content: {
@@ -635,6 +659,29 @@ function merge(list, envelopes) {
635
659
  } : {}
636
660
  };
637
661
  }
662
+ function wording(list) {
663
+ const seen = new Set;
664
+ const items = [];
665
+ for (const answer of list) {
666
+ for (const item of answer.said) {
667
+ const key = `${item.code ?? ""}
668
+ ${item.text}`;
669
+ if (!seen.has(key)) {
670
+ seen.add(key);
671
+ items.push(item);
672
+ }
673
+ }
674
+ }
675
+ const [only] = items;
676
+ if (items.length === 1 && only) {
677
+ return only.text;
678
+ }
679
+ if (items.length > 1) {
680
+ return items.map((item) => item.code === undefined ? `- ${item.text}` : `- \`${item.code}\`: ${item.text}`).join(`
681
+ `);
682
+ }
683
+ return list.find((answer) => answer.placeholder)?.placeholder ?? "";
684
+ }
638
685
  function unionOf(schemas, envelopes) {
639
686
  const [only] = schemas;
640
687
  if (schemas.length === 1 && only) {
@@ -800,11 +847,18 @@ class DocsController {
800
847
  this.json = { ...json, path: documentPath };
801
848
  this.ui = options.ui === false ? undefined : { ...ui, path: uiPath };
802
849
  }
850
+ tagsOf(tags) {
851
+ if (!this.options.documentSelf || Object.hasOwn(tags, "docs")) {
852
+ return tags;
853
+ }
854
+ return { ...tags, docs: "This documentation" };
855
+ }
803
856
  [onMount](app) {
804
857
  const generated = openapi(app, {
805
858
  info: this.options.info,
806
859
  ...this.options.servers ? { servers: this.options.servers } : {},
807
- ...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) } : {}
808
862
  });
809
863
  this.document = generated.document;
810
864
  const ui = this.options.ui ?? "scalar";
@@ -828,7 +882,7 @@ function docs(options) {
828
882
  return new DocsController(options, options.path ?? "/openapi.json", options.uiPath ?? "/docs");
829
883
  }
830
884
  function warn(warning) {
831
- console.warn(`[openapi] ${warning.route}: ${warning.message}`);
885
+ console.warn(warning.route === "" ? `[openapi] ${warning.message}` : `[openapi] ${warning.route}: ${warning.message}`);
832
886
  }
833
887
 
834
888
  // packages/openapi/src/index.ts
@@ -844,6 +898,7 @@ function openapi(app, options) {
844
898
  declareEnvelopes(entry, envelopes2);
845
899
  }
846
900
  const occupied = new Map;
901
+ const used = new Map;
847
902
  for (const entry of described) {
848
903
  const template = toTemplate(entry.path);
849
904
  const route = `${entry.method} ${entry.path}`;
@@ -872,22 +927,51 @@ function openapi(app, options) {
872
927
  });
873
928
  }
874
929
  occupied.set(slot, route);
930
+ for (const tag of operation.tags ?? []) {
931
+ if (!used.has(tag)) {
932
+ used.set(tag, route);
933
+ }
934
+ }
875
935
  paths[template] = {
876
936
  ...paths[template],
877
937
  [entry.method.toLowerCase()]: operation
878
938
  };
879
939
  }
940
+ const tags = options.tags ? tagsOf(options.tags, used, warnings) : undefined;
880
941
  return {
881
942
  document: {
882
943
  openapi: "3.1.0",
883
944
  info: options.info,
884
945
  ...options.servers ? { servers: options.servers } : {},
946
+ ...tags ? { tags } : {},
885
947
  paths,
886
948
  ...componentsOf(schemes, components.schemas)
887
949
  },
888
950
  warnings
889
951
  };
890
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
+ }
891
975
  function componentsOf(securitySchemes, schemas) {
892
976
  const components = {
893
977
  ...Object.keys(securitySchemes).length > 0 ? { securitySchemes } : {},
@@ -904,5 +988,5 @@ export {
904
988
  secured
905
989
  };
906
990
 
907
- //# debugId=B1D856E7F5B6819164756E2164756E21
991
+ //# debugId=3A3B60517CA111D464756E2164756E21
908
992
  //# sourceMappingURL=index.js.map