counterfact
Version:
Generate a TypeScript-based mock server from an OpenAPI spec in seconds — with stateful routes, hot reload, and REPL support.
122 lines (121 loc) • 4.8 kB
JavaScript
import { buildJsDoc } from "./jsdoc.js";
import { TypeCoder } from "./type-coder.js";
import { Requirement } from "./requirement.js";
import { pathJoin } from "../util/forward-slash-path.js";
export class SchemaTypeCoder extends TypeCoder {
names() {
return super.names(this.requirement.refUrl?.split("/").at(-1));
}
jsdoc() {
return buildJsDoc(this.requirement.data);
}
additionalPropertiesType(script) {
const { additionalProperties, properties } = this.requirement.data;
if (!additionalProperties.type) {
return "unknown";
}
if (Object.values(properties ?? {}).some((property) => property.type !== additionalProperties.type)) {
return "unknown";
}
const requirement = this.requirement.get("additionalProperties");
return new SchemaTypeCoder(requirement, this.version).write(script);
}
objectSchema(script) {
const { data } = this.requirement;
const typedData = data;
const properties = Object.keys(typedData.properties ?? {}).map((name) => {
const property = this.requirement.get("properties").get(name);
const propertyData = property.data;
const isRequired = typedData.required?.includes(name) || propertyData.required === true;
const optionalFlag = isRequired ? "" : "?";
const comment = buildJsDoc(property.data);
const commentPrefix = comment ? `\n${comment}` : "";
const propertyType = new SchemaTypeCoder(property, this.version).write(script);
return `${commentPrefix}"${name}"${optionalFlag}: ${propertyType}`;
});
if (typedData.additionalProperties) {
properties.push(`[key: string]: ${this.additionalPropertiesType(script)}`);
}
return `{${properties.join(",")}}`;
}
arraySchema(script) {
return `Array<${new SchemaTypeCoder(this.requirement.get("items"), this.version).write(script)}>`;
}
writePrimitive(value) {
if (typeof value === "string") {
return `"${value}"`;
}
if (value === null) {
return "null";
}
return String(value);
}
writeType(script, type) {
if (Array.isArray(type)) {
return type
.map((item) => this.writeType(script, item))
.join(" | ");
}
if (typeof type !== "string") {
return "unknown";
}
if (type === "object") {
return this.objectSchema(script);
}
if (type === "array") {
return this.arraySchema(script);
}
if (type === "integer") {
return "number";
}
return type ?? "unknown";
}
writeGroup(script, { allOf, anyOf, oneOf, }) {
function matchingKey() {
if (allOf) {
return "allOf";
}
if (anyOf) {
return "anyOf";
}
return "oneOf";
}
const key = matchingKey();
const items = (allOf ?? anyOf ?? oneOf);
const types = items.map((_item, index) => new SchemaTypeCoder(this.requirement.get(key).get(index), this.version).write(script));
// Include the default schema from discriminator.defaultMapping (OpenAPI 3.2)
if (!allOf) {
const { discriminator } = this.requirement.data;
if (discriminator?.defaultMapping) {
const defaultRequirement = new Requirement({ $ref: discriminator.defaultMapping }, "", this.requirement.specification);
types.push(new SchemaTypeCoder(defaultRequirement, this.version).write(script));
}
}
return types.join(allOf ? " & " : " | ");
}
writeEnum(_script, requirement) {
return requirement.data
.map((item) => this.writePrimitive(item))
.join(" | ");
}
modulePath() {
return pathJoin("types", this.version, this.requirement.refUrl.replace(/^#\//u, "") + ".ts");
}
writeCode(script) {
const { allOf, anyOf, oneOf, type, format, itemSchema } = this.requirement
.data;
if (itemSchema) {
return `AsyncIterable<${new SchemaTypeCoder(this.requirement.get("itemSchema"), this.version).write(script)}>`;
}
if (allOf ?? anyOf ?? oneOf) {
return this.writeGroup(script, { allOf, anyOf, oneOf });
}
if (this.requirement.has("enum")) {
return this.writeEnum(script, this.requirement.get("enum"));
}
if ((type === "string" && format === "binary") || type === "file") {
return "Uint8Array | string";
}
return this.writeType(script, type);
}
}