counterfact
Version:
Generate a TypeScript-based mock server from an OpenAPI spec in seconds — with stateful routes, hot reload, and REPL support.
76 lines (75 loc) • 3.46 kB
JavaScript
import { pathJoin } from "../util/forward-slash-path.js";
import { Coder } from "./coder.js";
import { OperationTypeCoder, VersionedArgTypeCoder, } from "./operation-type-coder.js";
import { STREAMING_CONTENT_TYPES } from "./streaming-content-types.js";
/**
* Generates the default route handler stub for a single OpenAPI operation.
*
* The generated stub calls `$.response[statusCode].random()` (or `.empty()`)
* for the first response defined in the spec. It is only written when no
* handler file exists yet — users are expected to replace it with real logic.
*
* The corresponding TypeScript type is emitted by {@link OperationTypeCoder}
* into the `types/paths/…` tree.
*/
export class OperationCoder extends Coder {
requestMethod;
securitySchemes;
constructor(requirement, version = "", requestMethod = "", securitySchemes = []) {
super(requirement, version);
if (requestMethod === "") {
throw new Error("requestMethod is required");
}
this.requestMethod = requestMethod;
this.securitySchemes = securitySchemes;
}
names() {
return super.names(this.requestMethod.toUpperCase());
}
write() {
const responses = this.requirement.get("responses");
const [firstStatusCode] = responses.map((_response, statusCode) => statusCode);
const [firstResponse] = responses.map((response) => response.data);
if (firstResponse === undefined ||
!("content" in firstResponse || "schema" in firstResponse)) {
return `async ($) => {
return $.response[${firstStatusCode === "default" ? 200 : firstStatusCode}].empty();
}`;
}
// Detect streaming responses (OpenAPI 3.2 itemSchema)
const content = firstResponse.content;
const hasStreamingContent = content !== undefined &&
Object.keys(content).some((ct) => STREAMING_CONTENT_TYPES.has(ct) &&
content[ct]?.itemSchema !== undefined);
if (hasStreamingContent) {
return `async ($) => {
async function* items() {
// yield items here
}
return $.response[${firstStatusCode === "default" ? 200 : firstStatusCode}].stream(items());
}`;
}
return `async ($) => {
return $.response[${firstStatusCode === "default" ? 200 : firstStatusCode}].random();
}`;
}
typeDeclaration(_namespace, script) {
const operationTypeCoder = new OperationTypeCoder(this.requirement, this.version, this.requestMethod, this.securitySchemes);
if (this.version !== "") {
// For versioned APIs: register this version's $-argument type on the
// shared script so that Script.versionsTypeStatements() can emit the
// merged handler type after all versions have been declared.
const versionedArgCoder = new VersionedArgTypeCoder(this.requirement, this.version, this.requestMethod, this.securitySchemes);
const sharedScript = script.repository.get(operationTypeCoder.modulePath());
sharedScript.declareVersion(versionedArgCoder, operationTypeCoder.getOperationBaseName());
}
return script.importType(operationTypeCoder);
}
modulePath() {
const pathString = this.requirement.url
.split("/")
.at(-2)
.replaceAll("~1", "/");
return `${pathJoin("routes", pathString)}.types.ts`;
}
}