@tetsujs/openapi 0.4.2 → 0.5.0

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
@@ -75,6 +75,7 @@ the hash, to be prefixed with `sha384-`.
75
75
  | `assets` | the renderer on jsDelivr, pinned | your own renderer URLs, with `integrity` hashes |
76
76
  | `documentSelf` | `false` | include `/openapi.json` and `/docs` in the document |
77
77
  | `onWarning` | `console.warn` | receives what could not be described |
78
+ | `errors` | the framework's envelope | an error format of your own — see [below](#an-error-format-of-your-own) |
78
79
 
79
80
  ## What goes into the document
80
81
 
@@ -151,6 +152,82 @@ When every alternative of a status is an envelope, the union carries a
151
152
  `discriminator` on `error`, with the mapping from each code to its
152
153
  definition, and a generated client narrows on the code.
153
154
 
155
+ ## An error format of your own
156
+
157
+ Every failure reaches the application's `onError` hooks — a thrown
158
+ `HttpError`, a validation or body failure, a `404`, a `405`, a rate limit's
159
+ refusal — so one hook answers all of them in your format:
160
+
161
+ ```ts
162
+ const inOurFormat = hook.onError(({ error }) => {
163
+ if (!(error instanceof HttpError)) return undefined;
164
+
165
+ const { status, error: code, ...rest } = error.body as ErrorBody;
166
+
167
+ return Response.json({ code, ...rest }, { status });
168
+ });
169
+ ```
170
+
171
+ The document is generated from the routes, not from that hook, so it is
172
+ told the same format with `errors`:
173
+
174
+ ```ts
175
+ const errors: ErrorFormat = {
176
+ schema: ({ error, message, fields }) => ({
177
+ type: "object",
178
+ required: ["code", "message", ...Object.keys(fields)],
179
+ properties: {
180
+ code: error ? { type: "string", const: error } : { type: "string" },
181
+ message: { type: "string", ...(message ? { examples: [message] } : {}) },
182
+ ...fields,
183
+ },
184
+ }),
185
+ discriminator: "code",
186
+ };
187
+
188
+ createApp({
189
+ hooks: { onError: [inOurFormat] },
190
+ routes: [api, docs({ info, errors })],
191
+ });
192
+ ```
193
+
194
+ `schema` describes one failure from what the generator knows of it: its
195
+ status, its code when known, an example message, and in `fields` what it
196
+ carries besides — `issues` for a validation failure, `retryAfter` for a
197
+ rate limit, a hook's own `fields`. It is used for the framework's failures
198
+ and the hooks' refusals alike.
199
+
200
+ `discriminator` names the top-level field that holds the code. Statuses of
201
+ envelopes are then discriminated on it, and a route's own envelope is
202
+ recognized by it — a `{ code: "ITEM_NOT_FOUND", … }` the route declares is
203
+ the same definition as a hook's. A format that nests its code has no
204
+ top-level field to name; it gives `code` instead, reading the code from a
205
+ route's schema:
206
+
207
+ ```ts
208
+ code: (schema) => {
209
+ const error = schema.properties?.error;
210
+ const code = typeof error === "object" ? error.properties?.code : undefined;
211
+
212
+ return typeof code === "object" && typeof code.const === "string" ? code.const : undefined;
213
+ },
214
+ ```
215
+
216
+ The hook and `errors` describe one format in two places — the core knows
217
+ nothing about documents, and a function cannot be read for the shape it
218
+ returns — so keep a test that holds them together: provoke a failure and
219
+ check its body against the document, with the JSON Schema validator of
220
+ your choice.
221
+
222
+ ```ts
223
+ test("a validation failure is what the document says", async () => {
224
+ const res = await request("/items", { method: "POST", body: "{}" });
225
+ const schema = document.components?.schemas?.ValidationFailed;
226
+
227
+ expect(validator.validate(schema, await res.json())).toBe(true);
228
+ });
229
+ ```
230
+
154
231
  ## Documenting hooks
155
232
 
156
233
  A hook that answers by itself — an auth check, a limiter — can say so, and
@@ -15,6 +15,7 @@
15
15
  import type { App, RouteDef, SchemaConfig, ValidatePath } from "@tetsujs/core";
16
16
  import { onMount } from "@tetsujs/core";
17
17
  import type { OpenApiDocument } from "./document.ts";
18
+ import type { ErrorFormat } from "./errors.ts";
18
19
  import type { GeneratorWarning } from "./index.ts";
19
20
  import type { DocsAssets, DocsUi } from "./page.ts";
20
21
  /** What the documentation controller serves, and from where. */
@@ -23,6 +24,11 @@ export interface DocsOptions<Path extends string, UiPath extends string, Ui exte
23
24
  readonly info: OpenApiDocument["info"];
24
25
  /** Servers the API is reachable at. */
25
26
  readonly servers?: OpenApiDocument["servers"];
27
+ /**
28
+ * How the application's errors look, when not like the framework's
29
+ * envelope — see `errors` on {@link openapi}.
30
+ */
31
+ readonly errors?: ErrorFormat;
26
32
  /** Where the document is served. Defaults to `/openapi.json`. */
27
33
  readonly path?: Path & ValidatePath<Path>;
28
34
  /** Where the page is served. Defaults to `/docs`. */
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAE,OAAO,EAAS,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGpD,gEAAgE;AAChE,MAAM,WAAW,WAAW,CAC1B,IAAI,SAAS,MAAM,EACnB,MAAM,SAAS,MAAM,EACrB,EAAE,SAAS,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK;IAE1C,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,iEAAiE;IACjE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAE1C,qDAAqD;IACrD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAEhD;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAEjB,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAE7B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAEhC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC1D;AAED;;;;;;GAMG;AACH,qBAAa,cAAc,CACzB,IAAI,SAAS,MAAM,EACnB,MAAM,SAAS,MAAM,EACrB,IAAI,SAAS,OAAO,GAAG,IAAI;IAezB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAb1B,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAErE,wDAAwD;IACxD,QAAQ,CAAC,EAAE,EAAE,IAAI,SAAS,IAAI,GAC1B,QAAQ,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,GACvD,SAAS,CAAC;IAEd,OAAO,CAAC,QAAQ,CAA8B;IAE9C,OAAO,CAAC,IAAI,CAAqB;IAEjC,YACmB,OAAO,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,EACnD,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EAkCf;IAED;;;;;;;;OAQG;IACH,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CA0BxB;CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,IAAI,CAClB,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG,eAAe,EAC3C,KAAK,CAAC,MAAM,SAAS,MAAM,GAAG,OAAO,EACrC,KAAK,CAAC,EAAE,SAAS,MAAM,GAAG,KAAK,GAAG,QAAQ,EAE1C,OAAO,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,GACrC,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC,CAM/D"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/controller.ts"],"names":[],"mappings":"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"}
@@ -20,14 +20,21 @@
20
20
  * read first, before any operation is built, so the winner does not depend
21
21
  * on which route happens to come first in the table.
22
22
  *
23
+ * What an envelope looks like — how one is described, where its code is,
24
+ * what a client discriminates on — is the document's error format, which
25
+ * the registry carries for everything that builds or reads one.
26
+ *
23
27
  * Internal to the package.
24
28
  *
25
29
  * @module
26
30
  */
27
31
  import type { SchemaComponents, SchemaRef } from "./components.ts";
28
32
  import type { JsonSchemaObject } from "./emit.ts";
33
+ import type { ResolvedFormat } from "./errors.ts";
29
34
  /** The definitions of every envelope of one document. */
30
35
  export interface Envelopes {
36
+ /** The document's error format. */
37
+ readonly format: ResolvedFormat;
31
38
  /**
32
39
  * Records a route's definition, before anything is referenced. The first
33
40
  * route to declare a status and code defines it.
@@ -48,16 +55,7 @@ export interface Envelopes {
48
55
  */
49
56
  codeOf(ref: string): string | undefined;
50
57
  }
51
- export declare function envelopes(components: SchemaComponents): Envelopes;
52
- /**
53
- * The error code of a schema that is an envelope: an object whose `error`
54
- * is required and holds one string — a `const`, or an `enum` of one value.
55
- *
56
- * Read off what the validator emitted, not off a declaration: the route
57
- * has no other way to say which of its responses is an envelope, and the
58
- * shape is what a client would branch on anyway.
59
- */
60
- export declare function envelopeCode(schema: JsonSchemaObject): string | undefined;
58
+ export declare function envelopes(components: SchemaComponents, format: ResolvedFormat): Envelopes;
61
59
  /**
62
60
  * The alternatives a schema stands for, as a list the status's own union
63
61
  * can take in.
@@ -70,5 +68,5 @@ export declare function envelopeCode(schema: JsonSchemaObject): string | undefin
70
68
  * beside its union, a `title` or a `$defs`, is left whole: those keywords
71
69
  * belong to the union, and would be lost with it.
72
70
  */
73
- export declare function branchesOf(schema: JsonSchemaObject): JsonSchemaObject[];
71
+ export declare function branchesOf(schema: JsonSchemaObject, code: (schema: JsonSchemaObject) => string | undefined): JsonSchemaObject[];
74
72
  //# sourceMappingURL=envelopes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"envelopes.d.ts","sourceRoot":"","sources":["../../src/envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD,yDAAyD;AACzD,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAEtE;;;;;;;OAOG;IACH,GAAG,CACD,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,SAAS,CAAC;IAEb;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACzC;AAED,wBAAgB,SAAS,CAAC,UAAU,EAAE,gBAAgB,GAAG,SAAS,CAoEjE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CA0BzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAwBvE"}
1
+ {"version":3,"file":"envelopes.d.ts","sourceRoot":"","sources":["../../src/envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,yDAAyD;AACzD,MAAM,WAAW,SAAS;IACxB,mCAAmC;IACnC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAEhC;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAEtE;;;;;;;OAOG;IACH,GAAG,CACD,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAC9B,SAAS,CAAC;IAEb;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACzC;AAED,wBAAgB,SAAS,CACvB,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,cAAc,GACrB,SAAS,CAsEX;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,MAAM,GAAG,SAAS,GACrD,gBAAgB,EAAE,CA0BpB"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The shape the document gives an error.
3
+ *
4
+ * Every failure an application answers with reaches its `onError` hooks,
5
+ * so one hook sets the format of all of them. The document is generated
6
+ * from the routes, not from that hook, and cannot see what it does: an
7
+ * application with a format of its own says so here too, and the framework's
8
+ * failures, the hooks' refusals and the routes' own envelopes are described
9
+ * in it. Keeping the two in step is the application's to do — the core knows
10
+ * nothing about documents, and a function cannot be read for the shape it
11
+ * returns.
12
+ *
13
+ * Without it the format is the framework's envelope,
14
+ * `{ status, message, error }`.
15
+ *
16
+ * @module
17
+ */
18
+ import type { JsonSchemaObject } from "./emit.ts";
19
+ import type { JsonSchema, JsonSchemaKeywords } from "./json-schema.ts";
20
+ /**
21
+ * One failure the document describes, as the generator knows it.
22
+ *
23
+ * This is everything known about a failure before it is answered: its
24
+ * status, its code when the framework or the hook knows it, an example of
25
+ * its message, and what else it carries — the `issues` of a validation
26
+ * failure, the `retryAfter` of a rate limit. Where each goes in the body
27
+ * is the format's to decide.
28
+ */
29
+ export interface DocumentedFailure {
30
+ readonly status: number;
31
+ /** The code a client branches on, when it is known. */
32
+ readonly error?: string;
33
+ /** An example of the message; wording, never a contract. */
34
+ readonly message?: string;
35
+ /** What the failure carries besides, as JSON Schema by name. */
36
+ readonly fields: Readonly<Record<string, JsonSchema>>;
37
+ }
38
+ /** How the application's errors look, for the document to describe them. */
39
+ export interface ErrorFormat {
40
+ /** The body of one failure. */
41
+ readonly schema: (failure: DocumentedFailure) => JsonSchemaKeywords;
42
+ /**
43
+ * The top-level field that holds the code, for a client to discriminate
44
+ * on — `"code"` in `{ code, message }`. A status whose alternatives are
45
+ * all envelopes then carries a `discriminator` on it.
46
+ *
47
+ * None unless named: OpenAPI discriminates on a top-level field only, and
48
+ * a format that nests its code — `{ error: { code } }` — has none to
49
+ * name.
50
+ */
51
+ readonly discriminator?: string;
52
+ /**
53
+ * The code of a schema a route or a hook declared, when it is an
54
+ * envelope in this format — how the document knows a route's own `404`
55
+ * is the same failure as a hook's, and gives both one definition.
56
+ *
57
+ * Read off the JSON Schema the validator emitted. By default, the
58
+ * `const` of the discriminator's field, when there is one; a nested
59
+ * format says where its code is.
60
+ */
61
+ readonly code?: (schema: JsonSchemaKeywords) => string | undefined;
62
+ }
63
+ /** What the generator works with: every part of a format, filled in. */
64
+ export interface ResolvedFormat {
65
+ describe(failure: DocumentedFailure): JsonSchemaObject;
66
+ code(schema: JsonSchemaObject): string | undefined;
67
+ readonly discriminator: string | undefined;
68
+ }
69
+ /**
70
+ * A format as the generator uses it. Absent, the framework's envelope,
71
+ * discriminated on `error`.
72
+ */
73
+ export declare function resolveFormat(format: ErrorFormat | undefined): ResolvedFormat;
74
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,uDAAuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAExB,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAE1B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;CACvD;AAED,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,kBAAkB,CAAC;IAEpE;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEhC;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,MAAM,GAAG,SAAS,CAAC;CACpE;AAED,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IACvD,IAAI,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,cAAc,CAqB7E"}
@@ -20,11 +20,13 @@
20
20
  */
21
21
  import type { App } from "@tetsujs/core";
22
22
  import type { OpenApiDocument } from "./document.ts";
23
+ import type { ErrorFormat } from "./errors.ts";
23
24
  export type { DocumentedHeader, DocumentedResponse, HookContributions, HookDocs, SecurityAlternatives, SecurityRequirement, SecurityScheme, } from "./annotations.ts";
24
25
  export { documented, secured } from "./annotations.ts";
25
26
  export type { DocsOptions } from "./controller.ts";
26
27
  export { DocsController, docs } from "./controller.ts";
27
28
  export type { ContentMap, DocumentInfo, DocumentServer, HeaderObject, OpenApiDocument, OperationObject, ParameterObject, PathItemObject, ResponseObject, } from "./document.ts";
29
+ export type { DocumentedFailure, ErrorFormat } from "./errors.ts";
28
30
  export type { JsonSchema, JsonSchemaKeywords, JsonSchemaType, } from "./json-schema.ts";
29
31
  export type { DocsAssets, DocsPageOptions, DocsUi } from "./page.ts";
30
32
  export { docsPage } from "./page.ts";
@@ -36,6 +38,32 @@ export interface OpenApiOptions {
36
38
  readonly info: OpenApiDocument["info"];
37
39
  /** Servers the API is reachable at. */
38
40
  readonly servers?: OpenApiDocument["servers"];
41
+ /**
42
+ * How the application's errors look, when not like the framework's
43
+ * envelope — for an application whose `onError` hook answers in a format
44
+ * of its own. The framework's failures, the hooks' refusals and the
45
+ * routes' own envelopes are then described in it.
46
+ *
47
+ * @example `{ code, message }`, discriminated on `code`
48
+ * ```ts
49
+ * openapi(app, {
50
+ * info,
51
+ * errors: {
52
+ * schema: ({ error, message, fields }) => ({
53
+ * type: "object",
54
+ * required: ["code", "message", ...Object.keys(fields)],
55
+ * properties: {
56
+ * code: error ? { type: "string", const: error } : { type: "string" },
57
+ * message: { type: "string", ...(message ? { examples: [message] } : {}) },
58
+ * ...fields,
59
+ * },
60
+ * }),
61
+ * discriminator: "code",
62
+ * },
63
+ * });
64
+ * ```
65
+ */
66
+ readonly errors?: ErrorFormat;
39
67
  }
40
68
  /**
41
69
  * What the generator could not describe faithfully.
@@ -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;AAKrE,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,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;CAC/C;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,CA2F1E"}
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"}
package/dist/src/index.js CHANGED
@@ -103,7 +103,7 @@ function failureName(status, error) {
103
103
  }
104
104
 
105
105
  // packages/openapi/src/envelopes.ts
106
- function envelopes(components) {
106
+ function envelopes(components, format) {
107
107
  const declared = new Map;
108
108
  const defined = new Map;
109
109
  const refs = new Map;
@@ -111,6 +111,7 @@ function envelopes(components) {
111
111
  const names = new Map;
112
112
  const reported = new Set;
113
113
  return {
114
+ format,
114
115
  declare(status, code, schema) {
115
116
  const key = keyOf(status, code);
116
117
  if (!declared.has(key)) {
@@ -149,31 +150,18 @@ function envelopes(components) {
149
150
  return `${name}${status}`;
150
151
  }
151
152
  }
152
- function envelopeCode(schema) {
153
- const required = Array.isArray(schema.required) ? schema.required : [];
154
- if (!required.includes("error")) {
155
- return;
156
- }
157
- const error = schema.properties?.error;
158
- if (typeof error?.const === "string") {
159
- return error.const;
160
- }
161
- if (Array.isArray(error?.enum) && error.enum.length === 1 && typeof error.enum[0] === "string") {
162
- return error.enum[0];
163
- }
164
- return;
165
- }
166
- function branchesOf(schema) {
153
+ function branchesOf(schema, code) {
167
154
  const keys = Object.keys(schema);
168
155
  if (keys.length !== 1) {
169
156
  return [schema];
170
157
  }
158
+ const within = (branch) => branchesOf(branch, code);
171
159
  if (Array.isArray(schema.anyOf)) {
172
- return schema.anyOf.flatMap(branchesOf);
160
+ return schema.anyOf.flatMap(within);
173
161
  }
174
162
  if (Array.isArray(schema.oneOf)) {
175
- const branches = schema.oneOf.flatMap(branchesOf);
176
- const codes = branches.map(envelopeCode);
163
+ const branches = schema.oneOf.flatMap(within);
164
+ const codes = branches.map(code);
177
165
  if (codes.every((code) => code !== undefined) && new Set(codes).size === codes.length) {
178
166
  return branches;
179
167
  }
@@ -184,17 +172,89 @@ function keyOf(status, code) {
184
172
  return `${status} ${code}`;
185
173
  }
186
174
  function differenceOf(kept, other) {
187
- const fields = (schema) => Object.keys(schema.properties ?? {});
188
- const keptFields = fields(kept);
189
- const otherFields = fields(other);
190
- const missing = otherFields.filter((name) => !keptFields.includes(name));
191
- const extra = keptFields.filter((name) => !otherFields.includes(name));
192
- const changed = [...missing, ...extra];
175
+ const keptShape = shapeOf(kept);
176
+ const otherShape = shapeOf(other);
177
+ const changed = [
178
+ ...otherShape.fields.filter((path) => !keptShape.fields.includes(path)),
179
+ ...keptShape.fields.filter((path) => !otherShape.fields.includes(path))
180
+ ];
193
181
  if (changed.length > 0) {
194
182
  return `fields: ${changed.join(", ")}`;
195
183
  }
196
- const required = (schema) => JSON.stringify([...Array.isArray(schema.required) ? schema.required : []].sort());
197
- return required(kept) === required(other) ? undefined : "which fields are required";
184
+ return JSON.stringify(keptShape.required) === JSON.stringify(otherShape.required) ? undefined : "which fields are required";
185
+ }
186
+ var shapeDepth = 8;
187
+ function shapeOf(schema) {
188
+ const fields = [];
189
+ const required = [];
190
+ const walk = (node, prefix, depth) => {
191
+ const properties = node.properties;
192
+ if (depth > shapeDepth || properties === null || typeof properties !== "object") {
193
+ return;
194
+ }
195
+ const requiredHere = Array.isArray(node.required) ? node.required : [];
196
+ for (const [name, child] of Object.entries(properties)) {
197
+ const path = `${prefix}${name}`;
198
+ fields.push(path);
199
+ if (requiredHere.includes(name)) {
200
+ required.push(path);
201
+ }
202
+ if (child !== null && typeof child === "object") {
203
+ walk(child, `${path}.`, depth + 1);
204
+ }
205
+ }
206
+ };
207
+ walk(schema, "", 0);
208
+ return { fields: fields.sort(), required: required.sort() };
209
+ }
210
+
211
+ // packages/openapi/src/errors.ts
212
+ function resolveFormat(format) {
213
+ if (format === undefined) {
214
+ return {
215
+ describe: envelope,
216
+ code: codeIn("error"),
217
+ discriminator: "error"
218
+ };
219
+ }
220
+ const discriminator = format.discriminator;
221
+ const read = format.code;
222
+ return {
223
+ describe: (failure) => format.schema(failure),
224
+ code: read ? (schema) => read(schema) : discriminator === undefined ? () => {
225
+ return;
226
+ } : codeIn(discriminator),
227
+ discriminator
228
+ };
229
+ }
230
+ function envelope(failure) {
231
+ const own = {
232
+ status: { type: "integer", const: failure.status },
233
+ message: failure.message ? { type: "string", examples: [failure.message] } : { type: "string" },
234
+ error: failure.error ? { type: "string", const: failure.error } : { type: "string" }
235
+ };
236
+ const added = Object.keys(failure.fields).filter((name) => !Object.hasOwn(own, name));
237
+ return {
238
+ type: "object",
239
+ required: ["status", "message", "error", ...added],
240
+ properties: { ...failure.fields, ...own }
241
+ };
242
+ }
243
+ function codeIn(field) {
244
+ return (schema) => {
245
+ const required = Array.isArray(schema.required) ? schema.required : [];
246
+ if (!required.includes(field)) {
247
+ return;
248
+ }
249
+ const value = schema.properties?.[field];
250
+ if (typeof value?.const === "string") {
251
+ return value.const;
252
+ }
253
+ if (Array.isArray(value?.enum) && value.enum.length === 1 && typeof value.enum[0] === "string") {
254
+ return value.enum[0];
255
+ }
256
+ return;
257
+ };
198
258
  }
199
259
 
200
260
  // packages/openapi/src/operation.ts
@@ -315,10 +375,11 @@ function operationOf(entry, options, components, envelopes, ids, warn) {
315
375
  function declareEnvelopes(entry, envelopes) {
316
376
  for (const [status, schema] of declaredResponses(entry)) {
317
377
  const described = schema === null ? undefined : emitted(schema, "", () => {});
318
- for (const branch of described ? branchesOf(described) : []) {
319
- const code = envelopeCode(branch);
320
- if (code !== undefined) {
321
- envelopes.declare(Number(status), code, branch);
378
+ const code = envelopes.format.code;
379
+ for (const branch of described ? branchesOf(described, code) : []) {
380
+ const found = code(branch);
381
+ if (found !== undefined) {
382
+ envelopes.declare(Number(status), found, branch);
322
383
  }
323
384
  }
324
385
  }
@@ -334,8 +395,8 @@ function declaredResponses(entry) {
334
395
  return Object.entries(response);
335
396
  }
336
397
  function branchesFor(schema, status, envelopes, warn) {
337
- return branchesOf(schema).map((branch) => {
338
- const code = envelopeCode(branch);
398
+ return branchesOf(schema, envelopes.format.code).map((branch) => {
399
+ const code = envelopes.format.code(branch);
339
400
  return code === undefined ? branch : envelopes.ref(status, code, branch, warn);
340
401
  });
341
402
  }
@@ -475,7 +536,12 @@ function failures(entry, options, contributed, components, envelopes, warn) {
475
536
  const schema = entry.def.schema;
476
537
  const found = [];
477
538
  const envelopeRef = (status, error, message, fields) => {
478
- const described = envelope(status, error, message, fields);
539
+ const described = envelopes.format.describe({
540
+ status,
541
+ ...error === undefined ? {} : { error },
542
+ ...message === undefined ? {} : { message },
543
+ fields: fields ?? {}
544
+ });
479
545
  const ref = error === undefined ? components.ref(failureName(status), described) : envelopes.ref(status, error, described, warn);
480
546
  return ref;
481
547
  };
@@ -485,7 +551,12 @@ function failures(entry, options, contributed, components, envelopes, warn) {
485
551
  {
486
552
  description: "Request failed schema validation",
487
553
  schemas: [
488
- envelopes.ref(options.validationStatus, "VALIDATION_FAILED", validationFailed(options.validationStatus), warn)
554
+ envelopes.ref(options.validationStatus, "VALIDATION_FAILED", envelopes.format.describe({
555
+ status: options.validationStatus,
556
+ error: "VALIDATION_FAILED",
557
+ message: "Validation failed",
558
+ fields: { issues }
559
+ }), warn)
489
560
  ]
490
561
  }
491
562
  ]);
@@ -569,6 +640,10 @@ function unionOf(schemas, envelopes) {
569
640
  if (schemas.length === 1 && only) {
570
641
  return only;
571
642
  }
643
+ const propertyName = envelopes.format.discriminator;
644
+ if (propertyName === undefined) {
645
+ return { anyOf: schemas };
646
+ }
572
647
  const mapping = {};
573
648
  for (const schema of schemas) {
574
649
  const ref = schema.$ref;
@@ -580,7 +655,7 @@ function unionOf(schemas, envelopes) {
580
655
  }
581
656
  return {
582
657
  anyOf: schemas,
583
- discriminator: { propertyName: "error", mapping }
658
+ discriminator: { propertyName, mapping }
584
659
  };
585
660
  }
586
661
  function distinct(schemas) {
@@ -600,40 +675,17 @@ function describeStatus(status) {
600
675
  }
601
676
  return status.startsWith("2") ? "Successful response" : errorBody(Number(status)).message;
602
677
  }
603
- function envelope(status, error, message, fields = {}) {
604
- const own = {
605
- status: { type: "integer", const: status },
606
- message: message ? { type: "string", examples: [message] } : { type: "string" },
607
- error: error ? { type: "string", const: error } : { type: "string" }
608
- };
609
- const added = Object.keys(fields).filter((name) => !Object.hasOwn(own, name));
610
- return {
678
+ var issues = {
679
+ type: "array",
680
+ items: {
611
681
  type: "object",
612
- required: ["status", "message", "error", ...added],
613
- properties: { ...fields, ...own }
614
- };
615
- }
616
- function validationFailed(status) {
617
- const base = envelope(status, "VALIDATION_FAILED", "Validation failed");
618
- return {
619
- ...base,
620
- required: ["status", "message", "error", "issues"],
682
+ required: ["message", "path"],
621
683
  properties: {
622
- ...base.properties,
623
- issues: {
624
- type: "array",
625
- items: {
626
- type: "object",
627
- required: ["message", "path"],
628
- properties: {
629
- message: { type: "string" },
630
- path: { type: "array", items: { type: ["string", "number"] } }
631
- }
632
- }
633
- }
684
+ message: { type: "string" },
685
+ path: { type: "array", items: { type: ["string", "number"] } }
634
686
  }
635
- };
636
- }
687
+ }
688
+ };
637
689
  function parseFailure(bodyType) {
638
690
  if (bodyType === "text" || bodyType === "stream") {
639
691
  return;
@@ -751,7 +803,8 @@ class DocsController {
751
803
  [onMount](app) {
752
804
  const generated = openapi(app, {
753
805
  info: this.options.info,
754
- ...this.options.servers ? { servers: this.options.servers } : {}
806
+ ...this.options.servers ? { servers: this.options.servers } : {},
807
+ ...this.options.errors ? { errors: this.options.errors } : {}
755
808
  });
756
809
  this.document = generated.document;
757
810
  const ui = this.options.ui ?? "scalar";
@@ -784,7 +837,7 @@ function openapi(app, options) {
784
837
  const warnings = [];
785
838
  const schemes = {};
786
839
  const components = schemaComponents();
787
- const envelopes2 = envelopes(components);
840
+ const envelopes2 = envelopes(components, resolveFormat(options.errors));
788
841
  const ids = operationIds();
789
842
  const described = app.entries.filter((entry) => !entry.ws && !entry.def.docs?.hidden);
790
843
  for (const entry of described) {
@@ -851,5 +904,5 @@ export {
851
904
  secured
852
905
  };
853
906
 
854
- //# debugId=8D8E4C09AE6F825C64756E2164756E21
907
+ //# debugId=B1D856E7F5B6819164756E2164756E21
855
908
  //# sourceMappingURL=index.js.map