@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,264 @@
1
+ import { existsSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import nodePath, { dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ /* eslint-disable security/detect-non-literal-fs-filename -- repository writes and stats generated files only inside destination output directories. */
6
+ import createDebug from "debug";
7
+ import { ensureDirectoryExists } from "./ensure-directory-exists.js";
8
+ import { toForwardSlashPath, pathJoin, pathRelative, pathDirname, } from "./forward-slash-path.js";
9
+ import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
10
+ import { Script } from "./script.js";
11
+ import { escapePathForWindows } from "./windows-escape.js";
12
+ const debug = createDebug("counterfact:server:repository");
13
+ const __dirname = toForwardSlashPath(dirname(fileURLToPath(import.meta.url)));
14
+ debug("dirname is %s", __dirname);
15
+ /**
16
+ * Collection of {@link Script} objects keyed by their repository-relative
17
+ * path.
18
+ *
19
+ * Coders call {@link get} to obtain (or create) the script where they should
20
+ * export their generated TypeScript. After all coders have been registered,
21
+ * {@link writeFiles} waits for every script to finish and writes the output to
22
+ * disk.
23
+ */
24
+ export class Repository {
25
+ scripts;
26
+ constructor() {
27
+ this.scripts = new Map();
28
+ }
29
+ /**
30
+ * Returns the {@link Script} for `path`, creating it if it does not yet
31
+ * exist.
32
+ *
33
+ * @param path - Repository-relative path (e.g. `"routes/pets.ts"`).
34
+ */
35
+ get(path) {
36
+ debug("getting script at %s", path);
37
+ if (this.scripts.has(path)) {
38
+ debug("already have script %s, returning it", path);
39
+ return this.scripts.get(path);
40
+ }
41
+ debug("don't have %s, creating it", path);
42
+ const script = new Script(this, path);
43
+ this.scripts.set(path, script);
44
+ return script;
45
+ }
46
+ /** Waits until all scripts have resolved all of their pending export promises. */
47
+ async finished() {
48
+ while (Array.from(this.scripts.values()).some((script) => script.isInProgress())) {
49
+ debug("waiting for %i scripts to finish", this.scripts.size);
50
+ await Promise.all(Array.from(this.scripts.values(), (script) => script.finished()));
51
+ }
52
+ }
53
+ /**
54
+ * Copies the compiled `counterfact-types` directory from the Counterfact
55
+ * distribution into the generated output tree.
56
+ *
57
+ * Returns `false` when the source directory does not exist (e.g. running
58
+ * from source without a prior build).
59
+ *
60
+ * @param destination - The root of the generated output tree.
61
+ */
62
+ async copyCoreFiles(destination) {
63
+ const installedTemplatePath = nodePath.join(__dirname, "templates", "counterfact-types");
64
+ const sourceTemplatePath = nodePath.join(__dirname, "..", "dist", "templates", "counterfact-types");
65
+ const sourcePath = existsSync(installedTemplatePath)
66
+ ? installedTemplatePath
67
+ : sourceTemplatePath;
68
+ const destinationPath = nodePath.join(destination, "counterfact-types");
69
+ if (!existsSync(sourcePath)) {
70
+ return false;
71
+ }
72
+ return fs.cp(sourcePath, destinationPath, { recursive: true });
73
+ }
74
+ /**
75
+ * Waits for all scripts to finish, then writes each one to disk.
76
+ *
77
+ * Route files (`routes/…`) are never fully overwritten if they already exist
78
+ * on disk, preserving user edits. However, if the generated script contains
79
+ * HTTP-method handler exports that are absent from the existing file, those
80
+ * new exports (and their `import type` statements) are appended to the file.
81
+ * Type files (`types/…`) are always overwritten.
82
+ *
83
+ * @param destination - Absolute path to the output root directory.
84
+ * @param options - Controls which artefacts are written.
85
+ */
86
+ async writeFiles(destination, { routes, types }) {
87
+ debug("waiting for %i or more scripts to finish before writing files", this.scripts.size);
88
+ await this.finished();
89
+ debug("all %i scripts are finished", this.scripts.size);
90
+ const writeFiles = Array.from(this.scripts.entries(), async ([path, script]) => {
91
+ const contents = await script.contents();
92
+ const fullPath = escapePathForWindows(pathJoin(destination, path));
93
+ await ensureDirectoryExists(fullPath);
94
+ const shouldWriteRoutes = routes && path.startsWith("routes");
95
+ const shouldWriteTypes = types && !path.startsWith("routes");
96
+ if (shouldWriteRoutes) {
97
+ const fileExists = await fs
98
+ .stat(fullPath)
99
+ .then((stat) => stat.isFile())
100
+ .catch(() => false);
101
+ if (fileExists) {
102
+ debug(`route file exists, checking for new handlers: ${fullPath}`);
103
+ await this.appendNewHandlers(fullPath, contents.replaceAll(CONTEXT_FILE_TOKEN, this.findContextPath(destination, path)));
104
+ return;
105
+ }
106
+ }
107
+ if (shouldWriteRoutes || shouldWriteTypes) {
108
+ debug("about to write", fullPath);
109
+ await fs.writeFile(fullPath, contents.replaceAll(CONTEXT_FILE_TOKEN, this.findContextPath(destination, path)));
110
+ debug("did write", fullPath);
111
+ }
112
+ });
113
+ await Promise.all(writeFiles);
114
+ await this.copyCoreFiles(destination);
115
+ if (routes) {
116
+ await this.createDefaultContextFile(destination);
117
+ }
118
+ }
119
+ /**
120
+ * Creates the default `routes/_.context.ts` file if it does not already
121
+ * exist.
122
+ *
123
+ * @param destination - Absolute path to the output root directory.
124
+ */
125
+ async createDefaultContextFile(destination) {
126
+ const contextFilePath = nodePath.join(destination, "routes", "_.context.ts");
127
+ if (existsSync(contextFilePath)) {
128
+ return;
129
+ }
130
+ await ensureDirectoryExists(contextFilePath);
131
+ await fs.writeFile(contextFilePath, `import type { Context$ } from "../types/_.context.js";
132
+
133
+ /**
134
+ * This is the default context for Counterfact.
135
+ *
136
+ * It defines the context object in the REPL
137
+ * and the $.context object in the code.
138
+ *
139
+ * Add properties and methods to suit your needs.
140
+ *
141
+ * See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md
142
+ */
143
+
144
+ export class Context {
145
+ constructor($: Context$) {
146
+ void $;
147
+ }
148
+ }
149
+ `);
150
+ }
151
+ /**
152
+ * Appends any HTTP-method handler exports that appear in `generatedContent`
153
+ * but are absent from the existing file at `fullPath`.
154
+ *
155
+ * For each new export the corresponding `import type` statement is inserted
156
+ * after the last existing import line (or prepended when no imports exist),
157
+ * and the export block is appended at the end of the file.
158
+ *
159
+ * @param fullPath - Absolute path of the route file to update.
160
+ * @param generatedContent - The fully-generated file content (used as the
161
+ * source of new import and export statements).
162
+ */
163
+ async appendNewHandlers(fullPath, generatedContent) {
164
+ const existingContent = await fs.readFile(fullPath, "utf8");
165
+ // Names already exported by the existing file (e.g. GET, POST).
166
+ // RegExp match groups are typed as optional strings, so narrow defensively.
167
+ const existingExportNames = new Set(Array.from(existingContent.matchAll(/^export\s+const\s+(\w+)/gmu), (m) => m[1]).filter((name) => name !== undefined));
168
+ // All named exports in the generated content together with their type names.
169
+ const generatedExports = Array.from(generatedContent.matchAll(/^export\s+const\s+(\w+)\s*:\s*(\w+)/gmu), (m) => ({ methodName: m[1], typeName: m[2] })).filter((value) => value.methodName !== undefined && value.typeName !== undefined);
170
+ const newExports = generatedExports.filter(({ methodName }) => !existingExportNames.has(methodName));
171
+ if (newExports.length === 0) {
172
+ debug(`no new handlers to append to ${fullPath}`);
173
+ return;
174
+ }
175
+ debug(`appending ${newExports.length} new handler(s) to ${fullPath}: %o`, newExports.map(({ methodName }) => methodName));
176
+ const newImportLines = [];
177
+ const newExportBlocks = [];
178
+ for (const { methodName, typeName } of newExports) {
179
+ // Both names come from \w+ captures so they are safe identifiers, but
180
+ // guard explicitly to satisfy static analysis and avoid RegExp injection.
181
+ if (!/^\w+$/u.test(typeName) || !/^\w+$/u.test(methodName)) {
182
+ debug(`skipping handler with unsafe name – methodName: %s, typeName: %s`, methodName, typeName);
183
+ continue;
184
+ }
185
+ // Find the `import type { TypeName } from "..."` line for this type.
186
+ const importMatch = generatedContent.match(new RegExp(`^import\\s+type\\s+\\{[^}]*\\b${typeName}\\b[^}]*\\}\\s+from\\s+["'][^"']+["'];`, "mu"));
187
+ if (importMatch?.[0] && !existingContent.includes(importMatch[0])) {
188
+ newImportLines.push(importMatch[0]);
189
+ }
190
+ // Find the export block: from `export const METHOD` to the closing `};`.
191
+ // The generated code is always Prettier-formatted, so the closing brace
192
+ // and semicolon of every top-level arrow-function export appear on their
193
+ // own line as `\n};`.
194
+ const startMatch = new RegExp(`^export\\s+const\\s+${methodName}\\b`, "mu").exec(generatedContent);
195
+ if (startMatch) {
196
+ const fromExport = generatedContent.slice(startMatch.index);
197
+ const closingIndex = fromExport.indexOf("\n};");
198
+ if (closingIndex !== -1) {
199
+ // Include the closing `};` (3 chars: \n, }, ;)
200
+ newExportBlocks.push(fromExport.slice(0, closingIndex + 3));
201
+ }
202
+ }
203
+ }
204
+ let updatedContent = existingContent;
205
+ // Insert new import lines right after the last existing import statement.
206
+ if (newImportLines.length > 0) {
207
+ const importMatches = [...existingContent.matchAll(/^import\s[^\n]*/gmu)];
208
+ if (importMatches.length > 0) {
209
+ const lastImport = importMatches[importMatches.length - 1];
210
+ const importIndex = lastImport?.index;
211
+ const insertPos = importIndex === undefined
212
+ ? 0
213
+ : (() => {
214
+ const lineEnd = existingContent.indexOf("\n", importIndex);
215
+ return lineEnd === -1 ? existingContent.length : lineEnd + 1;
216
+ })();
217
+ updatedContent =
218
+ existingContent.slice(0, insertPos) +
219
+ newImportLines.join("\n") +
220
+ "\n" +
221
+ existingContent.slice(insertPos);
222
+ }
223
+ else {
224
+ updatedContent = newImportLines.join("\n") + "\n" + existingContent;
225
+ }
226
+ }
227
+ // Append new export blocks at the end of the file.
228
+ if (newExportBlocks.length > 0) {
229
+ const separator = updatedContent.endsWith("\n") ? "\n" : "\n\n";
230
+ updatedContent += separator + newExportBlocks.join("\n\n") + "\n";
231
+ }
232
+ await fs.writeFile(fullPath, updatedContent);
233
+ debug(`appended new handlers to ${fullPath}`);
234
+ }
235
+ /**
236
+ * Returns the path of the `_.context.ts` file that is nearest to `path` in
237
+ * the directory hierarchy, relative to the script's output directory.
238
+ *
239
+ * @param destination - Output root directory.
240
+ * @param path - Repository-relative path of the script being generated.
241
+ */
242
+ findContextPath(destination, path) {
243
+ return pathRelative(nodePath.join(destination, nodePath.dirname(path)), this.nearestContextFile(destination, path));
244
+ }
245
+ /**
246
+ * Walks up the directory tree from `path` to find the nearest
247
+ * `_.context.ts` file, falling back to `routes/_.context.ts` at the root.
248
+ *
249
+ * @param destination - Output root directory.
250
+ * @param path - Repository-relative path to start from.
251
+ */
252
+ nearestContextFile(destination, path) {
253
+ const directory = pathDirname(path).replace("types/paths", "routes");
254
+ const candidate = nodePath.join(destination, directory, "_.context.ts");
255
+ if (directory.length <= 1) {
256
+ // No _context.ts was found so import the one that should be in the root
257
+ return nodePath.join(destination, "routes", "_.context.ts");
258
+ }
259
+ if (existsSync(candidate)) {
260
+ return candidate;
261
+ }
262
+ return this.nearestContextFile(destination, nodePath.join(path, ".."));
263
+ }
264
+ }
@@ -0,0 +1,104 @@
1
+ import type { Specification } from "./specification.js";
2
+ export type RequirementData = (Record<string, unknown> & {
3
+ $ref?: string;
4
+ }) | string | number | boolean | null;
5
+ /**
6
+ * A node in the dereferenced OpenAPI spec tree.
7
+ *
8
+ * A `Requirement` wraps a raw JSON object (`data`) together with its location
9
+ * (`url`, a JSON Pointer) and a back-reference to the owning
10
+ * {@link Specification}. Navigation methods (`get`, `select`, `find`, …)
11
+ * transparently follow `$ref` pointers.
12
+ */
13
+ export declare class Requirement {
14
+ data: RequirementData;
15
+ url: string;
16
+ specification: Specification | undefined;
17
+ /**
18
+ * The requirement that produced this one via a `get()` call, or `undefined`
19
+ * for root requirements that were constructed directly.
20
+ *
21
+ * For path-traversal purposes this is the "logical" parent: when a `$ref` is
22
+ * followed, the parent is the resolved reference target rather than the
23
+ * `$ref` node itself.
24
+ */
25
+ parent: Requirement | undefined;
26
+ constructor(data: RequirementData, url?: string, specification?: Specification | undefined);
27
+ /** `true` when this node is a JSON Reference (`$ref`) rather than inline data. */
28
+ get isReference(): boolean;
29
+ /**
30
+ * When this node is a JSON Reference, returns the raw `$ref` URL string.
31
+ * Returns `undefined` for non-reference (inline) nodes.
32
+ */
33
+ get refUrl(): string | undefined;
34
+ /**
35
+ * Resolves the `$ref` and returns the target {@link Requirement}.
36
+ *
37
+ * @throws When `isReference` is `false` or the specification is not set.
38
+ */
39
+ reference(): Requirement;
40
+ /**
41
+ * Returns `true` when this node has a child property named `item`.
42
+ *
43
+ * Transparently follows `$ref` references.
44
+ *
45
+ * @param item - The property key to check.
46
+ */
47
+ has(item: string): boolean;
48
+ /**
49
+ * Returns the child {@link Requirement} for `item`, or `undefined`.
50
+ *
51
+ * @param item - The property key (string) or array index (number).
52
+ */
53
+ get(item: string | number): Requirement | undefined;
54
+ /**
55
+ * Navigates to a descendant node using a slash-delimited JSON Pointer path.
56
+ *
57
+ * Tilde-escaped characters (`~0` → `~`, `~1` → `/`) and percent-encoded
58
+ * characters are unescaped during traversal.
59
+ *
60
+ * @param path - A slash-delimited path (e.g. `"responses/200/content"`).
61
+ * @returns The target {@link Requirement}, or `undefined` if the path does
62
+ * not exist.
63
+ */
64
+ select(path: string): Requirement | undefined;
65
+ /**
66
+ * Iterates over all child properties and calls `callback` with each child
67
+ * requirement and its key.
68
+ *
69
+ * @param callback - Called for each child with `(child, key)`.
70
+ */
71
+ forEach(callback: (value: Requirement, key: string) => void): void;
72
+ /**
73
+ * Maps over all child properties and returns the collected results.
74
+ *
75
+ * @param callback - Transformation function called with `(child, key)`.
76
+ */
77
+ map<T>(callback: (value: Requirement, key: string) => T): T[];
78
+ /**
79
+ * Maps and flattens over all child properties.
80
+ *
81
+ * @param callback - Transformation function that may return a value or an
82
+ * array of values.
83
+ */
84
+ flatMap<T>(callback: (value: Requirement, key: string) => T | T[]): T[];
85
+ /**
86
+ * Returns the first child for which `callback` returns `true`, or
87
+ * `undefined` when nothing matches.
88
+ *
89
+ * @param callback - Predicate called with `(child, key)`.
90
+ */
91
+ find(callback: (value: Requirement, key: string) => boolean): Requirement | undefined;
92
+ /**
93
+ * Escapes a JSON Pointer token: `~` → `~0`, `/` → `~1`.
94
+ *
95
+ * @param value - The token to escape.
96
+ */
97
+ escapeJsonPointer(value: string | number): string | number;
98
+ /**
99
+ * Unescapes a JSON Pointer token: `~1` → `/`, `~0` → `~`.
100
+ *
101
+ * @param pointer - The token to unescape.
102
+ */
103
+ unescapeJsonPointer(pointer: string | number): string | number;
104
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * A node in the dereferenced OpenAPI spec tree.
3
+ *
4
+ * A `Requirement` wraps a raw JSON object (`data`) together with its location
5
+ * (`url`, a JSON Pointer) and a back-reference to the owning
6
+ * {@link Specification}. Navigation methods (`get`, `select`, `find`, …)
7
+ * transparently follow `$ref` pointers.
8
+ */
9
+ export class Requirement {
10
+ data;
11
+ url;
12
+ specification;
13
+ /**
14
+ * The requirement that produced this one via a `get()` call, or `undefined`
15
+ * for root requirements that were constructed directly.
16
+ *
17
+ * For path-traversal purposes this is the "logical" parent: when a `$ref` is
18
+ * followed, the parent is the resolved reference target rather than the
19
+ * `$ref` node itself.
20
+ */
21
+ parent;
22
+ constructor(data, url = "", specification = undefined) {
23
+ this.data = data;
24
+ this.url = url;
25
+ this.specification = specification;
26
+ }
27
+ /** `true` when this node is a JSON Reference (`$ref`) rather than inline data. */
28
+ get isReference() {
29
+ return (typeof this.data === "object" &&
30
+ this.data !== null &&
31
+ this.data["$ref"] !== undefined);
32
+ }
33
+ /**
34
+ * When this node is a JSON Reference, returns the raw `$ref` URL string.
35
+ * Returns `undefined` for non-reference (inline) nodes.
36
+ */
37
+ get refUrl() {
38
+ if (typeof this.data !== "object" || this.data === null) {
39
+ return undefined;
40
+ }
41
+ return this.data["$ref"];
42
+ }
43
+ /**
44
+ * Resolves the `$ref` and returns the target {@link Requirement}.
45
+ *
46
+ * @throws When `isReference` is `false` or the specification is not set.
47
+ */
48
+ reference() {
49
+ return this.specification.getRequirement(this.refUrl);
50
+ }
51
+ /**
52
+ * Returns `true` when this node has a child property named `item`.
53
+ *
54
+ * Transparently follows `$ref` references.
55
+ *
56
+ * @param item - The property key to check.
57
+ */
58
+ has(item) {
59
+ if (this.isReference) {
60
+ return this.reference().has(item);
61
+ }
62
+ if (typeof this.data !== "object" || this.data === null) {
63
+ return false;
64
+ }
65
+ return item in this.data;
66
+ }
67
+ /**
68
+ * Returns the child {@link Requirement} for `item`, or `undefined`.
69
+ *
70
+ * @param item - The property key (string) or array index (number).
71
+ */
72
+ get(item) {
73
+ if (this.isReference) {
74
+ return this.reference().get(item);
75
+ }
76
+ const key = String(item);
77
+ if (!this.has(key)) {
78
+ return undefined;
79
+ }
80
+ if (typeof this.data !== "object" || this.data === null) {
81
+ return undefined;
82
+ }
83
+ const objectData = this.data;
84
+ const child = new Requirement(objectData[key], `${this.url}/${this.escapeJsonPointer(key)}`, this.specification);
85
+ child.parent = this;
86
+ return child;
87
+ }
88
+ /**
89
+ * Navigates to a descendant node using a slash-delimited JSON Pointer path.
90
+ *
91
+ * Tilde-escaped characters (`~0` → `~`, `~1` → `/`) and percent-encoded
92
+ * characters are unescaped during traversal.
93
+ *
94
+ * @param path - A slash-delimited path (e.g. `"responses/200/content"`).
95
+ * @returns The target {@link Requirement}, or `undefined` if the path does
96
+ * not exist.
97
+ */
98
+ select(path) {
99
+ const parts = path
100
+ .split("/")
101
+ .map((p) => this.unescapeJsonPointer(p))
102
+ // Unescape URL encoded characters (e.g. %20 -> " ")
103
+ // Technically we should not be unescaping, but it came up in https://github.com/pmcelhaney/counterfact/issues/1083
104
+ // and I can't think of a reason anyone would intentionally put a % in a key name.
105
+ .map((part) => {
106
+ try {
107
+ return decodeURIComponent(part);
108
+ }
109
+ catch {
110
+ return part;
111
+ }
112
+ });
113
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
114
+ let result = this;
115
+ for (const part of parts) {
116
+ result = result.get(part);
117
+ if (result === undefined) {
118
+ return undefined;
119
+ }
120
+ }
121
+ return result;
122
+ }
123
+ /**
124
+ * Iterates over all child properties and calls `callback` with each child
125
+ * requirement and its key.
126
+ *
127
+ * @param callback - Called for each child with `(child, key)`.
128
+ */
129
+ forEach(callback) {
130
+ if (typeof this.data !== "object" || this.data === null) {
131
+ return;
132
+ }
133
+ Object.keys(this.data).forEach((key) => {
134
+ callback(this.select(this.escapeJsonPointer(key)), key);
135
+ });
136
+ }
137
+ /**
138
+ * Maps over all child properties and returns the collected results.
139
+ *
140
+ * @param callback - Transformation function called with `(child, key)`.
141
+ */
142
+ map(callback) {
143
+ const result = [];
144
+ this.forEach((value, key) => result.push(callback(value, key)));
145
+ return result;
146
+ }
147
+ /**
148
+ * Maps and flattens over all child properties.
149
+ *
150
+ * @param callback - Transformation function that may return a value or an
151
+ * array of values.
152
+ */
153
+ flatMap(callback) {
154
+ return this.map(callback).flat();
155
+ }
156
+ /**
157
+ * Returns the first child for which `callback` returns `true`, or
158
+ * `undefined` when nothing matches.
159
+ *
160
+ * @param callback - Predicate called with `(child, key)`.
161
+ */
162
+ find(callback) {
163
+ let result;
164
+ this.forEach((value, key) => {
165
+ if (result === undefined && callback(value, key)) {
166
+ result = value;
167
+ }
168
+ });
169
+ return result;
170
+ }
171
+ /**
172
+ * Escapes a JSON Pointer token: `~` → `~0`, `/` → `~1`.
173
+ *
174
+ * @param value - The token to escape.
175
+ */
176
+ escapeJsonPointer(value) {
177
+ if (typeof value !== "string")
178
+ return value;
179
+ return value.replaceAll("~", "~0").replaceAll("/", "~1");
180
+ }
181
+ /**
182
+ * Unescapes a JSON Pointer token: `~1` → `/`, `~0` → `~`.
183
+ *
184
+ * @param pointer - The token to unescape.
185
+ */
186
+ unescapeJsonPointer(pointer) {
187
+ if (typeof pointer !== "string")
188
+ return pointer;
189
+ return pointer.replaceAll("~1", "/").replaceAll("~0", "~");
190
+ }
191
+ }
@@ -0,0 +1 @@
1
+ export declare const RESERVED_WORDS: Set<string>;
@@ -0,0 +1,50 @@
1
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#reserved_words
2
+ export const RESERVED_WORDS = new Set([
3
+ "break",
4
+ "case",
5
+ "catch",
6
+ "class",
7
+ "const",
8
+ "continue",
9
+ "debugger",
10
+ "default",
11
+ "delete",
12
+ "do",
13
+ "else",
14
+ "export",
15
+ "extends",
16
+ "false",
17
+ "finally",
18
+ "for",
19
+ "function",
20
+ "if",
21
+ "import",
22
+ "in",
23
+ "instanceof",
24
+ "new",
25
+ "null",
26
+ "return",
27
+ "static",
28
+ "super",
29
+ "switch",
30
+ "this",
31
+ "throw",
32
+ "true",
33
+ "try",
34
+ "typeof",
35
+ "var",
36
+ "void",
37
+ "while",
38
+ "with",
39
+ "yield",
40
+ "await",
41
+ "enum",
42
+ "implements",
43
+ "interface",
44
+ "let",
45
+ "package",
46
+ "private",
47
+ "protected",
48
+ "public",
49
+ "type",
50
+ ]);
@@ -0,0 +1,16 @@
1
+ import { TypeCoder } from "./type-coder.js";
2
+ import type { Requirement } from "./requirement.js";
3
+ import type { Script } from "./script.js";
4
+ export declare class ResponseTypeCoder extends TypeCoder {
5
+ openApi2MediaTypes: string[];
6
+ constructor(requirement: Requirement, version?: string, openApi2MediaTypes?: string[]);
7
+ names(): Generator<string>;
8
+ buildContentObjectType(script: Script, response: Requirement): [string, string][];
9
+ printContentObjectType(script: Script, response: Requirement): string;
10
+ buildHeaders(script: Script, response: Requirement): [string, string][];
11
+ printHeaders(script: Script, response: Requirement): string;
12
+ printRequiredHeaders(response: Requirement): string;
13
+ buildExamplesObjectType(response: Requirement): string;
14
+ modulePath(): string;
15
+ writeCode(script: Script): string;
16
+ }