UNPKG

counterfact

Version:

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

120 lines (119 loc) 4.44 kB
import { load as loadYaml } from "js-yaml"; import { JSONPath } from "jsonpath-plus"; import { readFile } from "./read-file.js"; /** * Deeply merges `source` into `target`, overwriting scalar values and * recursively merging plain objects. Arrays and non-plain-object values in * `source` always overwrite the corresponding entry in `target`. */ function deepMerge(target, source) { for (const [key, value] of Object.entries(source)) { // Guard against prototype pollution attacks. if (key === "__proto__" || key === "constructor" || key === "prototype") { continue; } if (typeof value === "object" && value !== null && !Array.isArray(value) && typeof target[key] === "object" && target[key] !== null && !Array.isArray(target[key])) { deepMerge(target[key], value); } else { target[key] = value; } } } /** * Applies a list of overlay actions to `document` in place. * * Each action may either: * - **update**: deep-merge the `action.update` object into every node matched * by the JSONPath `action.target`. * - **remove**: delete every node matched by `action.target` from its parent. * * @param document - The OpenAPI document object to mutate. * @param actions - The ordered list of overlay actions to apply. */ export function applyOverlayActions(document, actions) { for (const action of actions) { const results = JSONPath({ path: action.target, json: document, resultType: "all", }); if (action.remove === true) { // Iterate in reverse so that removing by numeric index doesn't shift // subsequent items in the same parent array. for (const result of [...results].reverse()) { const { parent, parentProperty } = result; if (Array.isArray(parent)) { parent.splice(Number(parentProperty), 1); } else { delete parent[String(parentProperty)]; } } } else if (action.update !== undefined) { for (const result of results) { if (typeof result.value === "object" && result.value !== null && !Array.isArray(result.value)) { deepMerge(result.value, action.update); } } } } } /** * Loads and parses an overlay file (YAML or JSON), validates that it looks * like a valid OpenAPI overlay document, and returns the parsed object. * * @param overlayPath - Path or URL to the overlay file. * @throws When the file cannot be read, parsed, or does not contain an * `overlay` version field and an `actions` array. */ export async function loadOverlay(overlayPath) { let content; try { content = await readFile(overlayPath); } catch (error) { const details = error instanceof Error ? error.message : String(error); throw new Error(`Could not read overlay file "${overlayPath}".\n${details}`, { cause: error }); } let parsed; try { parsed = loadYaml(content); } catch (error) { const details = error instanceof Error ? error.message : String(error); throw new Error(`Could not parse overlay file "${overlayPath}".\n${details}`, { cause: error }); } if (typeof parsed !== "object" || parsed === null || !("overlay" in parsed) || !("actions" in parsed) || !Array.isArray(parsed.actions)) { throw new Error(`"${overlayPath}" does not appear to be a valid OpenAPI overlay file. ` + `Expected an object with "overlay" and "actions" fields.`); } return parsed; } /** * Applies all overlays listed in `overlayPaths` to `document` in order. * * Each overlay is loaded from disk (or a URL), parsed, and its actions are * applied sequentially. The document is mutated in place. * * @param document - The OpenAPI document object to mutate. * @param overlayPaths - Ordered list of paths/URLs to overlay files. */ export async function applyOverlays(document, overlayPaths) { for (const overlayPath of overlayPaths) { const overlay = await loadOverlay(overlayPath); applyOverlayActions(document, overlay.actions); } }