@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.
Files changed (89) hide show
  1. package/License.md +21 -0
  2. package/README.md +18 -0
  3. package/dist/code-generator.d.ts +60 -0
  4. package/dist/code-generator.js +182 -0
  5. package/dist/coder.d.ts +107 -0
  6. package/dist/coder.js +132 -0
  7. package/dist/context-file-token.d.ts +1 -0
  8. package/dist/context-file-token.js +1 -0
  9. package/dist/ensure-directory-exists.d.ts +1 -0
  10. package/dist/ensure-directory-exists.js +12 -0
  11. package/dist/forward-slash-path.d.ts +9 -0
  12. package/dist/forward-slash-path.js +13 -0
  13. package/dist/index.d.ts +5 -0
  14. package/dist/index.js +5 -0
  15. package/dist/jsdoc.d.ts +5 -0
  16. package/dist/jsdoc.js +49 -0
  17. package/dist/openapi-path.d.ts +11 -0
  18. package/dist/openapi-path.js +23 -0
  19. package/dist/operation-coder.d.ts +23 -0
  20. package/dist/operation-coder.js +73 -0
  21. package/dist/operation-type-coder.d.ts +140 -0
  22. package/dist/operation-type-coder.js +387 -0
  23. package/dist/operation-type-name-mapping.d.ts +7 -0
  24. package/dist/operation-type-name-mapping.js +53 -0
  25. package/dist/parameter-export-type-coder.d.ts +13 -0
  26. package/dist/parameter-export-type-coder.js +27 -0
  27. package/dist/parameters-type-coder.d.ts +10 -0
  28. package/dist/parameters-type-coder.js +44 -0
  29. package/dist/printers.d.ts +2 -0
  30. package/dist/printers.js +10 -0
  31. package/dist/prune.d.ts +20 -0
  32. package/dist/prune.js +146 -0
  33. package/dist/read-only-comments.d.ts +1 -0
  34. package/dist/read-only-comments.js +5 -0
  35. package/dist/repository.d.ts +87 -0
  36. package/dist/repository.js +264 -0
  37. package/dist/requirement.d.ts +104 -0
  38. package/dist/requirement.js +191 -0
  39. package/dist/reserved-words.d.ts +1 -0
  40. package/dist/reserved-words.js +50 -0
  41. package/dist/response-type-coder.d.ts +16 -0
  42. package/dist/response-type-coder.js +104 -0
  43. package/dist/responses-type-coder.d.ts +11 -0
  44. package/dist/responses-type-coder.js +43 -0
  45. package/dist/scenario-file-generator.d.ts +31 -0
  46. package/dist/scenario-file-generator.js +368 -0
  47. package/dist/schema-coder.d.ts +10 -0
  48. package/dist/schema-coder.js +49 -0
  49. package/dist/schema-type-coder.d.ts +20 -0
  50. package/dist/schema-type-coder.js +121 -0
  51. package/dist/script.d.ts +130 -0
  52. package/dist/script.js +322 -0
  53. package/dist/specification.d.ts +40 -0
  54. package/dist/specification.js +58 -0
  55. package/dist/templates/counterfact-types/cookie-options.ts +14 -0
  56. package/dist/templates/counterfact-types/counterfact-response.ts +15 -0
  57. package/dist/templates/counterfact-types/example-names.ts +13 -0
  58. package/dist/templates/counterfact-types/example.ts +14 -0
  59. package/dist/templates/counterfact-types/generic-response-builder.ts +167 -0
  60. package/dist/templates/counterfact-types/http-status-code.ts +62 -0
  61. package/dist/templates/counterfact-types/if-has-key.ts +19 -0
  62. package/dist/templates/counterfact-types/index.ts +30 -0
  63. package/dist/templates/counterfact-types/maybe-promise.ts +6 -0
  64. package/dist/templates/counterfact-types/media-type.ts +6 -0
  65. package/dist/templates/counterfact-types/middleware.ts +87 -0
  66. package/dist/templates/counterfact-types/omit-all.ts +11 -0
  67. package/dist/templates/counterfact-types/omit-value-when-never.ts +11 -0
  68. package/dist/templates/counterfact-types/open-api-content.ts +8 -0
  69. package/dist/templates/counterfact-types/open-api-header.ts +4 -0
  70. package/dist/templates/counterfact-types/open-api-operation.ts +36 -0
  71. package/dist/templates/counterfact-types/open-api-parameters.ts +26 -0
  72. package/dist/templates/counterfact-types/open-api-response.ts +22 -0
  73. package/dist/templates/counterfact-types/random-function.ts +9 -0
  74. package/dist/templates/counterfact-types/response-builder-factory.ts +16 -0
  75. package/dist/templates/counterfact-types/response-builder.ts +36 -0
  76. package/dist/templates/counterfact-types/wide-operation-argument.ts +20 -0
  77. package/dist/templates/counterfact-types/wide-response-builder.ts +28 -0
  78. package/dist/type-coder.d.ts +5 -0
  79. package/dist/type-coder.js +9 -0
  80. package/dist/versions-ts-generator.d.ts +10 -0
  81. package/dist/versions-ts-generator.js +82 -0
  82. package/dist/wait-for-event.d.ts +2 -0
  83. package/dist/wait-for-event.js +17 -0
  84. package/dist/watch-options.d.ts +5 -0
  85. package/dist/watch-options.js +5 -0
  86. package/dist/windows-escape.d.ts +1 -0
  87. package/dist/windows-escape.js +9 -0
  88. package/examples/generate-routes.mjs +40 -0
  89. 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>;