@danstackme/apity
Version:
Type-safe API client generator for React applications with file-based routing and runtime validation
585 lines (574 loc) • 19.4 kB
JavaScript
;
var commander = require('commander');
var promises = require('fs/promises');
var swagger2openapi = require('swagger2openapi');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var swagger2openapi__default = /*#__PURE__*/_interopDefault(swagger2openapi);
function parseYaml(yamlString) {
try {
const lines = yamlString.split("\n");
const result = {};
let currentObject = result;
const stack = [result];
let currentIndent = 0;
let currentArray = null;
let arrayIndent = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === "" || line.trim().startsWith("#")) continue;
const indent = line.search(/\S/);
if (indent < 0) continue;
if (line.trim().startsWith("-")) {
const arrayItemMatch = line.trim().match(/^-\s*(.*)$/);
if (!arrayItemMatch) continue;
const value2 = arrayItemMatch[1].trim();
if (arrayIndent === -1 || indent !== arrayIndent) {
let j = i - 1;
while (j >= 0) {
const prevLine = lines[j].trim();
if (prevLine === "" || prevLine.startsWith("#")) {
j--;
continue;
}
const keyMatch = prevLine.match(/^([\w-]+):(?:\s*)?$/);
if (keyMatch) {
const arrayKey = keyMatch[1];
currentObject[arrayKey] = [];
currentArray = currentObject[arrayKey];
arrayIndent = indent;
break;
}
j--;
}
}
if (currentArray) {
if (value2 === "") {
const newObj = {};
currentArray.push(newObj);
stack.push(currentObject);
currentObject = newObj;
currentIndent = indent;
currentArray = null;
} else {
let parsedValue = value2;
if (parsedValue === "true") parsedValue = true;
else if (parsedValue === "false") parsedValue = false;
else if (!isNaN(Number(parsedValue)) && parsedValue !== "")
parsedValue = Number(parsedValue);
else if (parsedValue.startsWith("'") && parsedValue.endsWith("'"))
parsedValue = parsedValue.slice(1, -1);
else if (parsedValue.startsWith('"') && parsedValue.endsWith('"'))
parsedValue = parsedValue.slice(1, -1);
currentArray.push(parsedValue);
}
}
continue;
} else {
if (indent <= arrayIndent) {
arrayIndent = -1;
currentArray = null;
}
}
const match = line.trim().match(/^([\w-]+):(?:\s(.+))?$/);
if (!match) continue;
const [_, key, value] = match;
if (value === void 0 || value.trim() === "") {
currentObject[key] = {};
if (indent > currentIndent) {
stack.push(currentObject);
currentObject = currentObject[key];
currentIndent = indent;
} else if (indent < currentIndent) {
while (stack.length > 1 && indent <= currentIndent) {
stack.pop();
currentObject = stack[stack.length - 1];
currentIndent -= 2;
}
currentObject[key] = {};
stack.push(currentObject);
currentObject = currentObject[key];
} else {
currentObject = stack[stack.length - 1];
currentObject[key] = {};
currentObject = currentObject[key];
}
} else {
let parsedValue = value.trim();
if (parsedValue === "true") parsedValue = true;
else if (parsedValue === "false") parsedValue = false;
else if (!isNaN(Number(parsedValue)) && parsedValue !== "")
parsedValue = Number(parsedValue);
else if (parsedValue.startsWith("'") && parsedValue.endsWith("'"))
parsedValue = parsedValue.slice(1, -1);
else if (parsedValue.startsWith('"') && parsedValue.endsWith('"'))
parsedValue = parsedValue.slice(1, -1);
currentObject[key] = parsedValue;
}
}
return result;
} catch (error) {
console.error("Error parsing YAML:", error);
throw error;
}
}
async function convertToOpenAPI3(doc) {
if (doc.openapi && doc.openapi.startsWith("3.")) {
return doc;
}
const result = await swagger2openapi__default.default.convert(doc, {});
return result.openapi;
}
function isReferenceObject(obj) {
return obj !== null && obj !== void 0 && "$ref" in obj;
}
function getRefName(ref) {
if (!ref) return "";
const parts = ref.split("/");
return parts[parts.length - 1];
}
function resolveRef(ref, spec) {
if (!ref.startsWith("#/")) return void 0;
const parts = ref.substring(2).split("/");
let current = spec;
for (const part of parts) {
if (!current[part]) return void 0;
current = current[part];
}
return current;
}
function processSchemaDefinitions(spec) {
const schemas = /* @__PURE__ */ new Map();
if (spec.components?.schemas) {
for (const [name, schema] of Object.entries(spec.components.schemas)) {
if (!isReferenceObject(schema)) {
schemas.set(name, schema);
}
}
}
return schemas;
}
async function generateRoutes(spec, options) {
const routes = /* @__PURE__ */ new Map();
const schemas = processSchemaDefinitions(spec);
for (const [path, pathItem] of Object.entries(spec.paths || {})) {
if (!pathItem) continue;
const routePath = path.replace(/{([^}]+)}/g, "[$1]");
const methods = {};
for (const [method, operation] of Object.entries(pathItem)) {
if (method === "parameters" || !operation || typeof operation === "string" || Array.isArray(operation))
continue;
const upperMethod = method.toUpperCase();
const response = operation.responses?.[200];
const requestBody = operation.requestBody;
methods[upperMethod] = {
method: upperMethod,
response: processResponseSchema(response, spec),
body: processRequestBodySchema(requestBody, spec),
query: {},
params: {}
};
const queryParams = operation.parameters?.filter((p) => {
if (!p || typeof p === "string") return false;
return "in" in p && p.in === "query";
}) || [];
if (queryParams.length > 0) {
methods[upperMethod].query = {
type: "object",
properties: Object.fromEntries(
queryParams.map((p) => {
if (typeof p === "string" || isReferenceObject(p))
return ["", {}];
return ["name" in p ? p.name : "", "schema" in p ? p.schema : {}];
})
)
};
}
const pathParams = operation.parameters?.filter((p) => {
if (!p || typeof p === "string") return false;
return "in" in p && p.in === "path";
}) || [];
if (pathParams.length > 0) {
methods[upperMethod].params = {
type: "object",
properties: Object.fromEntries(
pathParams.map((p) => [
"name" in p ? p.name : "",
"schema" in p ? p.schema : {}
])
)
};
}
}
routes.set(routePath, methods);
}
await generateSingleFile(
routes,
schemas,
options.outDir || "src",
spec.servers?.[0]?.url || ""
);
}
function processResponseSchema(response, spec) {
if (!response) return {};
if (isReferenceObject(response)) {
const resolvedSchema = resolveRef(response.$ref, spec);
return resolvedSchema || {};
}
const contentSchema = response.content?.["application/json"]?.schema;
if (!contentSchema) return {};
if (isReferenceObject(contentSchema)) {
const resolvedSchema = resolveRef(contentSchema.$ref, spec);
return resolvedSchema || {};
}
if (contentSchema.allOf) {
return processAllOf(contentSchema, spec);
}
if (contentSchema.oneOf) {
return contentSchema;
}
return contentSchema;
}
function processRequestBodySchema(requestBody, spec) {
if (!requestBody) return {};
if (isReferenceObject(requestBody)) {
const resolvedSchema = resolveRef(requestBody.$ref, spec);
return resolvedSchema || {};
}
const contentSchema = requestBody.content?.["application/json"]?.schema;
if (!contentSchema) return {};
if (isReferenceObject(contentSchema)) {
const resolvedSchema = resolveRef(contentSchema.$ref, spec);
return resolvedSchema || {};
}
if (contentSchema.allOf) {
return processAllOf(contentSchema, spec);
}
if (contentSchema.oneOf) {
return contentSchema;
}
return contentSchema;
}
function processAllOf(schema, spec) {
if (!schema.allOf || !Array.isArray(schema.allOf) || schema.allOf.length === 0) {
return schema;
}
const { allOf, ...baseSchema } = schema;
const mergedSchema = { ...baseSchema };
for (const subSchema of allOf) {
let resolvedSubSchema;
if (isReferenceObject(subSchema)) {
const resolved = resolveRef(subSchema.$ref, spec);
if (!resolved) continue;
resolvedSubSchema = resolved;
} else {
resolvedSubSchema = subSchema;
}
if (resolvedSubSchema.allOf) {
resolvedSubSchema = processAllOf(resolvedSubSchema, spec);
}
if (resolvedSubSchema.properties) {
mergedSchema.properties = {
...mergedSchema.properties,
...resolvedSubSchema.properties
};
}
if (resolvedSubSchema.required && resolvedSubSchema.required.length > 0) {
mergedSchema.required = [
...mergedSchema.required || [],
...resolvedSubSchema.required
];
}
if (resolvedSubSchema.type && !mergedSchema.type) {
mergedSchema.type = resolvedSubSchema.type;
}
if (resolvedSubSchema.format && !mergedSchema.format) {
mergedSchema.format = resolvedSubSchema.format;
}
if (resolvedSubSchema.description && !mergedSchema.description) {
mergedSchema.description = resolvedSubSchema.description;
}
}
return mergedSchema;
}
function convertToZodSchema(schema, _isRequired = false, schemas = /* @__PURE__ */ new Map()) {
if (!schema) return "z.void()";
if (isReferenceObject(schema)) {
const refName = getRefName(schema.$ref);
if (schemas.has(refName)) {
return `z.lazy(() => ${refName}Schema)`;
}
return "z.unknown()";
}
if (schema.allOf) {
const allOfSchemas = schema.allOf.map(
(subSchema) => convertToZodSchema(subSchema, _isRequired, schemas)
);
if (allOfSchemas.length === 1) {
return allOfSchemas[0];
}
let result = `z.intersection(${allOfSchemas[0]}, ${allOfSchemas[1]})`;
for (let i = 2; i < allOfSchemas.length; i++) {
result = `z.intersection(${result}, ${allOfSchemas[i]})`;
}
return result;
}
if (schema.oneOf && schema.oneOf.length > 0) {
const oneOfSchemas = schema.oneOf.map(
(subSchema) => convertToZodSchema(subSchema, _isRequired, schemas)
);
if (oneOfSchemas.length === 1) {
return oneOfSchemas[0];
}
return oneOfSchemas.reduce((acc, current, index) => {
if (index === 0) return current;
return `${acc}.or(${current})`;
}, "");
}
let numberType;
let itemType;
let properties;
let zodSchema;
if (Array.isArray(schema.type)) {
if (schema.type.includes("null")) {
const nonNullTypes = schema.type.filter((t) => t !== "null");
if (nonNullTypes.length === 1) {
const nonNullSchema = { ...schema, type: nonNullTypes[0] };
return `${convertToZodSchema(nonNullSchema, _isRequired, schemas)}.nullable()`;
}
}
schema.type = "string";
}
switch (schema.type) {
case "string":
if (schema.enum) {
zodSchema = `z.enum([${schema.enum.map((e) => `'${e}'`).join(", ")}])`;
} else if (schema.format === "date-time") {
zodSchema = "z.string().datetime()";
} else if (schema.format === "email") {
zodSchema = "z.string().email()";
} else {
zodSchema = "z.string()";
if (schema.minLength !== void 0) {
zodSchema += `.min(${schema.minLength})`;
}
if (schema.maxLength !== void 0) {
zodSchema += `.max(${schema.maxLength})`;
}
}
break;
case "number":
case "integer":
numberType = "z.number()";
if (schema.minimum !== void 0) {
numberType += `.min(${schema.minimum})`;
}
if (schema.maximum !== void 0) {
numberType += `.max(${schema.maximum})`;
}
zodSchema = numberType;
break;
case "boolean":
zodSchema = "z.boolean()";
break;
case "array":
if (schema.items) {
itemType = convertToZodSchema(schema.items, true, schemas);
zodSchema = `z.array(${itemType})`;
} else {
zodSchema = "z.array(z.unknown())";
}
break;
case "object":
if (!schema.properties) return "z.object({})";
properties = Object.entries(schema.properties).map(([key, prop]) => {
const propIsRequired = schema.required?.includes(key);
const propType = convertToZodSchema(prop, propIsRequired, schemas);
return `${key}: ${propIsRequired ? propType : `${propType}.optional()`}`;
});
zodSchema = `z.object({
${properties.join(",\n ")}
})`;
break;
default:
zodSchema = "z.unknown()";
}
if (schema.nullable) {
zodSchema += ".nullable()";
}
if (schema.description) {
const escapedDescription = schema.description.replace(/[`'\\]/g, "\\$&");
zodSchema += `.describe(\`${escapedDescription}\`)`;
}
return zodSchema;
}
async function generateSingleFile(routes, schemas, outDir, baseUrl) {
await promises.mkdir(outDir, { recursive: true });
const getRoutes = /* @__PURE__ */ new Map();
const otherRoutes = /* @__PURE__ */ new Map();
for (const [path, methods] of routes) {
for (const [method, schema] of Object.entries(methods)) {
if (method === "GET") {
if (!getRoutes.has(path)) {
getRoutes.set(path, /* @__PURE__ */ new Map());
}
getRoutes.get(path)?.set(method, schema);
} else {
if (!otherRoutes.has(path)) {
otherRoutes.set(path, /* @__PURE__ */ new Map());
}
otherRoutes.get(path)?.set(method, schema);
}
}
}
let content = `import { createApi, createApiEndpoint } from '@danstackme/apity';
`;
content += `import { z } from 'zod';
`;
if (schemas.size > 0) {
content += `// Schema definitions
`;
for (const [name, schema] of schemas) {
content += `export const ${name}Schema = ${convertToZodSchema(schema, true, schemas)};
`;
}
}
const fetchEndpointNames = /* @__PURE__ */ new Map();
for (const [path, methods] of getRoutes) {
const endpointNames = [];
for (const [method, schema] of methods) {
const pathParams = Object.keys(schema.params.properties || {});
const endpointName = `${method}_${path.replace(/\//g, "").replace(/\[.*?\]/g, "")}${pathParams.length > 0 ? `_${pathParams.join("_")}` : ""}`;
endpointNames.push(endpointName);
content += `const ${endpointName} = createApiEndpoint({
`;
content += ` method: '${method}',
`;
if (Object.keys(schema.response).length > 0) {
if (isReferenceObject(schema.response)) {
const refName = getRefName(schema.response.$ref);
content += ` response: ${refName}Schema,
`;
} else {
content += ` response: ${convertToZodSchema(schema.response, true, schemas)},
`;
}
} else {
content += ` response: z.void(),
`;
}
if (Object.keys(schema.query).length > 0) {
content += ` query: ${convertToZodSchema(schema.query, false, schemas)},
`;
}
content += `});
`;
}
if (endpointNames.length > 0) {
fetchEndpointNames.set(path, endpointNames);
}
}
const mutateEndpointNames = /* @__PURE__ */ new Map();
for (const [path, methods] of otherRoutes) {
const endpointNames = [];
for (const [method, schema] of methods) {
const pathParams = Object.keys(schema.params.properties || {});
const endpointName = `${method}_${path.replace(/\//g, "").replace(/\[.*?\]/g, "")}${pathParams.length > 0 ? `_${pathParams.join("_")}` : ""}`;
endpointNames.push(endpointName);
content += `const ${endpointName} = createApiEndpoint({
`;
content += ` method: '${method}',
`;
if (Object.keys(schema.response).length > 0) {
if (isReferenceObject(schema.response)) {
const refName = getRefName(schema.response.$ref);
content += ` response: ${refName}Schema,
`;
} else {
content += ` response: ${convertToZodSchema(schema.response, true, schemas)},
`;
}
} else {
content += ` response: z.void(),
`;
}
if (Object.keys(schema.body).length > 0) {
if (isReferenceObject(schema.body)) {
const refName = getRefName(schema.body.$ref);
content += ` body: ${refName}Schema,
`;
} else {
content += ` body: ${convertToZodSchema(schema.body, false, schemas)},
`;
}
}
if (Object.keys(schema.query).length > 0) {
content += ` query: ${convertToZodSchema(schema.query, false, schemas)},
`;
}
content += `});
`;
}
if (endpointNames.length > 0) {
mutateEndpointNames.set(path, endpointNames);
}
}
content += `export const fetchEndpoints = {
`;
for (const [path, endpointNames] of fetchEndpointNames) {
content += ` '${path}': [${endpointNames.join(", ")}],
`;
}
content += `} as const;
`;
content += `export const mutateEndpoints = {
`;
for (const [path, endpointNames] of mutateEndpointNames) {
content += ` '${path}': [${endpointNames.join(", ")}],
`;
}
content += `} as const;
`;
content += `export const api = createApi({
`;
content += ` baseUrl: '${baseUrl}',
`;
content += ` fetchEndpoints,
`;
content += ` mutateEndpoints,
`;
content += `});
`;
await promises.writeFile(`${outDir}/endpoints.ts`, content);
}
async function main() {
const program = new commander.Command();
program.name("import-openapi").description("Import OpenAPI/Swagger specification and generate API routes").argument("<file>", "OpenAPI/Swagger specification file (JSON or YAML)").option("-d, --outDir <directory>", "Output directory", void 0);
program.parse();
const options = program.opts();
const [file] = program.args;
try {
const content = await promises.readFile(file, "utf-8");
const doc = file.endsWith(".yaml") || file.endsWith(".yml") ? parseYaml(content) : JSON.parse(content);
const openapi = await convertToOpenAPI3(doc);
await generateRoutes(openapi, {
outDir: options.outDir
});
console.log("Successfully generated API routes!");
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
if (process.env.NODE_ENV !== "test") {
main();
}
exports.convertToOpenAPI3 = convertToOpenAPI3;
exports.generateRoutes = generateRoutes;
exports.generateSingleFile = generateSingleFile;
exports.getRefName = getRefName;
exports.isReferenceObject = isReferenceObject;
exports.processSchemaDefinitions = processSchemaDefinitions;
exports.resolveRef = resolveRef;
//# sourceMappingURL=import-openapi.cjs.map
//# sourceMappingURL=import-openapi.cjs.map