counterfact 2.6.0 → 2.7.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 +103 -141
- package/bin/README.md +24 -4
- package/bin/counterfact.js +44 -1
- package/dist/app.js +15 -16
- package/dist/counterfact-types/cookie-options.js +1 -0
- package/dist/counterfact-types/counterfact-response.js +7 -0
- package/dist/counterfact-types/example-names.js +1 -0
- package/dist/counterfact-types/example.js +1 -0
- package/dist/counterfact-types/generic-response-builder.js +1 -0
- package/dist/counterfact-types/http-status-code.js +1 -0
- package/dist/counterfact-types/if-has-key.js +1 -0
- package/dist/counterfact-types/index.js +0 -1
- package/dist/counterfact-types/maybe-promise.js +1 -0
- package/dist/counterfact-types/media-type.js +1 -0
- package/dist/counterfact-types/omit-all.js +1 -0
- package/dist/counterfact-types/omit-value-when-never.js +1 -0
- package/dist/counterfact-types/open-api-content.js +1 -0
- package/dist/counterfact-types/open-api-operation.js +1 -0
- package/dist/counterfact-types/open-api-parameters.js +1 -0
- package/dist/counterfact-types/open-api-response.js +1 -0
- package/dist/counterfact-types/random-function.js +1 -0
- package/dist/counterfact-types/response-builder-factory.js +1 -0
- package/dist/counterfact-types/response-builder.js +1 -0
- package/dist/counterfact-types/wide-operation-argument.js +1 -0
- package/dist/counterfact-types/wide-response-builder.js +1 -0
- package/dist/repl/repl.js +96 -3
- package/dist/server/context-registry.js +17 -1
- package/dist/server/counterfact-types/cookie-options.ts +14 -0
- package/dist/server/counterfact-types/counterfact-response.ts +15 -0
- package/dist/server/counterfact-types/example-names.ts +13 -0
- package/dist/server/counterfact-types/example.ts +10 -0
- package/dist/server/counterfact-types/generic-response-builder.ts +164 -0
- package/dist/server/counterfact-types/http-status-code.ts +62 -0
- package/dist/server/counterfact-types/if-has-key.ts +19 -0
- package/dist/server/counterfact-types/index.ts +20 -338
- package/dist/server/counterfact-types/maybe-promise.ts +6 -0
- package/dist/server/counterfact-types/media-type.ts +6 -0
- package/dist/server/counterfact-types/omit-all.ts +11 -0
- package/dist/server/counterfact-types/omit-value-when-never.ts +11 -0
- package/dist/server/counterfact-types/open-api-content.ts +8 -0
- package/dist/server/counterfact-types/open-api-operation.ts +36 -0
- package/dist/server/counterfact-types/open-api-parameters.ts +16 -0
- package/dist/server/counterfact-types/open-api-response.ts +22 -0
- package/dist/server/counterfact-types/random-function.ts +9 -0
- package/dist/server/counterfact-types/response-builder-factory.ts +16 -0
- package/dist/server/counterfact-types/response-builder.ts +31 -0
- package/dist/server/counterfact-types/wide-operation-argument.ts +17 -0
- package/dist/server/counterfact-types/wide-response-builder.ts +26 -0
- package/dist/server/create-koa-app.js +1 -20
- package/dist/server/dispatcher.js +18 -5
- package/dist/server/json-to-xml.js +1 -1
- package/dist/server/koa-middleware.js +7 -1
- package/dist/server/load-openapi-document.js +13 -0
- package/dist/server/module-loader.js +76 -4
- package/dist/server/openapi-watcher.js +35 -0
- package/dist/server/request-validator.js +3 -7
- package/dist/server/response-builder.js +3 -0
- package/dist/server/response-validator.js +58 -0
- package/dist/server/scenario-registry.js +29 -0
- package/dist/server/tools.js +2 -2
- package/dist/typescript-generator/coder.js +4 -2
- package/dist/typescript-generator/generate.js +155 -0
- package/dist/typescript-generator/operation-coder.js +1 -1
- package/dist/typescript-generator/operation-type-coder.js +1 -49
- package/dist/typescript-generator/read-only-comments.js +1 -1
- package/dist/typescript-generator/requirement.js +8 -1
- package/dist/typescript-generator/reserved-words.js +50 -0
- package/dist/util/load-config-file.js +44 -0
- package/package.json +7 -8
- package/dist/client/README.md +0 -14
- package/dist/client/index.html.hbs +0 -244
- package/dist/client/rapi-doc.html.hbs +0 -36
- package/dist/server/page-middleware.js +0 -23
package/dist/repl/repl.js
CHANGED
|
@@ -15,8 +15,51 @@ const ROUTE_BUILDER_METHODS = [
|
|
|
15
15
|
"ready(",
|
|
16
16
|
"send(",
|
|
17
17
|
];
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Creates a tab-completion function for the REPL.
|
|
20
|
+
*
|
|
21
|
+
* @param registry - The route registry used to complete path arguments for `route()` and `client.*()` calls.
|
|
22
|
+
* @param fallback - Optional fallback completer (e.g. the Node.js built-in completer) invoked when no custom completion matches.
|
|
23
|
+
* @param scenarioRegistry - When provided, enables tab completion for `.apply` commands by enumerating
|
|
24
|
+
* exported function names and file-key prefixes from the loaded scenario modules.
|
|
25
|
+
*/
|
|
26
|
+
export function createCompleter(registry, fallback, scenarioRegistry) {
|
|
19
27
|
return (line, callback) => {
|
|
28
|
+
// Check for .apply completion: .apply <partial>
|
|
29
|
+
const applyMatch = line.match(/^\.apply\s+(?<partial>\S*)$/u);
|
|
30
|
+
if (applyMatch) {
|
|
31
|
+
const partial = applyMatch.groups?.["partial"] ?? "";
|
|
32
|
+
if (scenarioRegistry !== undefined) {
|
|
33
|
+
const slashIdx = partial.lastIndexOf("/");
|
|
34
|
+
if (slashIdx === -1) {
|
|
35
|
+
// No slash: complete exports from "index" key + top-level file prefixes
|
|
36
|
+
const indexFunctions = scenarioRegistry.getExportedFunctionNames("index");
|
|
37
|
+
const fileKeys = scenarioRegistry
|
|
38
|
+
.getFileKeys()
|
|
39
|
+
.filter((k) => k !== "index");
|
|
40
|
+
const topLevelPrefixes = [
|
|
41
|
+
...new Set(fileKeys.map((k) => k.split("/")[0] + "/")),
|
|
42
|
+
];
|
|
43
|
+
const allOptions = [...indexFunctions, ...topLevelPrefixes];
|
|
44
|
+
const matches = allOptions.filter((c) => c.startsWith(partial));
|
|
45
|
+
callback(null, [matches, partial]);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Has slash: complete exports from the named file key
|
|
49
|
+
const fileKey = partial.slice(0, slashIdx);
|
|
50
|
+
const funcPartial = partial.slice(slashIdx + 1);
|
|
51
|
+
const functions = scenarioRegistry.getExportedFunctionNames(fileKey);
|
|
52
|
+
const matches = functions
|
|
53
|
+
.filter((e) => e.startsWith(funcPartial))
|
|
54
|
+
.map((e) => `${fileKey}/${e}`);
|
|
55
|
+
callback(null, [matches, partial]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
callback(null, [[], partial]);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
20
63
|
// Check for RouteBuilder method completion: route("..."). or chained calls
|
|
21
64
|
const builderMatch = line.match(/route\(.*\)\.(?<partial>[a-zA-Z]*)$/u);
|
|
22
65
|
if (builderMatch) {
|
|
@@ -41,7 +84,7 @@ export function createCompleter(registry, fallback) {
|
|
|
41
84
|
callback(null, [matches, partial]);
|
|
42
85
|
};
|
|
43
86
|
}
|
|
44
|
-
export function startRepl(contextRegistry, registry, config, print = printToStdout, openApiDocument) {
|
|
87
|
+
export function startRepl(contextRegistry, registry, config, print = printToStdout, openApiDocument, scenarioRegistry) {
|
|
45
88
|
function printProxyStatus() {
|
|
46
89
|
if (config.proxyUrl === "") {
|
|
47
90
|
print("The proxy URL is not set.");
|
|
@@ -85,7 +128,7 @@ export function startRepl(contextRegistry, registry, config, print = printToStdo
|
|
|
85
128
|
const builtinCompleter = replServer.completer;
|
|
86
129
|
// completer is typed as readonly in @types/node but is writable at runtime
|
|
87
130
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
-
replServer.completer = createCompleter(registry, builtinCompleter);
|
|
131
|
+
replServer.completer = createCompleter(registry, builtinCompleter, scenarioRegistry);
|
|
89
132
|
replServer.defineCommand("counterfact", {
|
|
90
133
|
action() {
|
|
91
134
|
print("This is a read-eval-print loop (REPL), the same as the one you get when you run node with no arguments.");
|
|
@@ -129,5 +172,55 @@ export function startRepl(contextRegistry, registry, config, print = printToStdo
|
|
|
129
172
|
replServer.context.client = new RawHttpClient("localhost", config.port);
|
|
130
173
|
replServer.context.RawHttpClient = RawHttpClient;
|
|
131
174
|
replServer.context.route = createRouteFunction(config.port, "localhost", openApiDocument);
|
|
175
|
+
replServer.context.routes = {};
|
|
176
|
+
replServer.defineCommand("apply", {
|
|
177
|
+
async action(text) {
|
|
178
|
+
const parts = text.trim().split("/").filter(Boolean);
|
|
179
|
+
if (parts.length === 0) {
|
|
180
|
+
print("usage: .apply <path>");
|
|
181
|
+
this.clearBufferedCommand();
|
|
182
|
+
this.displayPrompt();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (parts.some((part) => part === ".." || part === ".")) {
|
|
186
|
+
print("Error: Path must not contain '.' or '..' segments");
|
|
187
|
+
this.clearBufferedCommand();
|
|
188
|
+
this.displayPrompt();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const functionName = parts[parts.length - 1] ?? "";
|
|
192
|
+
const fileKey = parts.length === 1 ? "index" : parts.slice(0, -1).join("/");
|
|
193
|
+
const module = scenarioRegistry?.getModule(fileKey);
|
|
194
|
+
if (module === undefined) {
|
|
195
|
+
print(`Error: Could not find scenario file "${fileKey}"`);
|
|
196
|
+
this.clearBufferedCommand();
|
|
197
|
+
this.displayPrompt();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const fn = module[functionName];
|
|
201
|
+
if (typeof fn !== "function") {
|
|
202
|
+
print(`Error: "${functionName}" is not a function exported from "${fileKey}"`);
|
|
203
|
+
this.clearBufferedCommand();
|
|
204
|
+
this.displayPrompt();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const applyContext = {
|
|
209
|
+
context: replServer.context["context"],
|
|
210
|
+
loadContext: replServer.context["loadContext"],
|
|
211
|
+
route: replServer.context["route"],
|
|
212
|
+
routes: replServer.context["routes"],
|
|
213
|
+
};
|
|
214
|
+
await fn(applyContext);
|
|
215
|
+
print(`Applied ${text.trim()}`);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
print(`Error: ${String(error)}`);
|
|
219
|
+
}
|
|
220
|
+
this.clearBufferedCommand();
|
|
221
|
+
this.displayPrompt();
|
|
222
|
+
},
|
|
223
|
+
help: 'apply a scenario script (".apply <path>" calls the named export from scenarios/)',
|
|
224
|
+
});
|
|
132
225
|
return replServer;
|
|
133
226
|
}
|
|
@@ -29,11 +29,12 @@ function cloneForCache(value) {
|
|
|
29
29
|
}
|
|
30
30
|
return clone;
|
|
31
31
|
}
|
|
32
|
-
export class ContextRegistry {
|
|
32
|
+
export class ContextRegistry extends EventTarget {
|
|
33
33
|
entries = new Map();
|
|
34
34
|
cache = new Map();
|
|
35
35
|
seen = new Set();
|
|
36
36
|
constructor() {
|
|
37
|
+
super();
|
|
37
38
|
this.add("/", {});
|
|
38
39
|
}
|
|
39
40
|
getContextIgnoreCase(map, key) {
|
|
@@ -48,6 +49,21 @@ export class ContextRegistry {
|
|
|
48
49
|
add(path, context) {
|
|
49
50
|
this.entries.set(path, context);
|
|
50
51
|
this.cache.set(path, cloneForCache(context));
|
|
52
|
+
this.dispatchEvent(new Event("context-changed"));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Removes the context entry for the given path and dispatches a
|
|
56
|
+
* "context-changed" event so that listeners (e.g. the scenario-context type
|
|
57
|
+
* generator) can regenerate type files in response to the removal.
|
|
58
|
+
*
|
|
59
|
+
* @param path - The route path whose context entry should be deleted
|
|
60
|
+
* (e.g. "/pets").
|
|
61
|
+
*/
|
|
62
|
+
remove(path) {
|
|
63
|
+
this.entries.delete(path);
|
|
64
|
+
this.cache.delete(path);
|
|
65
|
+
this.seen.delete(path);
|
|
66
|
+
this.dispatchEvent(new Event("context-changed"));
|
|
51
67
|
}
|
|
52
68
|
find(path) {
|
|
53
69
|
return (this.getContextIgnoreCase(this.entries, path) ??
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for setting an HTTP cookie on a response.
|
|
3
|
+
* These correspond to standard `Set-Cookie` attributes and are passed to the
|
|
4
|
+
* `.cookie()` method on the response builder.
|
|
5
|
+
*/
|
|
6
|
+
export interface CookieOptions {
|
|
7
|
+
domain?: string;
|
|
8
|
+
expires?: Date;
|
|
9
|
+
httpOnly?: boolean;
|
|
10
|
+
maxAge?: number;
|
|
11
|
+
path?: string;
|
|
12
|
+
sameSite?: "lax" | "none" | "strict";
|
|
13
|
+
secure?: boolean;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A unique symbol used as a brand for the `COUNTERFACT_RESPONSE` type.
|
|
3
|
+
* This prevents arbitrary objects from being accidentally treated as a
|
|
4
|
+
* completed response value.
|
|
5
|
+
*/
|
|
6
|
+
const counterfactResponse = Symbol("Counterfact Response");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The terminal value type returned by the fluent response builder once all
|
|
10
|
+
* required fields (body, headers, etc.) have been provided. When a route
|
|
11
|
+
* handler returns this type, Counterfact treats the response as complete.
|
|
12
|
+
*/
|
|
13
|
+
export type COUNTERFACT_RESPONSE = {
|
|
14
|
+
[counterfactResponse]: typeof counterfactResponse;
|
|
15
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OpenApiResponse } from "./open-api-response.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts the union of named example keys defined on an OpenAPI response.
|
|
5
|
+
* Resolves to `never` when the response has no named examples.
|
|
6
|
+
* Used to constrain the argument to the `.example(name)` method on the
|
|
7
|
+
* response builder.
|
|
8
|
+
*/
|
|
9
|
+
export type ExampleNames<Response extends OpenApiResponse> = Response extends {
|
|
10
|
+
examples: infer E;
|
|
11
|
+
}
|
|
12
|
+
? keyof E & string
|
|
13
|
+
: never;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a named example defined in an OpenAPI document.
|
|
3
|
+
* Examples can be referenced by route handlers via the `.example(name)` method
|
|
4
|
+
* on the response builder.
|
|
5
|
+
*/
|
|
6
|
+
export interface Example {
|
|
7
|
+
description: string;
|
|
8
|
+
summary: string;
|
|
9
|
+
value: unknown;
|
|
10
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { COUNTERFACT_RESPONSE } from "./counterfact-response.js";
|
|
2
|
+
import type { CookieOptions } from "./cookie-options.js";
|
|
3
|
+
import type { ExampleNames } from "./example-names.js";
|
|
4
|
+
import type { IfHasKey } from "./if-has-key.js";
|
|
5
|
+
import type { MediaType } from "./media-type.js";
|
|
6
|
+
import type { OmitAll } from "./omit-all.js";
|
|
7
|
+
import type { OmitValueWhenNever } from "./omit-value-when-never.js";
|
|
8
|
+
import type { OpenApiResponse } from "./open-api-response.js";
|
|
9
|
+
import type { RandomFunction } from "./random-function.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns `never` when `Record` is an empty object type (`{}`), signalling
|
|
13
|
+
* that there are no remaining choices available on the response builder.
|
|
14
|
+
*/
|
|
15
|
+
type NeverIfEmpty<Record> = object extends Record ? never : Record;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extracts the union of schema types from a map of media-type content entries.
|
|
19
|
+
* Used to type the body argument of shortcut methods like `.json()` or `.html()`.
|
|
20
|
+
*/
|
|
21
|
+
type SchemasOf<T extends { [key: string]: { schema: unknown } }> = {
|
|
22
|
+
[K in keyof T]: T[K]["schema"];
|
|
23
|
+
}[keyof T];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Produces a builder method for a shortcut (e.g. `.json()`, `.html()`) when
|
|
27
|
+
* the response contains at least one of the given `ContentTypes`, and `never`
|
|
28
|
+
* otherwise. Calling the method narrows the builder by removing those content
|
|
29
|
+
* types from the remaining options.
|
|
30
|
+
*/
|
|
31
|
+
type MaybeShortcut<
|
|
32
|
+
ContentTypes extends MediaType[],
|
|
33
|
+
Response extends OpenApiResponse,
|
|
34
|
+
> = IfHasKey<
|
|
35
|
+
Response["content"],
|
|
36
|
+
ContentTypes,
|
|
37
|
+
(body: SchemasOf<Response["content"]>) => GenericResponseBuilder<{
|
|
38
|
+
content: NeverIfEmpty<OmitAll<Response["content"], ContentTypes>>;
|
|
39
|
+
headers: Response["headers"];
|
|
40
|
+
requiredHeaders: Response["requiredHeaders"];
|
|
41
|
+
}>,
|
|
42
|
+
never
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The type of the `.match(contentType, body)` method on the generic response
|
|
47
|
+
* builder. Calling it narrows the builder by removing the chosen content type
|
|
48
|
+
* from the remaining options.
|
|
49
|
+
*/
|
|
50
|
+
type MatchFunction<Response extends OpenApiResponse> = <
|
|
51
|
+
ContentType extends MediaType & keyof Response["content"],
|
|
52
|
+
>(
|
|
53
|
+
contentType: ContentType,
|
|
54
|
+
body: Response["content"][ContentType]["schema"],
|
|
55
|
+
) => GenericResponseBuilder<{
|
|
56
|
+
content: NeverIfEmpty<Omit<Response["content"], ContentType>>;
|
|
57
|
+
headers: Response["headers"];
|
|
58
|
+
requiredHeaders: Response["requiredHeaders"];
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The type of the `.header(name, value)` method on the generic response
|
|
63
|
+
* builder. Calling it narrows the builder by removing the satisfied header
|
|
64
|
+
* from the set of required headers.
|
|
65
|
+
*/
|
|
66
|
+
type HeaderFunction<Response extends OpenApiResponse> = <
|
|
67
|
+
Header extends string & keyof Response["headers"],
|
|
68
|
+
>(
|
|
69
|
+
header: Header,
|
|
70
|
+
value: Response["headers"][Header]["schema"],
|
|
71
|
+
) => GenericResponseBuilder<{
|
|
72
|
+
content: NeverIfEmpty<Response["content"]>;
|
|
73
|
+
headers: NeverIfEmpty<Omit<Response["headers"], Header>>;
|
|
74
|
+
requiredHeaders: Exclude<Response["requiredHeaders"], Header>;
|
|
75
|
+
}>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The inner shape of the generic response builder, listing all methods that
|
|
79
|
+
* are currently available given the remaining response constraints.
|
|
80
|
+
* Methods whose type resolves to `never` are stripped by `OmitValueWhenNever`.
|
|
81
|
+
*
|
|
82
|
+
* Note: `[T] extends [never]` (non-distributive tuple wrapping) is used
|
|
83
|
+
* alongside `[keyof T] extends [never]` to correctly handle both `T = never`
|
|
84
|
+
* (spec-generated no-body) and `T = {}` (all content types consumed) cases.
|
|
85
|
+
* TypeScript evaluates `keyof never` as `string | number | symbol`, so a
|
|
86
|
+
* direct `[keyof never] extends [never]` check would incorrectly return false.
|
|
87
|
+
*/
|
|
88
|
+
export type GenericResponseBuilderInner<
|
|
89
|
+
Response extends OpenApiResponse = OpenApiResponse,
|
|
90
|
+
> = OmitValueWhenNever<{
|
|
91
|
+
binary: MaybeShortcut<["application/octet-stream"], Response>;
|
|
92
|
+
cookie: (
|
|
93
|
+
name: string,
|
|
94
|
+
value: string,
|
|
95
|
+
options?: CookieOptions,
|
|
96
|
+
) => GenericResponseBuilder<Response>;
|
|
97
|
+
empty: [Response["content"]] extends [never]
|
|
98
|
+
? () => COUNTERFACT_RESPONSE
|
|
99
|
+
: [keyof Response["content"]] extends [never]
|
|
100
|
+
? () => COUNTERFACT_RESPONSE
|
|
101
|
+
: never;
|
|
102
|
+
header: [Response["headers"]] extends [never]
|
|
103
|
+
? never
|
|
104
|
+
: [keyof Response["headers"]] extends [never]
|
|
105
|
+
? never
|
|
106
|
+
: HeaderFunction<Response>;
|
|
107
|
+
html: MaybeShortcut<["text/html"], Response>;
|
|
108
|
+
json: MaybeShortcut<
|
|
109
|
+
[
|
|
110
|
+
"application/json",
|
|
111
|
+
"text/json",
|
|
112
|
+
"text/x-json",
|
|
113
|
+
"application/xml",
|
|
114
|
+
"text/xml",
|
|
115
|
+
],
|
|
116
|
+
Response
|
|
117
|
+
>;
|
|
118
|
+
match: [Response["content"]] extends [never]
|
|
119
|
+
? never
|
|
120
|
+
: [keyof Response["content"]] extends [never]
|
|
121
|
+
? never
|
|
122
|
+
: MatchFunction<Response>;
|
|
123
|
+
random: [Response["content"]] extends [never]
|
|
124
|
+
? never
|
|
125
|
+
: [keyof Response["content"]] extends [never]
|
|
126
|
+
? never
|
|
127
|
+
: RandomFunction;
|
|
128
|
+
example: [ExampleNames<Response>] extends [never]
|
|
129
|
+
? never
|
|
130
|
+
: (name: ExampleNames<Response>) => COUNTERFACT_RESPONSE;
|
|
131
|
+
text: MaybeShortcut<["text/plain"], Response>;
|
|
132
|
+
xml: MaybeShortcut<["application/xml", "text/xml"], Response>;
|
|
133
|
+
}>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The strongly-typed, fluent response builder generated for each operation in
|
|
137
|
+
* a route handler. Its available methods are derived from the OpenAPI response
|
|
138
|
+
* schema: as methods are called, the builder type narrows until all required
|
|
139
|
+
* content and headers have been provided, at which point it resolves to
|
|
140
|
+
* `COUNTERFACT_RESPONSE`.
|
|
141
|
+
*
|
|
142
|
+
* When a Response type carries an `examples` key it is a spec-generated
|
|
143
|
+
* response (either the initial no-body builder or a builder that still has
|
|
144
|
+
* content/headers to satisfy). Those always go through
|
|
145
|
+
* `GenericResponseBuilderInner`, which exposes `empty()` when `content` is
|
|
146
|
+
* `never`.
|
|
147
|
+
*
|
|
148
|
+
* When a Response type has no `examples` key it is a narrowed type produced
|
|
149
|
+
* by a method call (e.g. `.json()` sets the body and returns a type without
|
|
150
|
+
* `examples`). Those go through the existing collapse logic so that
|
|
151
|
+
* fully-satisfied responses resolve directly to `COUNTERFACT_RESPONSE`.
|
|
152
|
+
*/
|
|
153
|
+
export type GenericResponseBuilder<
|
|
154
|
+
Response extends OpenApiResponse = OpenApiResponse,
|
|
155
|
+
> = "examples" extends keyof Response
|
|
156
|
+
? GenericResponseBuilderInner<Response>
|
|
157
|
+
: object extends OmitValueWhenNever<Omit<Response, "examples">>
|
|
158
|
+
? COUNTERFACT_RESPONSE
|
|
159
|
+
: keyof OmitValueWhenNever<Omit<Response, "examples">> extends "headers"
|
|
160
|
+
? {
|
|
161
|
+
ALL_REMAINING_HEADERS_ARE_OPTIONAL: COUNTERFACT_RESPONSE;
|
|
162
|
+
header: HeaderFunction<Response>;
|
|
163
|
+
}
|
|
164
|
+
: GenericResponseBuilderInner<Response>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A union of all standard HTTP status codes.
|
|
3
|
+
* Used to constrain the status code argument in response builder calls and
|
|
4
|
+
* generated route handler types.
|
|
5
|
+
*/
|
|
6
|
+
export type HttpStatusCode =
|
|
7
|
+
| 100
|
|
8
|
+
| 101
|
|
9
|
+
| 102
|
|
10
|
+
| 200
|
|
11
|
+
| 201
|
|
12
|
+
| 202
|
|
13
|
+
| 203
|
|
14
|
+
| 204
|
|
15
|
+
| 205
|
|
16
|
+
| 206
|
|
17
|
+
| 207
|
|
18
|
+
| 226
|
|
19
|
+
| 300
|
|
20
|
+
| 301
|
|
21
|
+
| 302
|
|
22
|
+
| 303
|
|
23
|
+
| 304
|
|
24
|
+
| 305
|
|
25
|
+
| 307
|
|
26
|
+
| 308
|
|
27
|
+
| 400
|
|
28
|
+
| 401
|
|
29
|
+
| 402
|
|
30
|
+
| 403
|
|
31
|
+
| 404
|
|
32
|
+
| 405
|
|
33
|
+
| 406
|
|
34
|
+
| 407
|
|
35
|
+
| 408
|
|
36
|
+
| 409
|
|
37
|
+
| 410
|
|
38
|
+
| 411
|
|
39
|
+
| 412
|
|
40
|
+
| 413
|
|
41
|
+
| 414
|
|
42
|
+
| 415
|
|
43
|
+
| 416
|
|
44
|
+
| 417
|
|
45
|
+
| 418
|
|
46
|
+
| 422
|
|
47
|
+
| 423
|
|
48
|
+
| 424
|
|
49
|
+
| 426
|
|
50
|
+
| 428
|
|
51
|
+
| 429
|
|
52
|
+
| 431
|
|
53
|
+
| 451
|
|
54
|
+
| 500
|
|
55
|
+
| 501
|
|
56
|
+
| 502
|
|
57
|
+
| 503
|
|
58
|
+
| 504
|
|
59
|
+
| 505
|
|
60
|
+
| 506
|
|
61
|
+
| 507
|
|
62
|
+
| 511;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conditional type that resolves to `Yes` when `SomeObject` has at least one
|
|
3
|
+
* key that contains any string from `Keys` as a substring, and `No` otherwise.
|
|
4
|
+
* Used to determine whether a shortcut method (e.g. `.json()`, `.html()`)
|
|
5
|
+
* should be present on the response builder for a given response type.
|
|
6
|
+
*/
|
|
7
|
+
export type IfHasKey<
|
|
8
|
+
SomeObject,
|
|
9
|
+
Keys extends readonly string[],
|
|
10
|
+
Yes,
|
|
11
|
+
No,
|
|
12
|
+
> = Keys extends [
|
|
13
|
+
infer FirstKey extends string,
|
|
14
|
+
...infer RestKeys extends string[],
|
|
15
|
+
]
|
|
16
|
+
? Extract<keyof SomeObject, `${string}${FirstKey}${string}`> extends never
|
|
17
|
+
? IfHasKey<SomeObject, RestKeys, Yes, No>
|
|
18
|
+
: Yes
|
|
19
|
+
: No;
|