@tetsujs/openapi 0.6.1 → 0.6.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
@@ -17,7 +17,14 @@ createApp({
17
17
  });
18
18
  ```
19
19
 
20
- Options, details and recipes: **[tetsujs.com/docs/packages/openapi](https://tetsujs.com/docs/packages/openapi/)**
20
+ ## Documentation
21
+
22
+ - [tetsujs.com/docs/packages/openapi](https://tetsujs.com/docs/packages/openapi/): options, details
23
+ and recipes, also [as Markdown](https://tetsujs.com/docs/packages/openapi.md)
24
+ - [llms-full.txt](https://tetsujs.com/llms-full.txt): the whole documentation in one
25
+ file, for tools and AI assistants
26
+ - every export and option is also documented in the package's type
27
+ definitions, which editors and tools read from `node_modules`
21
28
 
22
29
  ## License
23
30
 
@@ -0,0 +1,28 @@
1
+ // @bun
2
+ // packages/openapi/src/media.ts
3
+ var part = /^[!#$%&'+.^_`|~0-9a-z-]+$/i;
4
+ function isMediaType(value) {
5
+ if (typeof value !== "string") {
6
+ return false;
7
+ }
8
+ const [type, subtype, ...rest] = value.split("/");
9
+ if (type === undefined || subtype === undefined || rest.length > 0) {
10
+ return false;
11
+ }
12
+ if (type === "*") {
13
+ return subtype === "*";
14
+ }
15
+ return part.test(type) && (subtype === "*" || part.test(subtype));
16
+ }
17
+ function isJson(type) {
18
+ return type.toLowerCase() === "application/json";
19
+ }
20
+ function parsesAsJson(type) {
21
+ const lower = type.toLowerCase();
22
+ return lower === "application/json" || lower.endsWith("+json");
23
+ }
24
+
25
+ export { isMediaType, isJson, parsesAsJson };
26
+
27
+ //# debugId=E3642FF098E4375B64756E2164756E21
28
+ //# sourceMappingURL=index-f49v8ege.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/media.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Media types, as the document's `content` keys them.\n *\n * The core checks a route's `contentType` when the route is declared; a\n * response annotated with `documented()` reaches the document without the\n * core seeing it, so the same rule is applied here, when it is annotated.\n *\n * @module\n */\n\n/** One part of a media type: HTTP's token characters, a range's star aside. */\nconst part = /^[!#$%&'+.^_`|~0-9a-z-]+$/i;\n\n/**\n * Whether a value is a media type or a range of them, as a document's\n * `content` names it: `text/csv`, `image/*`, the range of every type — and\n * no parameters, which a key would carry into every comparison with what a\n * response says.\n */\nexport function isMediaType(value: unknown): value is string {\n if (typeof value !== \"string\") {\n return false;\n }\n\n const [type, subtype, ...rest] = value.split(\"/\");\n\n if (type === undefined || subtype === undefined || rest.length > 0) {\n return false;\n }\n\n if (type === \"*\") {\n return subtype === \"*\";\n }\n\n return part.test(type) && (subtype === \"*\" || part.test(subtype));\n}\n\n/**\n * Whether a media type is the one a body is described as by default, and\n * the framework sends a returned value with. Compared without regard to\n * case, as media types are.\n */\nexport function isJson(type: string): boolean {\n return type.toLowerCase() === \"application/json\";\n}\n\n/**\n * Whether a body under this key is JSON to parse: `application/json`, or\n * a type with the `+json` suffix. A range is not — its schema describes\n * the bytes of whatever type answers — and neither is `x-ndjson`, which\n * is lines of JSON, not one value.\n */\nexport function parsesAsJson(type: string): boolean {\n const lower = type.toLowerCase();\n\n return lower === \"application/json\" || lower.endsWith(\"+json\");\n}\n"
6
+ ],
7
+ "mappings": ";;AAWA,IAAM,OAAO;AAQN,SAAS,WAAW,CAAC,OAAiC;AAAA,EAC3D,IAAI,OAAO,UAAU,UAAU;AAAA,IAC7B,OAAO;AAAA,EACT;AAAA,EAEA,OAAO,MAAM,YAAY,QAAQ,MAAM,MAAM,GAAG;AAAA,EAEhD,IAAI,SAAS,aAAa,YAAY,aAAa,KAAK,SAAS,GAAG;AAAA,IAClE,OAAO;AAAA,EACT;AAAA,EAEA,IAAI,SAAS,KAAK;AAAA,IAChB,OAAO,YAAY;AAAA,EACrB;AAAA,EAEA,OAAO,KAAK,KAAK,IAAI,MAAM,YAAY,OAAO,KAAK,KAAK,OAAO;AAAA;AAQ1D,SAAS,MAAM,CAAC,MAAuB;AAAA,EAC5C,OAAO,KAAK,YAAY,MAAM;AAAA;AASzB,SAAS,YAAY,CAAC,MAAuB;AAAA,EAClD,MAAM,QAAQ,KAAK,YAAY;AAAA,EAE/B,OAAO,UAAU,sBAAsB,MAAM,SAAS,OAAO;AAAA;",
8
+ "debugId": "E3642FF098E4375B64756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,12 +1,14 @@
1
1
  /**
2
- * What hooks contribute to the documentation.
2
+ * What hooks, and the handlers packages hand out, contribute to the
3
+ * documentation.
3
4
  *
4
5
  * What a route is protected by, or answers with when a guard refuses, is
5
6
  * not in its config: a hook is a function in a slot, and the route that
6
7
  * mounts it says nothing about tokens or rate limits. The contribution is
7
8
  * therefore annotated onto the hook — from this package, not from
8
9
  * `hook.*` — so the core keeps knowing nothing about OpenAPI and the
9
- * factories stay free of documentation options.
10
+ * factories stay free of documentation options. A package's handler is
11
+ * annotated the same way, for the routes that mount it.
10
12
  *
11
13
  * A hook annotated once is documented everywhere it runs: application and
12
14
  * group chains are merged into every route's chains at startup, so a guard
@@ -53,13 +55,34 @@ export interface SecurityRequirement {
53
55
  }
54
56
  /**
55
57
  * One response a hook can produce, which the route's own schema knows
56
- * nothing about — a rate limiter's `429`, a guard's `401`.
58
+ * nothing about — a rate limiter's `429`, a guard's `401` — or one a
59
+ * package's handler answers with, for the routes that mount it: a file,
60
+ * a `304`.
57
61
  */
58
62
  export interface DocumentedResponse {
59
63
  readonly status: number;
60
64
  readonly description: string;
61
- /** The body, when the hook answers with one. */
65
+ /**
66
+ * The body, when it is described: JSON, or the content under
67
+ * `contentType` when there is one.
68
+ *
69
+ * Without `schema` or `contentType`, an error status — `400` and above —
70
+ * answers with the framework's envelope, which `error`, `message` and
71
+ * `fields` describe, and any other status carries no body: a `304`, a
72
+ * redirect.
73
+ */
62
74
  readonly schema?: AnySchema;
75
+ /**
76
+ * The media type of the body when it is not JSON — `"text/html"`, or a
77
+ * range such as `"image/*"` or the range of every type for a file whose
78
+ * type is not known in advance. Without a `schema`, the body is
79
+ * described by its type alone.
80
+ *
81
+ * Two responses of one status with different types are both in the
82
+ * document, each under its own: the site's `404.html` next to the
83
+ * envelope an API client gets.
84
+ */
85
+ readonly contentType?: string;
63
86
  /**
64
87
  * The `error` code of the failure envelope, when the hook answers with
65
88
  * the framework's shape — `"RATE_LIMITED"`, `"SESSION_EXPIRED"`.
@@ -134,14 +157,58 @@ export interface HookDocs {
134
157
  readonly responses?: readonly DocumentedResponse[];
135
158
  }
136
159
  /**
137
- * Annotates a hook with what it contributes to the documentation.
160
+ * Everything a handler tells the generator about the route it answers
161
+ * for.
162
+ *
163
+ * A handler a package hands out — a directory of files, say — knows what
164
+ * it answers with better than the route it is mounted on, whose author
165
+ * wrote one line: annotated once, it describes every route that mounts it.
166
+ */
167
+ export interface HandlerDocs {
168
+ /**
169
+ * Keeps the route out of the document unless the route says
170
+ * `docs: { hidden: false }`.
171
+ *
172
+ * For a handler whose routes are rarely anyone's API — the files of a
173
+ * site — and which a generated client could not call anyway. The route's
174
+ * own word wins either way: `docs: { hidden: true }` hides it whatever
175
+ * the handler says.
176
+ */
177
+ readonly hidden?: boolean;
178
+ /**
179
+ * What the handler answers with, as the route's own responses: a status
180
+ * the route's response map declares too is described by both, and a
181
+ * route that says nothing gets no placeholder `200` in their place.
182
+ */
183
+ readonly responses?: readonly DocumentedResponse[];
184
+ }
185
+ /** Any handler a route can take; the context is the route's to type. */
186
+ type AnyHandler = (ctx: never) => unknown;
187
+ /**
188
+ * Annotates a hook, or a handler, with what it contributes to the
189
+ * documentation.
138
190
  *
139
191
  * Returns a copy: what the caller exports is what carries the annotation,
140
- * and a hook used elsewhere is not changed behind its author's back. The
141
- * type is preserved exactly, so an annotated hook goes into a stack like
142
- * any other.
192
+ * and a hook or a handler used elsewhere is not changed behind its
193
+ * author's back. A handler's copy calls the handler it was made from. The
194
+ * type is preserved exactly, so an annotated hook goes into a stack, and an
195
+ * annotated handler onto a route, like any other.
143
196
  *
144
- * @example
197
+ * Annotating a handler is for one whose type is already settled — a
198
+ * package's, typed by its own context. An arrow written inside
199
+ * `documented()` on a route does not get the route's context, which
200
+ * `route()` would have given it, and does not compile; its statuses belong
201
+ * in the route's response map.
202
+ *
203
+ * One signature, for a hook and a handler alike, and one shape of what is
204
+ * said about either. An overload for each would keep `hidden` off a hook
205
+ * and `security` off a handler in the types, but TypeScript before 7
206
+ * reports a call no overload matches on the whole call, and a misspelled
207
+ * keyword deep in a response would no longer be pointed at. Those two are
208
+ * refused here instead, when `documented()` is called — as a module loads,
209
+ * before anything is served.
210
+ *
211
+ * @example A hook
145
212
  * ```ts
146
213
  * export const limiter = documented(hook.beforeParse(check), {
147
214
  * responses: [
@@ -149,8 +216,19 @@ export interface HookDocs {
149
216
  * ],
150
217
  * });
151
218
  * ```
219
+ *
220
+ * @example A package's handler, out of the document until a route asks
221
+ * ```ts
222
+ * return documented(serveFile, {
223
+ * hidden: true,
224
+ * responses: [
225
+ * { status: 200, description: "The file", contentType: "image/*" },
226
+ * { status: 304, description: "Not modified" },
227
+ * ],
228
+ * });
229
+ * ```
152
230
  */
153
- export declare function documented<H extends AnyHook>(hook: H, docs: HookDocs): H;
231
+ export declare function documented<T extends AnyHook | AnyHandler>(target: T, docs: HookDocs & HandlerDocs): T;
154
232
  /**
155
233
  * Annotates a hook with the security it enforces — {@link documented} for
156
234
  * the case that comes up most.
@@ -189,6 +267,8 @@ export declare function documented<H extends AnyHook>(hook: H, docs: HookDocs):
189
267
  export declare function secured<H extends AnyHook>(hook: H, requirement: SecurityRequirement | SecurityAlternatives): H;
190
268
  /** Reads everything a hook was annotated with. */
191
269
  export declare function docsOf(hook: AnyHook): HookDocs | undefined;
270
+ /** Reads everything a handler was annotated with. */
271
+ export declare function handlerDocsOf(handler: unknown): HandlerDocs | undefined;
192
272
  /** Reads the security a hook was annotated with, if it was. */
193
273
  export declare function securityOf(hook: AnyHook): SecurityRequirement | SecurityAlternatives | undefined;
194
274
  /** What the hooks of one route contribute, in execution order. */
@@ -209,11 +289,12 @@ export interface HookContributions {
209
289
  /**
210
290
  * Collects the contributions of a route's chains.
211
291
  *
212
- * A scheme is registered once per name and a response once per status and
213
- * description: the same guard on the application and on a group is one
214
- * requirement, not two. Two hooks of one scheme asking for different
215
- * scopes both run, so the requirement asks for the scopes of both — each
216
- * once, in the order they were first asked for.
292
+ * A scheme is registered once per name: two hooks of one scheme asking for
293
+ * different scopes both run, so the requirement asks for the scopes of
294
+ * both — each once, in the order they were first asked for. Responses are
295
+ * kept as the hooks give them. Two guards may answer one status with one
296
+ * description and different codes, and both are what the route sends;
297
+ * answers that say the same are folded when the document is written.
217
298
  *
218
299
  * Deduplicating by name means two hooks claiming one name with different
219
300
  * schemes lose one of them, and that is not a duplicate being collapsed —
@@ -221,4 +302,5 @@ export interface HookContributions {
221
302
  * `warn` is how the caller hears about it; the generator does.
222
303
  */
223
304
  export declare function contributionsOf(chains: Record<string, readonly AnyHook[]>, warn?: (message: string) => void): HookContributions;
305
+ export {};
224
306
  //# sourceMappingURL=annotations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../../src/annotations.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"annotations.d.ts","sourceRoot":"","sources":["../../src/annotations.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Error envelopes, one definition per status and code.
3
3
  *
4
- * An envelope reaches a document from four directions: a route's response
5
- * map, a hook's annotation, a guard's security requirement, and the
6
- * framework's own failures. Each used to be described where it arrived —
4
+ * An envelope reaches a document from five directions: a route's response
5
+ * map, its handler's annotation, a hook's annotation, a guard's security
6
+ * requirement, and the framework's own failures. Each used to be described
7
+ * where it arrived —
7
8
  * a route's inline, the rest by reference — so one `UNAUTHORIZED` could be
8
9
  * listed twice under one status, once as a `$ref` and once as the same
9
10
  * shape without a name, and a generated client got a named type next to an
@@ -14,11 +15,12 @@
14
15
  * discriminable: a client can branch on `error` only if every code under
15
16
  * the status has exactly one shape.
16
17
  *
17
- * The route's definition wins. It is the author's own contract — usually
18
- * the stricter one, a validator's `additionalProperties: false` — where a
19
- * hook's is the framework's generic envelope. The routes are therefore
20
- * read first, before any operation is built, so the winner does not depend
21
- * on which route happens to come first in the table.
18
+ * The route's definition wins, its handler's annotation counting as the
19
+ * route's own. It is the author's own contract — usually the stricter one,
20
+ * a validator's `additionalProperties: false` — where a hook's is the
21
+ * framework's generic envelope. The routes are therefore read first,
22
+ * before any operation is built, so the winner does not depend on which
23
+ * route happens to come first in the table.
22
24
  *
23
25
  * What an envelope looks like — how one is described, where its code is,
24
26
  * what a client discriminates on — is the document's error format, which
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"envelopes.d.ts","sourceRoot":"","sources":["../../src/envelopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;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"}
@@ -21,7 +21,7 @@
21
21
  import type { App } from "@tetsujs/core";
22
22
  import type { OpenApiDocument } from "./document.ts";
23
23
  import type { ErrorFormat } from "./errors.ts";
24
- export type { DocumentedHeader, DocumentedResponse, HookContributions, HookDocs, SecurityAlternatives, SecurityRequirement, SecurityScheme, } from "./annotations.ts";
24
+ export type { DocumentedHeader, DocumentedResponse, HandlerDocs, HookContributions, HookDocs, SecurityAlternatives, SecurityRequirement, SecurityScheme, } from "./annotations.ts";
25
25
  export { documented, secured } from "./annotations.ts";
26
26
  export type { DocsOptions } from "./controller.ts";
27
27
  export { DocsController, docs } from "./controller.ts";
@@ -118,7 +118,9 @@ export interface GeneratorResult {
118
118
  *
119
119
  * So are routes marked `docs: { hidden: true }`, and everything they would
120
120
  * have contributed with them: a security scheme only a hidden route
121
- * requires has nothing left to describe.
121
+ * requires has nothing left to describe. A route that says nothing either
122
+ * way is hidden when its handler was annotated so — see {@link documented}
123
+ * — and `docs: { hidden: false }` shows it all the same.
122
124
  *
123
125
  * Every route of the table becomes an operation under its path template,
124
126
  * with the parameters, body and responses its schemas describe. What the
@@ -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,EAA6B,MAAM,eAAe,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM/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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAmB,MAAM,eAAe,CAAC;AAG1D,OAAO,KAAK,EAAE,eAAe,EAA6B,MAAM,eAAe,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM/C,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,WAAW,EACX,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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe,CAyG1E"}
package/dist/src/index.js CHANGED
@@ -1,7 +1,35 @@
1
1
  // @bun
2
+ import {
3
+ isMediaType,
4
+ isJson
5
+ } from "../index-f49v8ege.js";
6
+
2
7
  // packages/openapi/src/annotations.ts
3
8
  var docsKey = "~tetsu/hook-docs";
4
- function documented(hook, docs) {
9
+ var handlerDocsKey = "~tetsu/handler-docs";
10
+ function documented(target, docs) {
11
+ const handled = typeof target === "function";
12
+ if (handled && docs.security !== undefined) {
13
+ throw new Error("documented() was given security for a handler \u2014 security is stated on the hook that enforces it, with secured()");
14
+ }
15
+ if (!handled && docs.hidden !== undefined) {
16
+ throw new Error("documented() was given hidden for a hook \u2014 a route is hidden by its own docs, or by its handler's annotation, not by a hook it mounts");
17
+ }
18
+ for (const response of docs.responses ?? []) {
19
+ if (response.contentType !== undefined && !isMediaType(response.contentType)) {
20
+ throw new Error(`The ${response.status} response given to documented() has the content type ${JSON.stringify(response.contentType)}, which is not a type such as text/html or a range such as image/* \u2014 the type alone, without parameters: the charset belongs on the response`);
21
+ }
22
+ }
23
+ if (handled) {
24
+ const handler = target;
25
+ const copy = (ctx) => handler(ctx);
26
+ Object.defineProperty(copy, handlerDocsKey, {
27
+ enumerable: false,
28
+ value: { ...handlerDocsOf(handler), ...docs }
29
+ });
30
+ return copy;
31
+ }
32
+ const hook = target;
5
33
  const annotated = { ...hook };
6
34
  Object.defineProperty(annotated, docsKey, {
7
35
  enumerable: false,
@@ -15,10 +43,13 @@ function secured(hook, requirement) {
15
43
  function docsOf(hook) {
16
44
  return hook[docsKey];
17
45
  }
46
+ function handlerDocsOf(handler) {
47
+ return typeof handler === "function" ? handler[handlerDocsKey] : undefined;
48
+ }
18
49
  function contributionsOf(chains, warn) {
19
50
  const security = new Map;
20
51
  const conditions = new Map;
21
- const responses = new Map;
52
+ const responses = [];
22
53
  for (const slot of Object.values(chains)) {
23
54
  for (const hook of slot) {
24
55
  const docs = docsOf(hook);
@@ -42,18 +73,13 @@ function contributionsOf(chains, warn) {
42
73
  }
43
74
  }
44
75
  }
45
- for (const response of docs.responses ?? []) {
46
- const key = `${response.status} ${response.description}`;
47
- if (!responses.has(key)) {
48
- responses.set(key, response);
49
- }
50
- }
76
+ responses.push(...docs.responses ?? []);
51
77
  }
52
78
  }
53
79
  return {
54
80
  security: [...security.values()],
55
81
  conditions: [...conditions.values()],
56
- responses: [...responses.values()]
82
+ responses
57
83
  };
58
84
  }
59
85
  function alternativesOf(security) {
@@ -373,13 +399,25 @@ function operationOf(entry, options, components, envelopes, ids, warn) {
373
399
  };
374
400
  }
375
401
  function declareEnvelopes(entry, envelopes) {
376
- for (const [status, { body }] of declaredResponses(entry)) {
377
- const described = body === null ? undefined : emitted(body, "", () => {});
378
- const code = envelopes.format.code;
402
+ const own = [];
403
+ for (const [status, { body, contentType }] of declaredResponses(entry)) {
404
+ if (body !== null && (contentType === undefined || isJson(contentType))) {
405
+ own.push([Number(status), body]);
406
+ }
407
+ }
408
+ for (const response of handlerDocsOf(entry.def.handler)?.responses ?? []) {
409
+ const { contentType, schema, status } = response;
410
+ if (schema && (contentType === undefined || isJson(contentType))) {
411
+ own.push([status, schema]);
412
+ }
413
+ }
414
+ const code = envelopes.format.code;
415
+ for (const [status, body] of own) {
416
+ const described = emitted(body, "", () => {});
379
417
  for (const branch of described ? branchesOf(described, code) : []) {
380
418
  const found = code(branch);
381
419
  if (found !== undefined) {
382
- envelopes.declare(Number(status), found, branch);
420
+ envelopes.declare(status, found, branch);
383
421
  }
384
422
  }
385
423
  }
@@ -404,7 +442,8 @@ function asDeclared(value) {
404
442
  return {
405
443
  body: value.body ?? null,
406
444
  ...value.headers === undefined ? {} : { headers: value.headers },
407
- ...value.cookies === undefined ? {} : { cookies: value.cookies }
445
+ ...value.cookies === undefined ? {} : { cookies: value.cookies },
446
+ ...value.contentType === undefined ? {} : { contentType: value.contentType }
408
447
  };
409
448
  }
410
449
  function declaredHeaders(declared, warn) {
@@ -562,17 +601,24 @@ function responses(entry, options, contributed, components, envelopes, warn) {
562
601
  answers.set(status, [...answers.get(status) ?? [], answer]);
563
602
  };
564
603
  const declared = declaredResponses(entry);
565
- for (const [status, { body, ...rest }] of declared) {
604
+ for (const [status, { body, contentType, ...rest }] of declared) {
566
605
  const described = body === null ? undefined : emitted(body, "a response", warn);
567
606
  const headers = declaredHeaders({ body, ...rest }, warn);
607
+ const own = contentType !== undefined && !isJson(contentType);
568
608
  add(status, {
569
609
  placeholder: describeStatus(status),
570
610
  said: described ? wordsOf(described, envelopes) : [],
571
- schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : [],
611
+ schemas: described && !own ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : [],
612
+ ...own ? { media: [mediaOf(contentType, described)] } : {},
572
613
  ...headers === undefined ? {} : { headers }
573
614
  });
574
615
  }
575
- if (declared.length === 0) {
616
+ const handled = handlerDocsOf(entry.def.handler)?.responses ?? [];
617
+ const envelopeRef = envelopeRefs(components, envelopes, warn);
618
+ for (const response of handled) {
619
+ add(String(response.status), documentedAnswer(response, "a handler's response", envelopeRef, envelopes, warn));
620
+ }
621
+ if (declared.length === 0 && handled.length === 0) {
576
622
  add("200", { placeholder: "Successful response", said: [], schemas: [] });
577
623
  }
578
624
  for (const [status, answer] of failures(entry, options, contributed, components, envelopes, warn)) {
@@ -587,16 +633,7 @@ function responses(entry, options, contributed, components, envelopes, warn) {
587
633
  function failures(entry, options, contributed, components, envelopes, warn) {
588
634
  const schema = entry.def.schema;
589
635
  const found = [];
590
- const envelopeRef = (status, error, message, fields) => {
591
- const described = envelopes.format.describe({
592
- status,
593
- ...error === undefined ? {} : { error },
594
- ...message === undefined ? {} : { message },
595
- fields: fields ?? {}
596
- });
597
- const ref = error === undefined ? components.ref(failureName(status), described) : envelopes.ref(status, error, described, warn);
598
- return ref;
599
- };
636
+ const envelopeRef = envelopeRefs(components, envelopes, warn);
600
637
  if (schema?.params || schema?.query || schema?.headers || schema?.cookies || schema?.body) {
601
638
  found.push([
602
639
  options.validationStatus,
@@ -626,21 +663,14 @@ function failures(entry, options, contributed, components, envelopes, warn) {
626
663
  said: [
627
664
  said(requirement.error, requirement.description ?? `Request did not satisfy ${requirement.name}`)
628
665
  ],
629
- schemas: [envelopeRef(status, requirement.error, requirement.message)]
666
+ schemas: status >= 400 ? [envelopeRef(status, requirement.error, requirement.message)] : []
630
667
  }
631
668
  ]);
632
669
  }
633
670
  for (const response of contributed.responses) {
634
- const described = response.schema ? emitted(response.schema, "a hook's response", warn) : undefined;
635
671
  found.push([
636
672
  response.status,
637
- {
638
- said: [said(response.error, response.description)],
639
- schemas: described ? branchesFor(described, response.status, envelopes, warn) : [
640
- envelopeRef(response.status, response.error, response.message, response.fields)
641
- ],
642
- ...response.headers ? { headers: response.headers } : {}
643
- }
673
+ documentedAnswer(response, "a hook's response", envelopeRef, envelopes, warn)
644
674
  ]);
645
675
  }
646
676
  if (schema?.body || entry.def.bodyType || entry.def.rawBody) {
@@ -681,6 +711,47 @@ function failures(entry, options, contributed, components, envelopes, warn) {
681
711
  ]);
682
712
  return found;
683
713
  }
714
+ function envelopeRefs(components, envelopes, warn) {
715
+ return (status, error, message, fields) => {
716
+ const described = envelopes.format.describe({
717
+ status,
718
+ ...error === undefined ? {} : { error },
719
+ ...message === undefined ? {} : { message },
720
+ fields: fields ?? {}
721
+ });
722
+ const ref = error === undefined ? components.ref(failureName(status), described) : envelopes.ref(status, error, described, warn);
723
+ return ref;
724
+ };
725
+ }
726
+ function documentedAnswer(response, subject, envelopeRef, envelopes, warn) {
727
+ const described = response.schema ? emitted(response.schema, subject, warn) : undefined;
728
+ const answer = {
729
+ said: [said(response.error, response.description)],
730
+ ...response.headers ? { headers: response.headers } : {}
731
+ };
732
+ if (response.contentType !== undefined && !isJson(response.contentType)) {
733
+ return {
734
+ ...answer,
735
+ schemas: [],
736
+ media: [mediaOf(response.contentType, described)]
737
+ };
738
+ }
739
+ if (described) {
740
+ return {
741
+ ...answer,
742
+ schemas: branchesFor(described, response.status, envelopes, warn)
743
+ };
744
+ }
745
+ return {
746
+ ...answer,
747
+ schemas: response.status >= 400 ? [
748
+ envelopeRef(response.status, response.error, response.message, response.fields)
749
+ ] : []
750
+ };
751
+ }
752
+ function mediaOf(type, schema) {
753
+ return schema === undefined ? { type } : { type, schema };
754
+ }
684
755
  function said(code, text) {
685
756
  return code === undefined ? { text } : { code, text };
686
757
  }
@@ -700,19 +771,33 @@ function unionOnly(schema) {
700
771
  function merge(list, envelopes) {
701
772
  const schemas = distinct(list.flatMap((answer) => answer.schemas));
702
773
  const headers = {};
774
+ const media = new Map;
775
+ const unconstrained = new Set;
703
776
  for (const answer of list) {
704
777
  for (const [name, header] of Object.entries(answer.headers ?? {})) {
705
778
  headers[name] ??= header;
706
779
  }
780
+ for (const { type, schema } of answer.media ?? []) {
781
+ const key = type.toLowerCase();
782
+ const alternatives = media.get(key) ?? [];
783
+ media.set(key, schema ? [...alternatives, schema] : alternatives);
784
+ if (!schema) {
785
+ unconstrained.add(key);
786
+ }
787
+ }
788
+ }
789
+ const content = {};
790
+ if (schemas.length > 0) {
791
+ content["application/json"] = { schema: unionOf(schemas, envelopes) };
792
+ }
793
+ for (const [type, alternatives] of media) {
794
+ const [only, ...others] = distinct(alternatives);
795
+ content[type] = only === undefined || unconstrained.has(type) ? {} : { schema: others.length === 0 ? only : { anyOf: [only, ...others] } };
707
796
  }
708
797
  return {
709
798
  description: wording(list),
710
799
  ...Object.keys(headers).length > 0 ? { headers } : {},
711
- ...schemas.length > 0 ? {
712
- content: {
713
- "application/json": { schema: unionOf(schemas, envelopes) }
714
- }
715
- } : {}
800
+ ...Object.keys(content).length > 0 ? { content } : {}
716
801
  };
717
802
  }
718
803
  function wording(list) {
@@ -728,12 +813,14 @@ ${item.text}`;
728
813
  }
729
814
  }
730
815
  }
731
- const [only] = items;
732
- if (items.length === 1 && only) {
816
+ const coded = new Set(items.flatMap((item) => item.code === undefined ? [] : [item.text]));
817
+ const said = items.filter((item) => item.code !== undefined || !coded.has(item.text));
818
+ const [only] = said;
819
+ if (said.length === 1 && only) {
733
820
  return only.text;
734
821
  }
735
- if (items.length > 1) {
736
- return items.map((item) => item.code === undefined ? `- ${item.text}` : `- \`${item.code}\`: ${item.text}`).join(`
822
+ if (said.length > 1) {
823
+ return said.map((item) => item.code === undefined ? `- ${item.text}` : `- \`${item.code}\`: ${item.text}`).join(`
737
824
  `);
738
825
  }
739
826
  return list.find((answer) => answer.placeholder)?.placeholder ?? "";
@@ -776,8 +863,22 @@ function describeStatus(status) {
776
863
  if (status === "204") {
777
864
  return "No content";
778
865
  }
866
+ if (status === "default") {
867
+ return "Any other response";
868
+ }
869
+ const range = ranges[status.toUpperCase()];
870
+ if (range !== undefined) {
871
+ return range;
872
+ }
779
873
  return status.startsWith("2") ? "Successful response" : errorBody(Number(status)).message;
780
874
  }
875
+ var ranges = {
876
+ "1XX": "Informational response",
877
+ "2XX": "Successful response",
878
+ "3XX": "Redirection",
879
+ "4XX": "Client error",
880
+ "5XX": "Server error"
881
+ };
781
882
  var issues = {
782
883
  type: "array",
783
884
  items: {
@@ -1007,7 +1108,7 @@ function openapi(app, options) {
1007
1108
  const components = schemaComponents();
1008
1109
  const envelopes2 = envelopes(components, resolveFormat(options.errors));
1009
1110
  const ids = operationIds();
1010
- const described = app.entries.filter((entry) => !entry.ws && !entry.def.docs?.hidden);
1111
+ const described = app.entries.filter((entry) => !entry.ws && !hidden(entry));
1011
1112
  for (const entry of described) {
1012
1113
  declareEnvelopes(entry, envelopes2);
1013
1114
  }
@@ -1063,6 +1164,9 @@ function openapi(app, options) {
1063
1164
  refsOf(document, occupied, warnings);
1064
1165
  return { document, warnings };
1065
1166
  }
1167
+ function hidden(entry) {
1168
+ return entry.def.docs?.hidden ?? handlerDocsOf(entry.def.handler)?.hidden ?? false;
1169
+ }
1066
1170
  function refsOf(document, routes, warnings) {
1067
1171
  const unresolved = (ref) => `refers to "${ref}", which is no schema in the document: references are embedded as they are, and resolve against the document's root`;
1068
1172
  for (const [template, item] of Object.entries(document.paths)) {
@@ -1121,5 +1225,5 @@ export {
1121
1225
  secured
1122
1226
  };
1123
1227
 
1124
- //# debugId=A9BC9A29B6C6C1DA64756E2164756E21
1228
+ //# debugId=94105F31CF41593E64756E2164756E21
1125
1229
  //# sourceMappingURL=index.js.map