UNPKG

counterfact

Version:

Generate a TypeScript-based mock server from an OpenAPI spec in seconds — with stateful routes, hot reload, and REPL support.

192 lines (191 loc) 6.27 kB
/** * A node in the dereferenced OpenAPI spec tree. * * A `Requirement` wraps a raw JSON object (`data`) together with its location * (`url`, a JSON Pointer) and a back-reference to the owning * {@link Specification}. Navigation methods (`get`, `select`, `find`, …) * transparently follow `$ref` pointers. */ export class Requirement { data; url; specification; /** * The requirement that produced this one via a `get()` call, or `undefined` * for root requirements that were constructed directly. * * For path-traversal purposes this is the "logical" parent: when a `$ref` is * followed, the parent is the resolved reference target rather than the * `$ref` node itself. */ parent; constructor(data, url = "", specification = undefined) { this.data = data; this.url = url; this.specification = specification; } /** `true` when this node is a JSON Reference (`$ref`) rather than inline data. */ get isReference() { return (typeof this.data === "object" && this.data !== null && this.data["$ref"] !== undefined); } /** * When this node is a JSON Reference, returns the raw `$ref` URL string. * Returns `undefined` for non-reference (inline) nodes. */ get refUrl() { if (typeof this.data !== "object" || this.data === null) { return undefined; } return this.data["$ref"]; } /** * Resolves the `$ref` and returns the target {@link Requirement}. * * @throws When `isReference` is `false` or the specification is not set. */ reference() { return this.specification.getRequirement(this.refUrl); } /** * Returns `true` when this node has a child property named `item`. * * Transparently follows `$ref` references. * * @param item - The property key to check. */ has(item) { if (this.isReference) { return this.reference().has(item); } if (typeof this.data !== "object" || this.data === null) { return false; } return item in this.data; } /** * Returns the child {@link Requirement} for `item`, or `undefined`. * * @param item - The property key (string) or array index (number). */ get(item) { if (this.isReference) { return this.reference().get(item); } const key = String(item); if (!this.has(key)) { return undefined; } if (typeof this.data !== "object" || this.data === null) { return undefined; } const objectData = this.data; const child = new Requirement(objectData[key], `${this.url}/${this.escapeJsonPointer(key)}`, this.specification); child.parent = this; return child; } /** * Navigates to a descendant node using a slash-delimited JSON Pointer path. * * Tilde-escaped characters (`~0` → `~`, `~1` → `/`) and percent-encoded * characters are unescaped during traversal. * * @param path - A slash-delimited path (e.g. `"responses/200/content"`). * @returns The target {@link Requirement}, or `undefined` if the path does * not exist. */ select(path) { const parts = path .split("/") .map((p) => this.unescapeJsonPointer(p)) // Unescape URL encoded characters (e.g. %20 -> " ") // Technically we should not be unescaping, but it came up in https://github.com/pmcelhaney/counterfact/issues/1083 // and I can't think of a reason anyone would intentionally put a % in a key name. .map((part) => { try { return decodeURIComponent(part); } catch { return part; } }); // eslint-disable-next-line @typescript-eslint/no-this-alias let result = this; for (const part of parts) { result = result.get(part); if (result === undefined) { return undefined; } } return result; } /** * Iterates over all child properties and calls `callback` with each child * requirement and its key. * * @param callback - Called for each child with `(child, key)`. */ forEach(callback) { if (typeof this.data !== "object" || this.data === null) { return; } Object.keys(this.data).forEach((key) => { callback(this.select(this.escapeJsonPointer(key)), key); }); } /** * Maps over all child properties and returns the collected results. * * @param callback - Transformation function called with `(child, key)`. */ map(callback) { const result = []; this.forEach((value, key) => result.push(callback(value, key))); return result; } /** * Maps and flattens over all child properties. * * @param callback - Transformation function that may return a value or an * array of values. */ flatMap(callback) { return this.map(callback).flat(); } /** * Returns the first child for which `callback` returns `true`, or * `undefined` when nothing matches. * * @param callback - Predicate called with `(child, key)`. */ find(callback) { let result; this.forEach((value, key) => { if (result === undefined && callback(value, key)) { result = value; } }); return result; } /** * Escapes a JSON Pointer token: `~` → `~0`, `/` → `~1`. * * @param value - The token to escape. */ escapeJsonPointer(value) { if (typeof value !== "string") return value; return value.replaceAll("~", "~0").replaceAll("/", "~1"); } /** * Unescapes a JSON Pointer token: `~1` → `/`, `~0` → `~`. * * @param pointer - The token to unescape. */ unescapeJsonPointer(pointer) { if (typeof pointer !== "string") return pointer; return pointer.replaceAll("~1", "/").replaceAll("~0", "~"); } }