counterfact 2.7.0 → 2.8.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/README.md +4 -159
- package/bin/counterfact.js +10 -2
- package/dist/app.js +74 -20
- package/dist/migrate/update-route-types.js +2 -3
- package/dist/repl/raw-http-client.js +19 -0
- package/dist/repl/repl.js +26 -7
- package/dist/repl/route-builder.js +68 -0
- package/dist/server/constants.js +8 -0
- package/dist/server/context-registry.js +54 -1
- package/dist/server/create-koa-app.js +27 -4
- package/dist/server/determine-module-kind.js +13 -0
- package/dist/server/dispatcher.js +46 -0
- package/dist/server/file-discovery.js +20 -9
- package/dist/server/is-proxy-enabled-for-path.js +12 -0
- package/dist/server/json-to-xml.js +10 -0
- package/dist/server/koa-middleware.js +18 -1
- package/dist/server/load-openapi-document.js +4 -11
- package/dist/server/module-dependency-graph.js +25 -0
- package/dist/server/module-loader.js +44 -21
- package/dist/server/module-tree.js +36 -0
- package/dist/server/openapi-document.js +69 -0
- package/dist/server/openapi-middleware.js +34 -5
- package/dist/server/registry.js +89 -0
- package/dist/server/response-builder.js +15 -0
- package/dist/server/scenario-registry.js +26 -0
- package/dist/server/tools.js +27 -0
- package/dist/server/transpiler.js +23 -9
- package/dist/typescript-generator/code-generator.js +117 -4
- package/dist/typescript-generator/coder.js +76 -0
- package/dist/typescript-generator/operation-coder.js +12 -4
- package/dist/typescript-generator/operation-type-coder.js +39 -4
- package/dist/typescript-generator/parameters-type-coder.js +2 -4
- package/dist/typescript-generator/prune.js +2 -1
- package/dist/typescript-generator/repository.js +76 -20
- package/dist/typescript-generator/requirement.js +69 -0
- package/dist/typescript-generator/{generate.js → scenario-file-generator.js} +98 -81
- package/dist/typescript-generator/script.js +70 -7
- package/dist/typescript-generator/specification.js +27 -0
- package/dist/util/ensure-directory-exists.js +7 -0
- package/dist/util/forward-slash-path.js +63 -0
- package/dist/util/read-file.js +11 -0
- package/dist/util/runtime-can-execute-erasable-ts.js +11 -0
- package/dist/util/windows-escape.js +18 -0
- package/package.json +4 -4
|
@@ -1,30 +1,84 @@
|
|
|
1
1
|
import { RESERVED_WORDS } from "./reserved-words.js";
|
|
2
|
+
/**
|
|
3
|
+
* Base class for all code-generation helpers in the TypeScript generator.
|
|
4
|
+
*
|
|
5
|
+
* A `Coder` wraps a single {@link Requirement} node from the OpenAPI spec and
|
|
6
|
+
* knows how to emit TypeScript code for it. Subclasses override
|
|
7
|
+
* {@link writeCode} to produce the actual source text.
|
|
8
|
+
*
|
|
9
|
+
* Coders are used by {@link Script} and {@link Repository} to lazily generate
|
|
10
|
+
* exports and imports, resolving `$ref` references through the
|
|
11
|
+
* {@link Specification} before writing.
|
|
12
|
+
*/
|
|
2
13
|
export class Coder {
|
|
3
14
|
requirement;
|
|
4
15
|
constructor(requirement) {
|
|
5
16
|
this.requirement = requirement;
|
|
6
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A stable cache key for this coder, composed of the constructor name and
|
|
20
|
+
* either the `$ref` value (for references) or the requirement URL.
|
|
21
|
+
*/
|
|
7
22
|
get id() {
|
|
8
23
|
if (this.requirement.isReference) {
|
|
9
24
|
return `${this.constructor.name}@${this.requirement.data["$ref"]}`;
|
|
10
25
|
}
|
|
11
26
|
return `${this.constructor.name}@${this.requirement.url}`;
|
|
12
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Optional preamble emitted before the `export` keyword.
|
|
30
|
+
*
|
|
31
|
+
* Subclasses can return a string (e.g. a type alias) that must appear in the
|
|
32
|
+
* output before this coder's export statement.
|
|
33
|
+
*
|
|
34
|
+
* @param _path - The path of the script being written (unused in base class).
|
|
35
|
+
*/
|
|
13
36
|
beforeExport(_path) {
|
|
14
37
|
return "";
|
|
15
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns a JSDoc comment block to be placed immediately before the export.
|
|
41
|
+
*
|
|
42
|
+
* Returns `""` by default; subclasses override this to surface OpenAPI
|
|
43
|
+
* metadata (description, summary, examples, etc.).
|
|
44
|
+
*/
|
|
16
45
|
jsdoc() {
|
|
17
46
|
return "";
|
|
18
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Writes this coder's contribution to `script`.
|
|
50
|
+
*
|
|
51
|
+
* When the requirement is a `$ref`, delegates to {@link Script.import} so
|
|
52
|
+
* the reference target is exported from its own module. Otherwise calls
|
|
53
|
+
* {@link writeCode}.
|
|
54
|
+
*
|
|
55
|
+
* @param script - The script being assembled.
|
|
56
|
+
* @returns The TypeScript source text for this coder's export value.
|
|
57
|
+
*/
|
|
19
58
|
write(script) {
|
|
20
59
|
if (this.requirement.isReference) {
|
|
21
60
|
return script.import(this);
|
|
22
61
|
}
|
|
23
62
|
return this.writeCode(script);
|
|
24
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Generates the TypeScript source text for this coder's value.
|
|
66
|
+
*
|
|
67
|
+
* This method is abstract — subclasses **must** override it.
|
|
68
|
+
*
|
|
69
|
+
* @param _script - The script being assembled.
|
|
70
|
+
* @throws Always — callers should never reach the base implementation.
|
|
71
|
+
*/
|
|
25
72
|
writeCode(_script) {
|
|
26
73
|
throw new Error("write() is abstract and should be overwritten by a subclass");
|
|
27
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolves `$ref` references by returning the target coder.
|
|
77
|
+
*
|
|
78
|
+
* When this coder's requirement is not a reference, returns `this`.
|
|
79
|
+
* Otherwise loads the referenced requirement and wraps it in an instance of
|
|
80
|
+
* the same concrete coder class.
|
|
81
|
+
*/
|
|
28
82
|
async delegate() {
|
|
29
83
|
if (!this.requirement.isReference) {
|
|
30
84
|
return this;
|
|
@@ -32,6 +86,15 @@ export class Coder {
|
|
|
32
86
|
const requirement = await this.requirement.reference();
|
|
33
87
|
return new this.constructor(requirement);
|
|
34
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Generator that yields candidate export names for this coder.
|
|
91
|
+
*
|
|
92
|
+
* The first name is derived from the last path segment of the requirement
|
|
93
|
+
* URL, sanitised to be a valid TypeScript identifier. Subsequent names have
|
|
94
|
+
* an incrementing numeric suffix to resolve collisions.
|
|
95
|
+
*
|
|
96
|
+
* @param rawName - Override the starting name (used by subclasses).
|
|
97
|
+
*/
|
|
35
98
|
*names(rawName = this.requirement.url.split("/").at(-1)) {
|
|
36
99
|
const name = rawName
|
|
37
100
|
.replace(/^\d/u, (digit) => `_${digit}`)
|
|
@@ -45,9 +108,22 @@ export class Coder {
|
|
|
45
108
|
yield baseName + index;
|
|
46
109
|
}
|
|
47
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Returns an optional TypeScript type annotation string to be placed between
|
|
113
|
+
* the export name and its value (`export const name: <type> = value`).
|
|
114
|
+
*
|
|
115
|
+
* Returns `""` by default.
|
|
116
|
+
*/
|
|
48
117
|
typeDeclaration(_namespace, _script) {
|
|
49
118
|
return "";
|
|
50
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Returns the repository-relative path of the script where this coder's
|
|
122
|
+
* export should live when imported by another script.
|
|
123
|
+
*
|
|
124
|
+
* Subclasses override this to place type exports in `types/paths/…` and
|
|
125
|
+
* route exports in `routes/…`.
|
|
126
|
+
*/
|
|
51
127
|
modulePath() {
|
|
52
128
|
return "did-not-override-coder-modulePath.ts";
|
|
53
129
|
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { pathJoin } from "../util/forward-slash-path.js";
|
|
2
2
|
import { Coder } from "./coder.js";
|
|
3
3
|
import { OperationTypeCoder, } from "./operation-type-coder.js";
|
|
4
|
+
/**
|
|
5
|
+
* Generates the default route handler stub for a single OpenAPI operation.
|
|
6
|
+
*
|
|
7
|
+
* The generated stub calls `$.response[statusCode].random()` (or `.empty()`)
|
|
8
|
+
* for the first response defined in the spec. It is only written when no
|
|
9
|
+
* handler file exists yet — users are expected to replace it with real logic.
|
|
10
|
+
*
|
|
11
|
+
* The corresponding TypeScript type is emitted by {@link OperationTypeCoder}
|
|
12
|
+
* into the `types/paths/…` tree.
|
|
13
|
+
*/
|
|
4
14
|
export class OperationCoder extends Coder {
|
|
5
15
|
requestMethod;
|
|
6
16
|
securitySchemes;
|
|
@@ -38,8 +48,6 @@ export class OperationCoder extends Coder {
|
|
|
38
48
|
.split("/")
|
|
39
49
|
.at(-2)
|
|
40
50
|
.replaceAll("~1", "/");
|
|
41
|
-
return `${
|
|
42
|
-
.join("routes", pathString)
|
|
43
|
-
.replaceAll("\\", "/")}.types.ts`;
|
|
51
|
+
return `${pathJoin("routes", pathString)}.types.ts`;
|
|
44
52
|
}
|
|
45
53
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { pathJoin } from "../util/forward-slash-path.js";
|
|
2
2
|
import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
|
|
3
3
|
import { buildJsDoc } from "./jsdoc.js";
|
|
4
4
|
import { ParameterExportTypeCoder } from "./parameter-export-type-coder.js";
|
|
@@ -24,6 +24,15 @@ function sanitizeIdentifier(value) {
|
|
|
24
24
|
}
|
|
25
25
|
return result || "_";
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Generates the TypeScript type for a single OpenAPI operation.
|
|
29
|
+
*
|
|
30
|
+
* The emitted type describes the function signature that a Counterfact route
|
|
31
|
+
* handler must satisfy, including strongly-typed `query`, `path`, `headers`,
|
|
32
|
+
* `cookie`, `body`, `context`, `response`, and `user` arguments.
|
|
33
|
+
*
|
|
34
|
+
* Output is written to `types/paths/<route>.types.ts`.
|
|
35
|
+
*/
|
|
27
36
|
export class OperationTypeCoder extends TypeCoder {
|
|
28
37
|
requestMethod;
|
|
29
38
|
securitySchemes;
|
|
@@ -35,6 +44,10 @@ export class OperationTypeCoder extends TypeCoder {
|
|
|
35
44
|
this.requestMethod = requestMethod;
|
|
36
45
|
this.securitySchemes = securitySchemes;
|
|
37
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns the base identifier for this operation, derived from its
|
|
49
|
+
* `operationId` (sanitised) or falling back to `HTTP_<METHOD>`.
|
|
50
|
+
*/
|
|
38
51
|
getOperationBaseName() {
|
|
39
52
|
const operationId = this.requirement.get("operationId")?.data;
|
|
40
53
|
return operationId
|
|
@@ -47,6 +60,19 @@ export class OperationTypeCoder extends TypeCoder {
|
|
|
47
60
|
names() {
|
|
48
61
|
return super.names(this.getOperationBaseName());
|
|
49
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Generates and exports a named parameter type (e.g. `ListPets_Query`) from
|
|
65
|
+
* `modulePath` and returns the exported type name.
|
|
66
|
+
*
|
|
67
|
+
* Returns `"never"` without creating an export when `inlineType` is
|
|
68
|
+
* `"never"`.
|
|
69
|
+
*
|
|
70
|
+
* @param script - The script being assembled.
|
|
71
|
+
* @param parameterKind - `"query"`, `"path"`, `"headers"`, or `"cookie"`.
|
|
72
|
+
* @param inlineType - The inline TypeScript type string to export.
|
|
73
|
+
* @param baseName - The base identifier prefix for the exported type name.
|
|
74
|
+
* @param modulePath - The repository-relative path of the type file.
|
|
75
|
+
*/
|
|
50
76
|
exportParameterType(script, parameterKind, inlineType, baseName, modulePath) {
|
|
51
77
|
if (inlineType === "never") {
|
|
52
78
|
return "never";
|
|
@@ -57,6 +83,11 @@ export class OperationTypeCoder extends TypeCoder {
|
|
|
57
83
|
coder._modulePath = modulePath;
|
|
58
84
|
return script.export(coder, true);
|
|
59
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Returns the union of all possible response type shapes for this operation.
|
|
88
|
+
*
|
|
89
|
+
* @param script - The script being assembled (used to resolve imports).
|
|
90
|
+
*/
|
|
60
91
|
responseTypes(script) {
|
|
61
92
|
return this.requirement
|
|
62
93
|
.get("responses")
|
|
@@ -96,10 +127,14 @@ export class OperationTypeCoder extends TypeCoder {
|
|
|
96
127
|
.split("/")
|
|
97
128
|
.at(-2)
|
|
98
129
|
.replaceAll("~1", "/");
|
|
99
|
-
return `${
|
|
100
|
-
.join("types/paths", pathString === "/" ? "/index" : pathString)
|
|
101
|
-
.replaceAll("\\", "/")}.types.ts`;
|
|
130
|
+
return `${pathJoin("types/paths", pathString === "/" ? "/index" : pathString)}.types.ts`;
|
|
102
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Returns the TypeScript type for the `user` argument.
|
|
134
|
+
*
|
|
135
|
+
* When the operation is protected by HTTP Basic auth, the type is
|
|
136
|
+
* `{username?: string, password?: string}`. Otherwise it is `"never"`.
|
|
137
|
+
*/
|
|
103
138
|
userType() {
|
|
104
139
|
if (this.securitySchemes.some(({ scheme, type }) => type === "http" && scheme === "basic")) {
|
|
105
140
|
return "{username?: string, password?: string}";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { pathJoin } from "../util/forward-slash-path.js";
|
|
2
2
|
import { buildJsDoc } from "./jsdoc.js";
|
|
3
3
|
import { SchemaTypeCoder } from "./schema-type-coder.js";
|
|
4
4
|
import { TypeCoder } from "./type-coder.js";
|
|
@@ -39,8 +39,6 @@ export class ParametersTypeCoder extends TypeCoder {
|
|
|
39
39
|
.split("/")
|
|
40
40
|
.at(-2)
|
|
41
41
|
.replaceAll("~1", "/");
|
|
42
|
-
return `${
|
|
43
|
-
.join("parameters", pathString)
|
|
44
|
-
.replaceAll("\\", "/")}.types.ts`;
|
|
42
|
+
return `${pathJoin("parameters", pathString)}.types.ts`;
|
|
45
43
|
}
|
|
46
44
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import nodePath from "node:path";
|
|
3
3
|
import createDebug from "debug";
|
|
4
|
+
import { toForwardSlashPath } from "../util/forward-slash-path.js";
|
|
4
5
|
const debug = createDebug("counterfact:typescript-generator:prune");
|
|
5
6
|
/**
|
|
6
7
|
* Collects all .ts route files in a directory recursively.
|
|
@@ -87,7 +88,7 @@ export async function pruneRoutes(destination, openApiPaths) {
|
|
|
87
88
|
debug("actual route files: %o", actualFiles);
|
|
88
89
|
let prunedCount = 0;
|
|
89
90
|
for (const file of actualFiles) {
|
|
90
|
-
const normalizedFile = file
|
|
91
|
+
const normalizedFile = toForwardSlashPath(file);
|
|
91
92
|
if (!expectedFiles.has(normalizedFile)) {
|
|
92
93
|
const fullPath = nodePath.join(routesDir, file);
|
|
93
94
|
debug("pruning %s", fullPath);
|
|
@@ -4,17 +4,33 @@ import nodePath, { dirname } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import createDebug from "debug";
|
|
6
6
|
import { ensureDirectoryExists } from "../util/ensure-directory-exists.js";
|
|
7
|
+
import { toForwardSlashPath, pathJoin, pathRelative, pathDirname, } from "../util/forward-slash-path.js";
|
|
7
8
|
import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
|
|
8
9
|
import { Script } from "./script.js";
|
|
9
10
|
import { escapePathForWindows } from "../util/windows-escape.js";
|
|
10
11
|
const debug = createDebug("counterfact:server:repository");
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const __dirname = toForwardSlashPath(dirname(fileURLToPath(import.meta.url)));
|
|
12
13
|
debug("dirname is %s", __dirname);
|
|
14
|
+
/**
|
|
15
|
+
* Collection of {@link Script} objects keyed by their repository-relative
|
|
16
|
+
* path.
|
|
17
|
+
*
|
|
18
|
+
* Coders call {@link get} to obtain (or create) the script where they should
|
|
19
|
+
* export their generated TypeScript. After all coders have been registered,
|
|
20
|
+
* {@link writeFiles} waits for every script to finish and writes the output to
|
|
21
|
+
* disk.
|
|
22
|
+
*/
|
|
13
23
|
export class Repository {
|
|
14
24
|
scripts;
|
|
15
25
|
constructor() {
|
|
16
26
|
this.scripts = new Map();
|
|
17
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns the {@link Script} for `path`, creating it if it does not yet
|
|
30
|
+
* exist.
|
|
31
|
+
*
|
|
32
|
+
* @param path - Repository-relative path (e.g. `"routes/pets.ts"`).
|
|
33
|
+
*/
|
|
18
34
|
get(path) {
|
|
19
35
|
debug("getting script at %s", path);
|
|
20
36
|
if (this.scripts.has(path)) {
|
|
@@ -26,12 +42,22 @@ export class Repository {
|
|
|
26
42
|
this.scripts.set(path, script);
|
|
27
43
|
return script;
|
|
28
44
|
}
|
|
45
|
+
/** Waits until all scripts have resolved all of their pending export promises. */
|
|
29
46
|
async finished() {
|
|
30
47
|
while (Array.from(this.scripts.values()).some((script) => script.isInProgress())) {
|
|
31
48
|
debug("waiting for %i scripts to finish", this.scripts.size);
|
|
32
49
|
await Promise.all(Array.from(this.scripts.values(), (script) => script.finished()));
|
|
33
50
|
}
|
|
34
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Copies the compiled `counterfact-types` directory from the Counterfact
|
|
54
|
+
* distribution into the generated output tree.
|
|
55
|
+
*
|
|
56
|
+
* Returns `false` when the source directory does not exist (e.g. running
|
|
57
|
+
* from source without a prior build).
|
|
58
|
+
*
|
|
59
|
+
* @param destination - The root of the generated output tree.
|
|
60
|
+
*/
|
|
35
61
|
async copyCoreFiles(destination) {
|
|
36
62
|
const sourcePath = nodePath.join(__dirname, "../../dist/server/counterfact-types");
|
|
37
63
|
const destinationPath = nodePath.join(destination, "counterfact-types");
|
|
@@ -40,13 +66,23 @@ export class Repository {
|
|
|
40
66
|
}
|
|
41
67
|
return fs.cp(sourcePath, destinationPath, { recursive: true });
|
|
42
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Waits for all scripts to finish, then writes each one to disk.
|
|
71
|
+
*
|
|
72
|
+
* Route files (`routes/…`) are never overwritten if they already exist on
|
|
73
|
+
* disk, preserving user edits. Type files (`types/…`) are always
|
|
74
|
+
* overwritten.
|
|
75
|
+
*
|
|
76
|
+
* @param destination - Absolute path to the output root directory.
|
|
77
|
+
* @param options - Controls which artefacts are written.
|
|
78
|
+
*/
|
|
43
79
|
async writeFiles(destination, { routes, types }) {
|
|
44
80
|
debug("waiting for %i or more scripts to finish before writing files", this.scripts.size);
|
|
45
81
|
await this.finished();
|
|
46
82
|
debug("all %i scripts are finished", this.scripts.size);
|
|
47
83
|
const writeFiles = Array.from(this.scripts.entries(), async ([path, script]) => {
|
|
48
84
|
const contents = await script.contents();
|
|
49
|
-
const fullPath = escapePathForWindows(
|
|
85
|
+
const fullPath = escapePathForWindows(pathJoin(destination, path));
|
|
50
86
|
await ensureDirectoryExists(fullPath);
|
|
51
87
|
const shouldWriteRoutes = routes && path.startsWith("routes");
|
|
52
88
|
const shouldWriteTypes = types && !path.startsWith("routes");
|
|
@@ -70,37 +106,57 @@ export class Repository {
|
|
|
70
106
|
await this.createDefaultContextFile(destination);
|
|
71
107
|
}
|
|
72
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Creates the default `routes/_.context.ts` file if it does not already
|
|
111
|
+
* exist.
|
|
112
|
+
*
|
|
113
|
+
* @param destination - Absolute path to the output root directory.
|
|
114
|
+
*/
|
|
73
115
|
async createDefaultContextFile(destination) {
|
|
74
116
|
const contextFilePath = nodePath.join(destination, "routes", "_.context.ts");
|
|
75
117
|
if (existsSync(contextFilePath)) {
|
|
76
118
|
return;
|
|
77
119
|
}
|
|
78
120
|
await ensureDirectoryExists(contextFilePath);
|
|
79
|
-
await fs.writeFile(contextFilePath,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
await fs.writeFile(contextFilePath, `import type { Context$ } from "../types/_.context.js";
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* This is the default context for Counterfact.
|
|
125
|
+
*
|
|
126
|
+
* It defines the context object in the REPL
|
|
127
|
+
* and the $.context object in the code.
|
|
128
|
+
*
|
|
129
|
+
* Add properties and methods to suit your needs.
|
|
130
|
+
*
|
|
131
|
+
* See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md
|
|
132
|
+
*/
|
|
90
133
|
|
|
134
|
+
export class Context {
|
|
135
|
+
constructor($: Context$) {
|
|
136
|
+
void $;
|
|
137
|
+
}
|
|
91
138
|
}
|
|
92
139
|
`);
|
|
93
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Returns the path of the `_.context.ts` file that is nearest to `path` in
|
|
143
|
+
* the directory hierarchy, relative to the script's output directory.
|
|
144
|
+
*
|
|
145
|
+
* @param destination - Output root directory.
|
|
146
|
+
* @param path - Repository-relative path of the script being generated.
|
|
147
|
+
*/
|
|
94
148
|
findContextPath(destination, path) {
|
|
95
|
-
return nodePath
|
|
96
|
-
.relative(nodePath.join(destination, nodePath.dirname(path)), this.nearestContextFile(destination, path))
|
|
97
|
-
.replaceAll("\\", "/");
|
|
149
|
+
return pathRelative(nodePath.join(destination, nodePath.dirname(path)), this.nearestContextFile(destination, path));
|
|
98
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Walks up the directory tree from `path` to find the nearest
|
|
153
|
+
* `_.context.ts` file, falling back to `routes/_.context.ts` at the root.
|
|
154
|
+
*
|
|
155
|
+
* @param destination - Output root directory.
|
|
156
|
+
* @param path - Repository-relative path to start from.
|
|
157
|
+
*/
|
|
99
158
|
nearestContextFile(destination, path) {
|
|
100
|
-
const directory =
|
|
101
|
-
.dirname(path)
|
|
102
|
-
.replaceAll("\\", "/")
|
|
103
|
-
.replace("types/paths", "routes");
|
|
159
|
+
const directory = pathDirname(path).replace("types/paths", "routes");
|
|
104
160
|
const candidate = nodePath.join(destination, directory, "_.context.ts");
|
|
105
161
|
if (directory.length <= 1) {
|
|
106
162
|
// No _context.ts was found so import the one that should be in the root
|
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
*/
|
|
1
9
|
export class Requirement {
|
|
2
10
|
data;
|
|
3
11
|
url;
|
|
@@ -7,18 +15,36 @@ export class Requirement {
|
|
|
7
15
|
this.url = url;
|
|
8
16
|
this.specification = specification;
|
|
9
17
|
}
|
|
18
|
+
/** `true` when this node is a JSON Reference (`$ref`) rather than inline data. */
|
|
10
19
|
get isReference() {
|
|
11
20
|
return this.data["$ref"] !== undefined;
|
|
12
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the `$ref` and returns the target {@link Requirement}.
|
|
24
|
+
*
|
|
25
|
+
* @throws When `isReference` is `false` or the specification is not set.
|
|
26
|
+
*/
|
|
13
27
|
reference() {
|
|
14
28
|
return this.specification.getRequirement(this.data["$ref"]);
|
|
15
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns `true` when this node has a child property named `item`.
|
|
32
|
+
*
|
|
33
|
+
* Transparently follows `$ref` references.
|
|
34
|
+
*
|
|
35
|
+
* @param item - The property key to check.
|
|
36
|
+
*/
|
|
16
37
|
has(item) {
|
|
17
38
|
if (this.isReference) {
|
|
18
39
|
return this.reference().has(item);
|
|
19
40
|
}
|
|
20
41
|
return item in this.data;
|
|
21
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns the child {@link Requirement} for `item`, or `undefined`.
|
|
45
|
+
*
|
|
46
|
+
* @param item - The property key (string) or array index (number).
|
|
47
|
+
*/
|
|
22
48
|
get(item) {
|
|
23
49
|
if (this.isReference) {
|
|
24
50
|
return this.reference().get(item);
|
|
@@ -29,6 +55,16 @@ export class Requirement {
|
|
|
29
55
|
}
|
|
30
56
|
return new Requirement(this.data[key], `${this.url}/${this.escapeJsonPointer(key)}`, this.specification);
|
|
31
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Navigates to a descendant node using a slash-delimited JSON Pointer path.
|
|
60
|
+
*
|
|
61
|
+
* Tilde-escaped characters (`~0` → `~`, `~1` → `/`) and percent-encoded
|
|
62
|
+
* characters are unescaped during traversal.
|
|
63
|
+
*
|
|
64
|
+
* @param path - A slash-delimited path (e.g. `"responses/200/content"`).
|
|
65
|
+
* @returns The target {@link Requirement}, or `undefined` if the path does
|
|
66
|
+
* not exist.
|
|
67
|
+
*/
|
|
32
68
|
select(path) {
|
|
33
69
|
const parts = path
|
|
34
70
|
.split("/")
|
|
@@ -54,19 +90,42 @@ export class Requirement {
|
|
|
54
90
|
}
|
|
55
91
|
return result;
|
|
56
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Iterates over all child properties and calls `callback` with each child
|
|
95
|
+
* requirement and its key.
|
|
96
|
+
*
|
|
97
|
+
* @param callback - Called for each child with `(child, key)`.
|
|
98
|
+
*/
|
|
57
99
|
forEach(callback) {
|
|
58
100
|
Object.keys(this.data).forEach((key) => {
|
|
59
101
|
callback(this.select(this.escapeJsonPointer(key)), key);
|
|
60
102
|
});
|
|
61
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Maps over all child properties and returns the collected results.
|
|
106
|
+
*
|
|
107
|
+
* @param callback - Transformation function called with `(child, key)`.
|
|
108
|
+
*/
|
|
62
109
|
map(callback) {
|
|
63
110
|
const result = [];
|
|
64
111
|
this.forEach((value, key) => result.push(callback(value, key)));
|
|
65
112
|
return result;
|
|
66
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Maps and flattens over all child properties.
|
|
116
|
+
*
|
|
117
|
+
* @param callback - Transformation function that may return a value or an
|
|
118
|
+
* array of values.
|
|
119
|
+
*/
|
|
67
120
|
flatMap(callback) {
|
|
68
121
|
return this.map(callback).flat();
|
|
69
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Returns the first child for which `callback` returns `true`, or
|
|
125
|
+
* `undefined` when nothing matches.
|
|
126
|
+
*
|
|
127
|
+
* @param callback - Predicate called with `(child, key)`.
|
|
128
|
+
*/
|
|
70
129
|
find(callback) {
|
|
71
130
|
let result;
|
|
72
131
|
this.forEach((value, key) => {
|
|
@@ -76,11 +135,21 @@ export class Requirement {
|
|
|
76
135
|
});
|
|
77
136
|
return result;
|
|
78
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Escapes a JSON Pointer token: `~` → `~0`, `/` → `~1`.
|
|
140
|
+
*
|
|
141
|
+
* @param value - The token to escape.
|
|
142
|
+
*/
|
|
79
143
|
escapeJsonPointer(value) {
|
|
80
144
|
if (typeof value !== "string")
|
|
81
145
|
return value;
|
|
82
146
|
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
83
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Unescapes a JSON Pointer token: `~1` → `/`, `~0` → `~`.
|
|
150
|
+
*
|
|
151
|
+
* @param pointer - The token to unescape.
|
|
152
|
+
*/
|
|
84
153
|
unescapeJsonPointer(pointer) {
|
|
85
154
|
if (typeof pointer !== "string")
|
|
86
155
|
return pointer;
|