@counterfact/generator 0.1.1
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/License.md +21 -0
- package/README.md +18 -0
- package/dist/code-generator.d.ts +60 -0
- package/dist/code-generator.js +182 -0
- package/dist/coder.d.ts +107 -0
- package/dist/coder.js +132 -0
- package/dist/context-file-token.d.ts +1 -0
- package/dist/context-file-token.js +1 -0
- package/dist/ensure-directory-exists.d.ts +1 -0
- package/dist/ensure-directory-exists.js +12 -0
- package/dist/forward-slash-path.d.ts +9 -0
- package/dist/forward-slash-path.js +13 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jsdoc.d.ts +5 -0
- package/dist/jsdoc.js +49 -0
- package/dist/openapi-path.d.ts +11 -0
- package/dist/openapi-path.js +23 -0
- package/dist/operation-coder.d.ts +23 -0
- package/dist/operation-coder.js +73 -0
- package/dist/operation-type-coder.d.ts +140 -0
- package/dist/operation-type-coder.js +387 -0
- package/dist/operation-type-name-mapping.d.ts +7 -0
- package/dist/operation-type-name-mapping.js +53 -0
- package/dist/parameter-export-type-coder.d.ts +13 -0
- package/dist/parameter-export-type-coder.js +27 -0
- package/dist/parameters-type-coder.d.ts +10 -0
- package/dist/parameters-type-coder.js +44 -0
- package/dist/printers.d.ts +2 -0
- package/dist/printers.js +10 -0
- package/dist/prune.d.ts +20 -0
- package/dist/prune.js +146 -0
- package/dist/read-only-comments.d.ts +1 -0
- package/dist/read-only-comments.js +5 -0
- package/dist/repository.d.ts +87 -0
- package/dist/repository.js +264 -0
- package/dist/requirement.d.ts +104 -0
- package/dist/requirement.js +191 -0
- package/dist/reserved-words.d.ts +1 -0
- package/dist/reserved-words.js +50 -0
- package/dist/response-type-coder.d.ts +16 -0
- package/dist/response-type-coder.js +104 -0
- package/dist/responses-type-coder.d.ts +11 -0
- package/dist/responses-type-coder.js +43 -0
- package/dist/scenario-file-generator.d.ts +31 -0
- package/dist/scenario-file-generator.js +368 -0
- package/dist/schema-coder.d.ts +10 -0
- package/dist/schema-coder.js +49 -0
- package/dist/schema-type-coder.d.ts +20 -0
- package/dist/schema-type-coder.js +121 -0
- package/dist/script.d.ts +130 -0
- package/dist/script.js +322 -0
- package/dist/specification.d.ts +40 -0
- package/dist/specification.js +58 -0
- package/dist/templates/counterfact-types/cookie-options.ts +14 -0
- package/dist/templates/counterfact-types/counterfact-response.ts +15 -0
- package/dist/templates/counterfact-types/example-names.ts +13 -0
- package/dist/templates/counterfact-types/example.ts +14 -0
- package/dist/templates/counterfact-types/generic-response-builder.ts +167 -0
- package/dist/templates/counterfact-types/http-status-code.ts +62 -0
- package/dist/templates/counterfact-types/if-has-key.ts +19 -0
- package/dist/templates/counterfact-types/index.ts +30 -0
- package/dist/templates/counterfact-types/maybe-promise.ts +6 -0
- package/dist/templates/counterfact-types/media-type.ts +6 -0
- package/dist/templates/counterfact-types/middleware.ts +87 -0
- package/dist/templates/counterfact-types/omit-all.ts +11 -0
- package/dist/templates/counterfact-types/omit-value-when-never.ts +11 -0
- package/dist/templates/counterfact-types/open-api-content.ts +8 -0
- package/dist/templates/counterfact-types/open-api-header.ts +4 -0
- package/dist/templates/counterfact-types/open-api-operation.ts +36 -0
- package/dist/templates/counterfact-types/open-api-parameters.ts +26 -0
- package/dist/templates/counterfact-types/open-api-response.ts +22 -0
- package/dist/templates/counterfact-types/random-function.ts +9 -0
- package/dist/templates/counterfact-types/response-builder-factory.ts +16 -0
- package/dist/templates/counterfact-types/response-builder.ts +36 -0
- package/dist/templates/counterfact-types/wide-operation-argument.ts +20 -0
- package/dist/templates/counterfact-types/wide-response-builder.ts +28 -0
- package/dist/type-coder.d.ts +5 -0
- package/dist/type-coder.js +9 -0
- package/dist/versions-ts-generator.d.ts +10 -0
- package/dist/versions-ts-generator.js +82 -0
- package/dist/wait-for-event.d.ts +2 -0
- package/dist/wait-for-event.js +17 -0
- package/dist/watch-options.d.ts +5 -0
- package/dist/watch-options.js +5 -0
- package/dist/windows-escape.d.ts +1 -0
- package/dist/windows-escape.js +9 -0
- package/examples/generate-routes.mjs +40 -0
- package/package.json +61 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes an OpenAPI path for Counterfact's route and type module trees.
|
|
3
|
+
*
|
|
4
|
+
* Incoming requests already match route modules with or without a terminal
|
|
5
|
+
* slash, so terminal slashes must not create an extra empty file segment.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeOpenApiPath(openApiPath) {
|
|
8
|
+
return openApiPath.replace(/\/+$/u, "") || "/";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Rejects path keys that would target the same generated route module.
|
|
12
|
+
*/
|
|
13
|
+
export function assertNoNormalizedPathCollisions(openApiPaths) {
|
|
14
|
+
const originalPathByNormalizedPath = new Map();
|
|
15
|
+
for (const openApiPath of openApiPaths) {
|
|
16
|
+
const normalizedPath = normalizeOpenApiPath(openApiPath);
|
|
17
|
+
const existingPath = originalPathByNormalizedPath.get(normalizedPath);
|
|
18
|
+
if (existingPath !== undefined && existingPath !== openApiPath) {
|
|
19
|
+
throw new Error(`OpenAPI paths "${existingPath}" and "${openApiPath}" normalize to the same path "${normalizedPath}". Remove one of the conflicting paths.`);
|
|
20
|
+
}
|
|
21
|
+
originalPathByNormalizedPath.set(normalizedPath, openApiPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Coder } from "./coder.js";
|
|
2
|
+
import { type SecurityScheme } from "./operation-type-coder.js";
|
|
3
|
+
import type { Requirement } from "./requirement.js";
|
|
4
|
+
import type { Script } from "./script.js";
|
|
5
|
+
/**
|
|
6
|
+
* Generates the default route handler stub for a single OpenAPI operation.
|
|
7
|
+
*
|
|
8
|
+
* The generated stub calls `$.response[statusCode].random()` (or `.empty()`)
|
|
9
|
+
* for the first response defined in the spec. It is only written when no
|
|
10
|
+
* handler file exists yet — users are expected to replace it with real logic.
|
|
11
|
+
*
|
|
12
|
+
* The corresponding TypeScript type is emitted by {@link OperationTypeCoder}
|
|
13
|
+
* into the `types/paths/…` tree.
|
|
14
|
+
*/
|
|
15
|
+
export declare class OperationCoder extends Coder {
|
|
16
|
+
requestMethod: string;
|
|
17
|
+
securitySchemes: SecurityScheme[];
|
|
18
|
+
constructor(requirement: Requirement, version?: string, requestMethod?: string, securitySchemes?: SecurityScheme[]);
|
|
19
|
+
names(): Generator<string>;
|
|
20
|
+
write(): string;
|
|
21
|
+
typeDeclaration(_namespace: Map<string, unknown> | undefined, script: Script): string;
|
|
22
|
+
modulePath(): string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { STREAMING_CONTENT_TYPES } from "@counterfact/types/streaming-content-types";
|
|
2
|
+
import { pathJoin } from "./forward-slash-path.js";
|
|
3
|
+
import { Coder } from "./coder.js";
|
|
4
|
+
import { normalizeOpenApiPath } from "./openapi-path.js";
|
|
5
|
+
import { OperationTypeCoder, VersionedArgTypeCoder, } from "./operation-type-coder.js";
|
|
6
|
+
/**
|
|
7
|
+
* Generates the default route handler stub for a single OpenAPI operation.
|
|
8
|
+
*
|
|
9
|
+
* The generated stub calls `$.response[statusCode].random()` (or `.empty()`)
|
|
10
|
+
* for the first response defined in the spec. It is only written when no
|
|
11
|
+
* handler file exists yet — users are expected to replace it with real logic.
|
|
12
|
+
*
|
|
13
|
+
* The corresponding TypeScript type is emitted by {@link OperationTypeCoder}
|
|
14
|
+
* into the `types/paths/…` tree.
|
|
15
|
+
*/
|
|
16
|
+
export class OperationCoder extends Coder {
|
|
17
|
+
requestMethod;
|
|
18
|
+
securitySchemes;
|
|
19
|
+
constructor(requirement, version = "", requestMethod = "", securitySchemes = []) {
|
|
20
|
+
super(requirement, version);
|
|
21
|
+
if (requestMethod === "") {
|
|
22
|
+
throw new Error("requestMethod is required");
|
|
23
|
+
}
|
|
24
|
+
this.requestMethod = requestMethod;
|
|
25
|
+
this.securitySchemes = securitySchemes;
|
|
26
|
+
}
|
|
27
|
+
names() {
|
|
28
|
+
return super.names(this.requestMethod.toUpperCase());
|
|
29
|
+
}
|
|
30
|
+
write() {
|
|
31
|
+
const responses = this.requirement.get("responses");
|
|
32
|
+
const [firstStatusCode] = responses.map((_response, statusCode) => statusCode);
|
|
33
|
+
const [firstResponse] = responses.map((response) => response.data);
|
|
34
|
+
if (firstResponse === undefined ||
|
|
35
|
+
!("content" in firstResponse || "schema" in firstResponse)) {
|
|
36
|
+
return `async ($) => {
|
|
37
|
+
return $.response[${firstStatusCode === "default" ? 200 : firstStatusCode}].empty();
|
|
38
|
+
}`;
|
|
39
|
+
}
|
|
40
|
+
// Detect streaming responses (OpenAPI 3.2 itemSchema)
|
|
41
|
+
const content = firstResponse.content;
|
|
42
|
+
const hasStreamingContent = content !== undefined &&
|
|
43
|
+
Object.keys(content).some((ct) => STREAMING_CONTENT_TYPES.has(ct) &&
|
|
44
|
+
content[ct]?.itemSchema !== undefined);
|
|
45
|
+
if (hasStreamingContent) {
|
|
46
|
+
return `async ($) => {
|
|
47
|
+
async function* items() {
|
|
48
|
+
// yield items here
|
|
49
|
+
}
|
|
50
|
+
return $.response[${firstStatusCode === "default" ? 200 : firstStatusCode}].stream(items());
|
|
51
|
+
}`;
|
|
52
|
+
}
|
|
53
|
+
return `async ($) => {
|
|
54
|
+
return $.response[${firstStatusCode === "default" ? 200 : firstStatusCode}].random();
|
|
55
|
+
}`;
|
|
56
|
+
}
|
|
57
|
+
typeDeclaration(_namespace, script) {
|
|
58
|
+
const operationTypeCoder = new OperationTypeCoder(this.requirement, this.version, this.requestMethod, this.securitySchemes);
|
|
59
|
+
if (this.version !== "") {
|
|
60
|
+
// For versioned APIs: register this version's $-argument type on the
|
|
61
|
+
// shared script so that Script.versionsTypeStatements() can emit the
|
|
62
|
+
// merged handler type after all versions have been declared.
|
|
63
|
+
const versionedArgCoder = new VersionedArgTypeCoder(this.requirement, this.version, this.requestMethod, this.securitySchemes);
|
|
64
|
+
const sharedScript = script.repository.get(operationTypeCoder.modulePath());
|
|
65
|
+
sharedScript.declareVersion(versionedArgCoder, operationTypeCoder.getOperationBaseName());
|
|
66
|
+
}
|
|
67
|
+
return script.importType(operationTypeCoder);
|
|
68
|
+
}
|
|
69
|
+
modulePath() {
|
|
70
|
+
const pathString = normalizeOpenApiPath(this.requirement.url.split("/").at(-2).replaceAll("~1", "/"));
|
|
71
|
+
return `${pathJoin("routes", pathString)}.types.ts`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { TypeCoder } from "./type-coder.js";
|
|
2
|
+
import { Requirement } from "./requirement.js";
|
|
3
|
+
import type { Script } from "./script.js";
|
|
4
|
+
export interface SecurityScheme {
|
|
5
|
+
in?: "cookie" | "header" | "query";
|
|
6
|
+
name?: string;
|
|
7
|
+
scheme?: string;
|
|
8
|
+
type?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generates the TypeScript type for a single OpenAPI operation.
|
|
12
|
+
*
|
|
13
|
+
* The emitted type describes the function signature that a Counterfact route
|
|
14
|
+
* handler must satisfy, including strongly-typed `query`, `path`, `headers`,
|
|
15
|
+
* `cookie`, `body`, `context`, `response`, and `user` arguments.
|
|
16
|
+
*
|
|
17
|
+
* Output is written to `types/paths/<route>.types.ts`.
|
|
18
|
+
*
|
|
19
|
+
* **Versioned APIs**: when `version` is non-empty this coder emits only a
|
|
20
|
+
* sentinel `{raw: ""}` export (suppressing the normal flat type) and
|
|
21
|
+
* registers a formatter on the shared script so that
|
|
22
|
+
* {@link Script.versionsTypeStatements} can later emit the merged
|
|
23
|
+
* `HTTP_<METHOD>_$_Versions` map and the `HTTP_<METHOD>` handler type.
|
|
24
|
+
* Each version's `$`-argument type is emitted to
|
|
25
|
+
* `types/<version>/paths/<path>.types.ts` by {@link VersionedArgTypeCoder}.
|
|
26
|
+
*/
|
|
27
|
+
export declare class OperationTypeCoder extends TypeCoder {
|
|
28
|
+
requestMethod: string;
|
|
29
|
+
securitySchemes: SecurityScheme[];
|
|
30
|
+
constructor(requirement: Requirement, version?: string, requestMethod?: string, securitySchemes?: SecurityScheme[]);
|
|
31
|
+
/**
|
|
32
|
+
* Returns the base identifier for this operation, derived from its
|
|
33
|
+
* `operationId` (sanitised) or falling back to `HTTP_<METHOD>`.
|
|
34
|
+
*/
|
|
35
|
+
getOperationBaseName(): string;
|
|
36
|
+
jsdoc(): string;
|
|
37
|
+
names(): Generator<string>;
|
|
38
|
+
/**
|
|
39
|
+
* Generates and exports a named parameter type (e.g. `ListPets_Query`) from
|
|
40
|
+
* `modulePath` and returns the exported type name.
|
|
41
|
+
*
|
|
42
|
+
* Returns `"never"` without creating an export when `inlineType` is
|
|
43
|
+
* `"never"`.
|
|
44
|
+
*
|
|
45
|
+
* @param script - The script being assembled.
|
|
46
|
+
* @param parameterKind - `"query"`, `"path"`, `"headers"`, or `"cookie"`.
|
|
47
|
+
* @param inlineType - The inline TypeScript type string to export.
|
|
48
|
+
* @param baseName - The base identifier prefix for the exported type name.
|
|
49
|
+
* @param modulePath - The repository-relative path of the type file.
|
|
50
|
+
*/
|
|
51
|
+
exportParameterType(script: Script, parameterKind: string, inlineType: string, baseName: string, modulePath: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Returns the union of all possible response type shapes for this operation.
|
|
54
|
+
*
|
|
55
|
+
* @param script - The script being assembled (used to resolve imports).
|
|
56
|
+
*/
|
|
57
|
+
responseTypes(script: Script): string;
|
|
58
|
+
modulePath(): string;
|
|
59
|
+
/**
|
|
60
|
+
* Returns the TypeScript type for the `user` argument.
|
|
61
|
+
*
|
|
62
|
+
* When the operation is protected by HTTP Basic auth, the type is
|
|
63
|
+
* `{username?: string, password?: string}`. Otherwise it is `"never"`.
|
|
64
|
+
*/
|
|
65
|
+
userType(): string;
|
|
66
|
+
/**
|
|
67
|
+
* Returns the TypeScript type for the `auth` argument.
|
|
68
|
+
*
|
|
69
|
+
* Includes basic-auth credentials when present and `apiKey` when at least one
|
|
70
|
+
* apiKey security scheme is configured.
|
|
71
|
+
*/
|
|
72
|
+
authType(): string;
|
|
73
|
+
/**
|
|
74
|
+
* Returns the effective parameters for this operation by merging path-item-level
|
|
75
|
+
* parameters with operation-level parameters. Per the OpenAPI specification,
|
|
76
|
+
* operation-level parameters override path-item-level parameters that share
|
|
77
|
+
* the same `name` and `in` location.
|
|
78
|
+
*
|
|
79
|
+
* Uses `this.requirement.parent` (the path item requirement) to access
|
|
80
|
+
* path-item-level parameters directly, without URL string parsing.
|
|
81
|
+
*
|
|
82
|
+
* When the parent is not set (e.g. in unit tests that construct requirements
|
|
83
|
+
* directly), only the operation-level parameters are returned.
|
|
84
|
+
*/
|
|
85
|
+
protected getEffectiveParameters(): Requirement | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Builds the `OmitValueWhenNever<{…}>` dollar-argument type body and sets
|
|
88
|
+
* up all required shared-type imports on `script`.
|
|
89
|
+
*
|
|
90
|
+
* This helper is reused by both {@link writeCode} (non-versioned) and
|
|
91
|
+
* {@link VersionedArgTypeCoder.writeCode} (per-version file).
|
|
92
|
+
*
|
|
93
|
+
* @param script - The script to write imports and parameter-type exports into.
|
|
94
|
+
* @param baseName - Identifier prefix used for named parameter-type exports.
|
|
95
|
+
* @param modulePath - Repository-relative path for parameter-type exports.
|
|
96
|
+
*/
|
|
97
|
+
protected buildDollarArgType(script: Script, baseName: string, modulePath: string): string;
|
|
98
|
+
writeCode(script: Script): string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Emits a per-version `$`-argument type to
|
|
102
|
+
* `types/<version>/paths/<path>.types.ts`.
|
|
103
|
+
*
|
|
104
|
+
* When called from a *different* script (e.g. the shared
|
|
105
|
+
* `types/paths/…` script via `Script.declareVersion`), `write()` delegates to
|
|
106
|
+
* `script.importType(this)` so that the type is written to the per-version
|
|
107
|
+
* file and an import is added to the calling script.
|
|
108
|
+
*
|
|
109
|
+
* Only the `OmitValueWhenNever<{…}>` type body is emitted — the
|
|
110
|
+
* function-wrapper `($: Versioned<…>) => MaybePromise<COUNTERFACT_RESPONSE>`
|
|
111
|
+
* is assembled by the shared script's `versionsTypeStatements()`.
|
|
112
|
+
*/
|
|
113
|
+
export declare class VersionedArgTypeCoder extends OperationTypeCoder {
|
|
114
|
+
/**
|
|
115
|
+
* Include the version in the cache key so v1 and v2 coders are treated as
|
|
116
|
+
* distinct exports even when they share the same requirement URL.
|
|
117
|
+
*/
|
|
118
|
+
get id(): string;
|
|
119
|
+
/**
|
|
120
|
+
* The per-version `$`-argument type is emitted to
|
|
121
|
+
* `types/<version>/paths/<path>.types.ts`, not to the shared path.
|
|
122
|
+
*/
|
|
123
|
+
modulePath(): string;
|
|
124
|
+
/**
|
|
125
|
+
* Names are version-qualified (e.g. `HTTP_GET_$_v1`) so that importing
|
|
126
|
+
* multiple versions into the shared script requires no aliasing.
|
|
127
|
+
*/
|
|
128
|
+
names(): Generator<string>;
|
|
129
|
+
/**
|
|
130
|
+
* When called from the per-version file itself, generate the actual type.
|
|
131
|
+
* When called from any other script (e.g. the shared file), export to the
|
|
132
|
+
* per-version file and import the result back into that script.
|
|
133
|
+
*/
|
|
134
|
+
write(script: Script): string;
|
|
135
|
+
/**
|
|
136
|
+
* Generates the `OmitValueWhenNever<{…}>` dollar-argument type and writes
|
|
137
|
+
* it to the per-version script.
|
|
138
|
+
*/
|
|
139
|
+
writeCode(script: Script): string;
|
|
140
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { STREAMING_CONTENT_TYPES } from "@counterfact/types/streaming-content-types";
|
|
2
|
+
import { pathJoin } from "./forward-slash-path.js";
|
|
3
|
+
import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
|
|
4
|
+
import { buildJsDoc } from "./jsdoc.js";
|
|
5
|
+
import { ParameterExportTypeCoder } from "./parameter-export-type-coder.js";
|
|
6
|
+
import { ParametersTypeCoder } from "./parameters-type-coder.js";
|
|
7
|
+
import { READ_ONLY_COMMENTS } from "./read-only-comments.js";
|
|
8
|
+
import { RESERVED_WORDS } from "./reserved-words.js";
|
|
9
|
+
import { ResponsesTypeCoder } from "./responses-type-coder.js";
|
|
10
|
+
import { SchemaTypeCoder } from "./schema-type-coder.js";
|
|
11
|
+
import { TypeCoder } from "./type-coder.js";
|
|
12
|
+
import { normalizeOpenApiPath } from "./openapi-path.js";
|
|
13
|
+
import { Requirement } from "./requirement.js";
|
|
14
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words
|
|
15
|
+
function sanitizeIdentifier(value) {
|
|
16
|
+
// Treat any run of non-identifier characters as a camelCase separator
|
|
17
|
+
let result = value.replaceAll(/[^\w$]+(?<next>.)/gu, (_, char) => char.toUpperCase());
|
|
18
|
+
// Strip any trailing non-identifier characters (no following char to capitalize)
|
|
19
|
+
result = result.replaceAll(/[^\w$]/gu, "");
|
|
20
|
+
// If the identifier starts with a digit, prefix with an underscore
|
|
21
|
+
if (/^\d/u.test(result)) {
|
|
22
|
+
result = `_${result}`;
|
|
23
|
+
}
|
|
24
|
+
// If the identifier is a reserved word, append an underscore
|
|
25
|
+
if (RESERVED_WORDS.has(result)) {
|
|
26
|
+
result = `${result}_`;
|
|
27
|
+
}
|
|
28
|
+
return result || "_";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generates the TypeScript type for a single OpenAPI operation.
|
|
32
|
+
*
|
|
33
|
+
* The emitted type describes the function signature that a Counterfact route
|
|
34
|
+
* handler must satisfy, including strongly-typed `query`, `path`, `headers`,
|
|
35
|
+
* `cookie`, `body`, `context`, `response`, and `user` arguments.
|
|
36
|
+
*
|
|
37
|
+
* Output is written to `types/paths/<route>.types.ts`.
|
|
38
|
+
*
|
|
39
|
+
* **Versioned APIs**: when `version` is non-empty this coder emits only a
|
|
40
|
+
* sentinel `{raw: ""}` export (suppressing the normal flat type) and
|
|
41
|
+
* registers a formatter on the shared script so that
|
|
42
|
+
* {@link Script.versionsTypeStatements} can later emit the merged
|
|
43
|
+
* `HTTP_<METHOD>_$_Versions` map and the `HTTP_<METHOD>` handler type.
|
|
44
|
+
* Each version's `$`-argument type is emitted to
|
|
45
|
+
* `types/<version>/paths/<path>.types.ts` by {@link VersionedArgTypeCoder}.
|
|
46
|
+
*/
|
|
47
|
+
export class OperationTypeCoder extends TypeCoder {
|
|
48
|
+
requestMethod;
|
|
49
|
+
securitySchemes;
|
|
50
|
+
constructor(requirement, version = "", requestMethod = "", securitySchemes = []) {
|
|
51
|
+
super(requirement, version);
|
|
52
|
+
if (requestMethod === "") {
|
|
53
|
+
throw new Error("requestMethod is required");
|
|
54
|
+
}
|
|
55
|
+
this.requestMethod = requestMethod;
|
|
56
|
+
this.securitySchemes = securitySchemes;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Returns the base identifier for this operation, derived from its
|
|
60
|
+
* `operationId` (sanitised) or falling back to `HTTP_<METHOD>`.
|
|
61
|
+
*/
|
|
62
|
+
getOperationBaseName() {
|
|
63
|
+
const operationId = this.requirement.get("operationId")?.data;
|
|
64
|
+
return operationId
|
|
65
|
+
? sanitizeIdentifier(operationId)
|
|
66
|
+
: `HTTP_${this.requestMethod.toUpperCase()}`;
|
|
67
|
+
}
|
|
68
|
+
jsdoc() {
|
|
69
|
+
return buildJsDoc(this.requirement.data);
|
|
70
|
+
}
|
|
71
|
+
names() {
|
|
72
|
+
return super.names(this.getOperationBaseName());
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Generates and exports a named parameter type (e.g. `ListPets_Query`) from
|
|
76
|
+
* `modulePath` and returns the exported type name.
|
|
77
|
+
*
|
|
78
|
+
* Returns `"never"` without creating an export when `inlineType` is
|
|
79
|
+
* `"never"`.
|
|
80
|
+
*
|
|
81
|
+
* @param script - The script being assembled.
|
|
82
|
+
* @param parameterKind - `"query"`, `"path"`, `"headers"`, or `"cookie"`.
|
|
83
|
+
* @param inlineType - The inline TypeScript type string to export.
|
|
84
|
+
* @param baseName - The base identifier prefix for the exported type name.
|
|
85
|
+
* @param modulePath - The repository-relative path of the type file.
|
|
86
|
+
*/
|
|
87
|
+
exportParameterType(script, parameterKind, inlineType, baseName, modulePath) {
|
|
88
|
+
if (inlineType === "never") {
|
|
89
|
+
return "never";
|
|
90
|
+
}
|
|
91
|
+
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
92
|
+
const typeName = `${baseName}_${capitalize(parameterKind)}`;
|
|
93
|
+
const coder = new ParameterExportTypeCoder(this.requirement, this.version, typeName, inlineType, parameterKind);
|
|
94
|
+
coder._modulePath = modulePath;
|
|
95
|
+
return script.export(coder, true);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns the union of all possible response type shapes for this operation.
|
|
99
|
+
*
|
|
100
|
+
* @param script - The script being assembled (used to resolve imports).
|
|
101
|
+
*/
|
|
102
|
+
responseTypes(script) {
|
|
103
|
+
return this.requirement
|
|
104
|
+
.get("responses")
|
|
105
|
+
.flatMap((response, responseCode) => {
|
|
106
|
+
const status = responseCode === "default"
|
|
107
|
+
? "number | undefined"
|
|
108
|
+
: Number.parseInt(responseCode, 10);
|
|
109
|
+
if (response.has("content")) {
|
|
110
|
+
return response.get("content").map((content, contentType) => {
|
|
111
|
+
let bodyType;
|
|
112
|
+
if (content.has("itemSchema") &&
|
|
113
|
+
STREAMING_CONTENT_TYPES.has(contentType)) {
|
|
114
|
+
bodyType = `AsyncIterable<${new SchemaTypeCoder(content.get("itemSchema"), this.version).write(script)}>`;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
bodyType = content.has("schema")
|
|
118
|
+
? new SchemaTypeCoder(content.get("schema"), this.version).write(script)
|
|
119
|
+
: "unknown";
|
|
120
|
+
}
|
|
121
|
+
return `{
|
|
122
|
+
status: ${status},
|
|
123
|
+
contentType?: "${contentType}",
|
|
124
|
+
body?: ${bodyType}
|
|
125
|
+
}`;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (response.has("schema")) {
|
|
129
|
+
const producesReq = this.requirement?.get("produces") ??
|
|
130
|
+
this.requirement.specification?.rootRequirement?.get("produces");
|
|
131
|
+
const produces = producesReq?.data;
|
|
132
|
+
if (produces) {
|
|
133
|
+
return produces
|
|
134
|
+
.map((contentType) => `{
|
|
135
|
+
status: ${status},
|
|
136
|
+
contentType?: "${contentType}",
|
|
137
|
+
body?: ${new SchemaTypeCoder(response.get("schema"), this.version).write(script)}
|
|
138
|
+
}`)
|
|
139
|
+
.join(" | ");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return `{
|
|
143
|
+
status: ${status}
|
|
144
|
+
}`;
|
|
145
|
+
})
|
|
146
|
+
.join(" | ");
|
|
147
|
+
}
|
|
148
|
+
modulePath() {
|
|
149
|
+
const pathString = normalizeOpenApiPath(this.requirement.url.split("/").at(-2).replaceAll("~1", "/"));
|
|
150
|
+
return `${pathJoin("types/paths", pathString === "/" ? "/index" : pathString)}.types.ts`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Returns the TypeScript type for the `user` argument.
|
|
154
|
+
*
|
|
155
|
+
* When the operation is protected by HTTP Basic auth, the type is
|
|
156
|
+
* `{username?: string, password?: string}`. Otherwise it is `"never"`.
|
|
157
|
+
*/
|
|
158
|
+
userType() {
|
|
159
|
+
if (this.securitySchemes.some(({ scheme, type }) => type === "http" && scheme === "basic")) {
|
|
160
|
+
return "{username?: string, password?: string}";
|
|
161
|
+
}
|
|
162
|
+
return "never";
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Returns the TypeScript type for the `auth` argument.
|
|
166
|
+
*
|
|
167
|
+
* Includes basic-auth credentials when present and `apiKey` when at least one
|
|
168
|
+
* apiKey security scheme is configured.
|
|
169
|
+
*/
|
|
170
|
+
authType() {
|
|
171
|
+
const fields = new Set();
|
|
172
|
+
if (this.securitySchemes.some(({ scheme, type }) => type === "http" && scheme === "basic")) {
|
|
173
|
+
fields.add("username?: string");
|
|
174
|
+
fields.add("password?: string");
|
|
175
|
+
}
|
|
176
|
+
if (this.securitySchemes.some(({ type }) => type === "apiKey")) {
|
|
177
|
+
fields.add("apiKey: string");
|
|
178
|
+
}
|
|
179
|
+
return fields.size === 0 ? "never" : `{${[...fields].join(", ")}}`;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Returns the effective parameters for this operation by merging path-item-level
|
|
183
|
+
* parameters with operation-level parameters. Per the OpenAPI specification,
|
|
184
|
+
* operation-level parameters override path-item-level parameters that share
|
|
185
|
+
* the same `name` and `in` location.
|
|
186
|
+
*
|
|
187
|
+
* Uses `this.requirement.parent` (the path item requirement) to access
|
|
188
|
+
* path-item-level parameters directly, without URL string parsing.
|
|
189
|
+
*
|
|
190
|
+
* When the parent is not set (e.g. in unit tests that construct requirements
|
|
191
|
+
* directly), only the operation-level parameters are returned.
|
|
192
|
+
*/
|
|
193
|
+
getEffectiveParameters() {
|
|
194
|
+
const operationParams = this.requirement.get("parameters");
|
|
195
|
+
const pathItemParams = this.requirement.parent?.get("parameters");
|
|
196
|
+
const apiKeyParameters = this.securitySchemes
|
|
197
|
+
.filter(({ in: location, name, type }) => type === "apiKey" &&
|
|
198
|
+
typeof name === "string" &&
|
|
199
|
+
(location === "header" ||
|
|
200
|
+
location === "query" ||
|
|
201
|
+
location === "cookie"))
|
|
202
|
+
.map(({ in: location, name }) => ({
|
|
203
|
+
in: location,
|
|
204
|
+
name,
|
|
205
|
+
required: true,
|
|
206
|
+
schema: { type: "string" },
|
|
207
|
+
}));
|
|
208
|
+
if (!pathItemParams && !operationParams && apiKeyParameters.length === 0) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
if (!pathItemParams && apiKeyParameters.length === 0) {
|
|
212
|
+
return operationParams;
|
|
213
|
+
}
|
|
214
|
+
if (!operationParams && apiKeyParameters.length === 0) {
|
|
215
|
+
return pathItemParams;
|
|
216
|
+
}
|
|
217
|
+
// Merge using a Map keyed on `${in}:${name}`.
|
|
218
|
+
// Path-level params are added first; operation-level and security-level
|
|
219
|
+
// params override them.
|
|
220
|
+
const pathData = pathItemParams?.data ?? [];
|
|
221
|
+
const opData = operationParams?.data ?? [];
|
|
222
|
+
const map = new Map();
|
|
223
|
+
for (const p of pathData) {
|
|
224
|
+
map.set(`${p.in}:${p.name}`, p);
|
|
225
|
+
}
|
|
226
|
+
for (const p of opData) {
|
|
227
|
+
map.set(`${p.in}:${p.name}`, p);
|
|
228
|
+
}
|
|
229
|
+
for (const p of apiKeyParameters) {
|
|
230
|
+
map.set(`${p.in}:${p.name}`, p);
|
|
231
|
+
}
|
|
232
|
+
return new Requirement([...map.values()], this.requirement.url, this.requirement.specification);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Builds the `OmitValueWhenNever<{…}>` dollar-argument type body and sets
|
|
236
|
+
* up all required shared-type imports on `script`.
|
|
237
|
+
*
|
|
238
|
+
* This helper is reused by both {@link writeCode} (non-versioned) and
|
|
239
|
+
* {@link VersionedArgTypeCoder.writeCode} (per-version file).
|
|
240
|
+
*
|
|
241
|
+
* @param script - The script to write imports and parameter-type exports into.
|
|
242
|
+
* @param baseName - Identifier prefix used for named parameter-type exports.
|
|
243
|
+
* @param modulePath - Repository-relative path for parameter-type exports.
|
|
244
|
+
*/
|
|
245
|
+
buildDollarArgType(script, baseName, modulePath) {
|
|
246
|
+
const xType = script.importSharedType("WideOperationArgument");
|
|
247
|
+
script.importSharedType("OmitValueWhenNever");
|
|
248
|
+
script.importSharedType("COUNTERFACT_RESPONSE");
|
|
249
|
+
const contextTypeImportName = script.importExternalType("Context", CONTEXT_FILE_TOKEN);
|
|
250
|
+
const parameters = this.getEffectiveParameters();
|
|
251
|
+
const queryType = new ParametersTypeCoder(parameters, this.version, "query").write(script);
|
|
252
|
+
const pathType = new ParametersTypeCoder(parameters, this.version, "path").write(script);
|
|
253
|
+
const headersType = new ParametersTypeCoder(parameters, this.version, "header").write(script);
|
|
254
|
+
const cookieType = new ParametersTypeCoder(parameters, this.version, "cookie").write(script);
|
|
255
|
+
const bodyRequirement = (this.requirement.get("consumes") ??
|
|
256
|
+
this.requirement.specification?.rootRequirement?.get("consumes"))
|
|
257
|
+
? parameters
|
|
258
|
+
?.find((parameter) => ["body", "formData"].includes(parameter.get("in")?.data))
|
|
259
|
+
?.get("schema")
|
|
260
|
+
: this.requirement.select("requestBody/content/application~1json/schema");
|
|
261
|
+
const bodyType = bodyRequirement === undefined
|
|
262
|
+
? "never"
|
|
263
|
+
: new SchemaTypeCoder(bodyRequirement, this.version).write(script);
|
|
264
|
+
const openApi2MediaTypes = (this.requirement.get("produces")?.data ??
|
|
265
|
+
this.requirement.specification?.rootRequirement?.get("produces")
|
|
266
|
+
?.data);
|
|
267
|
+
const responseType = new ResponsesTypeCoder(this.requirement.get("responses"), this.version, openApi2MediaTypes).write(script);
|
|
268
|
+
const proxyType = "(url: string) => COUNTERFACT_RESPONSE";
|
|
269
|
+
const delayType = "(milliseconds: number, maxMilliseconds?: number) => Promise<void>";
|
|
270
|
+
const queryTypeName = this.exportParameterType(script, "query", queryType, baseName, modulePath);
|
|
271
|
+
const pathTypeName = this.exportParameterType(script, "path", pathType, baseName, modulePath);
|
|
272
|
+
const headersTypeName = this.exportParameterType(script, "headers", headersType, baseName, modulePath);
|
|
273
|
+
const cookieTypeName = this.exportParameterType(script, "cookie", cookieType, baseName, modulePath);
|
|
274
|
+
// OpenAPI 3.2 querystring parameter: the entire query string treated as a
|
|
275
|
+
// single typed object (similar to requestBody for query strings).
|
|
276
|
+
const querystringParam = parameters?.find((parameter) => parameter.get("in")?.data === "querystring");
|
|
277
|
+
const querystringType = querystringParam?.has("schema") === true
|
|
278
|
+
? new SchemaTypeCoder(querystringParam.get("schema"), this.version).write(script)
|
|
279
|
+
: "never";
|
|
280
|
+
const querystringTypeName = this.exportParameterType(script, "querystring", querystringType, baseName, modulePath);
|
|
281
|
+
const versionLiteralType = this.version !== "" ? `"${this.version}"` : "never";
|
|
282
|
+
return `OmitValueWhenNever<{ query: ${queryTypeName}, querystring: ${querystringTypeName}, path: ${pathTypeName}, headers: ${headersTypeName}, cookie: ${cookieTypeName}, body: ${bodyType}, context: ${contextTypeImportName}, response: ${responseType}, x: ${xType}, proxy: ${proxyType}, auth: ${this.authType()}, user: ${this.userType()}, delay: ${delayType}, version: ${versionLiteralType} }>`;
|
|
283
|
+
}
|
|
284
|
+
writeCode(script) {
|
|
285
|
+
script.comments = READ_ONLY_COMMENTS;
|
|
286
|
+
if (this.version !== "") {
|
|
287
|
+
// Versioned case: suppress the normal flat export and register a
|
|
288
|
+
// formatter so that Script.versionsTypeStatements() can emit the
|
|
289
|
+
// merged HTTP_<METHOD>_$_Versions + HTTP_<METHOD> types after all
|
|
290
|
+
// versions have been declared via declareVersion().
|
|
291
|
+
const versionedType = script.importVersionsType("Versioned");
|
|
292
|
+
const maybePromiseType = script.importSharedType("MaybePromise");
|
|
293
|
+
const counterfactResponseType = script.importSharedType("COUNTERFACT_RESPONSE");
|
|
294
|
+
const baseName = this.getOperationBaseName();
|
|
295
|
+
script.setVersionFormatter(baseName, (versionCodes) => {
|
|
296
|
+
const versionsTypeName = `${baseName}_$_Versions`;
|
|
297
|
+
const versionMap = Array.from(versionCodes, ([v, code]) => `"${v}": ${code}`).join("; ");
|
|
298
|
+
return [
|
|
299
|
+
`type ${versionsTypeName} = { ${versionMap} };`,
|
|
300
|
+
`export type ${baseName} = ($: ${versionedType}<${versionsTypeName}>) => ${maybePromiseType}<${counterfactResponseType}>;`,
|
|
301
|
+
].join("\n");
|
|
302
|
+
});
|
|
303
|
+
// Return a raw-empty sentinel so exportStatements() emits nothing for
|
|
304
|
+
// this export entry. The real export is produced by
|
|
305
|
+
// versionsTypeStatements().
|
|
306
|
+
return { raw: "" };
|
|
307
|
+
}
|
|
308
|
+
// Non-versioned case: existing flat-type output.
|
|
309
|
+
// Import in the same order as the original writeCode so that the emitted
|
|
310
|
+
// import block is identical to the pre-refactor output (snapshot-safe).
|
|
311
|
+
script.importSharedType("WideOperationArgument");
|
|
312
|
+
script.importSharedType("OmitValueWhenNever");
|
|
313
|
+
script.importSharedType("MaybePromise");
|
|
314
|
+
script.importSharedType("COUNTERFACT_RESPONSE");
|
|
315
|
+
const baseName = this.getOperationBaseName();
|
|
316
|
+
const modulePath = this.modulePath();
|
|
317
|
+
const dollarArgType = this.buildDollarArgType(script, baseName, modulePath);
|
|
318
|
+
return `($: ${dollarArgType}) => MaybePromise<COUNTERFACT_RESPONSE>`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Emits a per-version `$`-argument type to
|
|
323
|
+
* `types/<version>/paths/<path>.types.ts`.
|
|
324
|
+
*
|
|
325
|
+
* When called from a *different* script (e.g. the shared
|
|
326
|
+
* `types/paths/…` script via `Script.declareVersion`), `write()` delegates to
|
|
327
|
+
* `script.importType(this)` so that the type is written to the per-version
|
|
328
|
+
* file and an import is added to the calling script.
|
|
329
|
+
*
|
|
330
|
+
* Only the `OmitValueWhenNever<{…}>` type body is emitted — the
|
|
331
|
+
* function-wrapper `($: Versioned<…>) => MaybePromise<COUNTERFACT_RESPONSE>`
|
|
332
|
+
* is assembled by the shared script's `versionsTypeStatements()`.
|
|
333
|
+
*/
|
|
334
|
+
export class VersionedArgTypeCoder extends OperationTypeCoder {
|
|
335
|
+
/**
|
|
336
|
+
* Include the version in the cache key so v1 and v2 coders are treated as
|
|
337
|
+
* distinct exports even when they share the same requirement URL.
|
|
338
|
+
*/
|
|
339
|
+
get id() {
|
|
340
|
+
return `${super.id}:${this.version}`;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The per-version `$`-argument type is emitted to
|
|
344
|
+
* `types/<version>/paths/<path>.types.ts`, not to the shared path.
|
|
345
|
+
*/
|
|
346
|
+
modulePath() {
|
|
347
|
+
const pathString = this.requirement.url
|
|
348
|
+
.split("/")
|
|
349
|
+
.at(-2)
|
|
350
|
+
.replaceAll("~1", "/");
|
|
351
|
+
return `${pathJoin(`types/${this.version}/paths`, pathString === "/" ? "/index" : pathString)}.types.ts`;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Names are version-qualified (e.g. `HTTP_GET_$_v1`) so that importing
|
|
355
|
+
* multiple versions into the shared script requires no aliasing.
|
|
356
|
+
*/
|
|
357
|
+
*names() {
|
|
358
|
+
const baseName = `${this.getOperationBaseName()}_$_${sanitizeIdentifier(this.version)}`;
|
|
359
|
+
yield baseName;
|
|
360
|
+
let index = 1;
|
|
361
|
+
const MAX = 100;
|
|
362
|
+
while (index < MAX) {
|
|
363
|
+
index += 1;
|
|
364
|
+
yield `${baseName}${index}`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* When called from the per-version file itself, generate the actual type.
|
|
369
|
+
* When called from any other script (e.g. the shared file), export to the
|
|
370
|
+
* per-version file and import the result back into that script.
|
|
371
|
+
*/
|
|
372
|
+
write(script) {
|
|
373
|
+
if (script.path === this.modulePath()) {
|
|
374
|
+
return this.writeCode(script);
|
|
375
|
+
}
|
|
376
|
+
return script.importType(this);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Generates the `OmitValueWhenNever<{…}>` dollar-argument type and writes
|
|
380
|
+
* it to the per-version script.
|
|
381
|
+
*/
|
|
382
|
+
writeCode(script) {
|
|
383
|
+
script.comments = READ_ONLY_COMMENTS;
|
|
384
|
+
const baseName = this.getOperationBaseName();
|
|
385
|
+
return this.buildDollarArgType(script, baseName, this.modulePath());
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type OperationTypeNameMapping = Map<string, Map<string, string>>;
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the generated operation type name for each route and HTTP method.
|
|
4
|
+
* This is the migration-facing API; concrete coder and specification classes
|
|
5
|
+
* remain internal implementation details.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildOperationTypeNameMapping(source: string, overlays?: readonly string[]): Promise<OperationTypeNameMapping>;
|