@moccona/apicodegen
Version:
`@moccona/apicodegen` is a command-line tool for generating TypeScript code from OpenAPI documentation. It provides a simple and efficient way to automate the process of creating API clients.
1,556 lines (1,546 loc) • 72.7 kB
JavaScript
// src/core/base/Adaptor.ts
var Adapter = class {
};
// src/core/constants/keywords.ts
var typescriptKeywords = /* @__PURE__ */ new Set([
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"enum",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"import",
"in",
"instanceof",
"new",
"null",
"return",
"super",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"as",
"implements",
"interface",
"let",
"package",
"private",
"protected",
"public",
"static",
"yield",
"abstract",
"any",
"async",
"await",
"constructor",
"declare",
"from",
"get",
"is",
"module",
"namespace",
"never",
"require",
"set",
"type",
"unknown",
"readonly",
"of",
"asserts",
"infer",
"keyof",
"boolean",
"number",
"string",
"symbol",
"object",
"undefined",
"bigint"
]);
// src/core/interface.ts
var SchemaType = /* @__PURE__ */ ((SchemaType2) => {
SchemaType2["schemas"] = "schemas";
SchemaType2["parameters"] = "parameters";
SchemaType2["responses"] = "responses";
SchemaType2["requestBodies"] = "requestBodies";
return SchemaType2;
})(SchemaType || {});
var NonArraySchemaType = /* @__PURE__ */ ((NonArraySchemaType2) => {
NonArraySchemaType2["object"] = "object";
NonArraySchemaType2["string"] = "string";
NonArraySchemaType2["number"] = "number";
NonArraySchemaType2["boolean"] = "boolean";
NonArraySchemaType2["integer"] = "integer";
NonArraySchemaType2["enum"] = "enum";
NonArraySchemaType2["file"] = "file";
return NonArraySchemaType2;
})(NonArraySchemaType || {});
var ArraySchemaType = /* @__PURE__ */ ((ArraySchemaType2) => {
ArraySchemaType2["array"] = "array";
return ArraySchemaType2;
})(ArraySchemaType || {});
var SchemaFormatType = /* @__PURE__ */ ((SchemaFormatType3) => {
SchemaFormatType3["string"] = "string";
SchemaFormatType3["number"] = "number";
SchemaFormatType3["boolean"] = "boolean";
SchemaFormatType3["file"] = "file";
SchemaFormatType3["binary"] = "binary";
SchemaFormatType3["blob"] = "blob";
return SchemaFormatType3;
})(SchemaFormatType || {});
var ParameterIn = /* @__PURE__ */ ((ParameterIn3) => {
ParameterIn3["header"] = "header";
ParameterIn3["body"] = "body";
ParameterIn3["query"] = "query";
ParameterIn3["cookie"] = "cookie";
ParameterIn3["path"] = "path";
ParameterIn3["formData"] = "formData";
return ParameterIn3;
})(ParameterIn || {});
var MediaTypes = /* @__PURE__ */ ((MediaTypes2) => {
MediaTypes2["JSON"] = "application/json";
MediaTypes2["TEXT"] = "text";
MediaTypes2["IMAGE"] = "image";
MediaTypes2["AUDIO"] = "audio";
MediaTypes2["VIDEO"] = "video";
return MediaTypes2;
})(MediaTypes || {});
var HttpMethods = /* @__PURE__ */ ((HttpMethods2) => {
HttpMethods2["GET"] = "get";
HttpMethods2["PUT"] = "put";
HttpMethods2["POST"] = "post";
HttpMethods2["DELETE"] = "delete";
HttpMethods2["OPTIONS"] = "options";
HttpMethods2["HEAD"] = "head";
HttpMethods2["PATCH"] = "patch";
HttpMethods2["TRACE"] = "trace";
return HttpMethods2;
})(HttpMethods || {});
var Adaptors = /* @__PURE__ */ ((Adaptors2) => {
Adaptors2["fetch"] = "fetch";
Adaptors2["axios"] = "axios";
return Adaptors2;
})(Adaptors || {});
// src/core/base/Base.ts
import { Agent, request } from "undici";
var SuccessHttpStatusCode = {
"200": "200",
// OK
"201": "201",
// Created
"202": "202",
// Accepted
"203": "203",
// Non-Authoritative Information
"204": "204",
// No Content
"205": "205",
// Reset Content
"206": "206",
// Partial Content
"207": "207",
// Multi_Status
"208": "208",
// Already_Reported
"226": "226"
// IM Used
};
var Base = class _Base {
constructor() {
if (new.target === _Base) {
throw new Error("Cannot instantiate abstract class");
}
}
/**
* Converts a reference string to a meaningful name.
* @param ref - The reference string to process.
* @param [doc] - Optional document reference for context.
* @returns - The processed name.
*/
static ref2name(ref, doc) {
const paths = ref.replace(/^#/, "").split("/").filter(Boolean);
if (!doc) {
return paths.slice(-1)[0];
}
let temporary = doc;
let lastPath = "";
for (const path of paths) {
const adjustedPath = path.replaceAll("~1", "/");
temporary = temporary[adjustedPath];
lastPath = adjustedPath;
}
if (!temporary) {
return "unknown";
}
return temporary.$ref ? this.ref2name(temporary.$ref, doc) : lastPath;
}
/**
* Converts an API path to a function name.
* @param path - The API endpoint path.
* @param [method] - The HTTP method (e.g., GET, POST).
* @param [operationId] - Unique identifier for the operation.
* @returns - The generated function name.
*/
static pathToFnName(path, method, _operationId = "") {
const name = this.normalize(this.camelCase(this.normalize(path)));
const suffix = method ? this.capitalize(this.upperCamelCase(`using_${method}`)) : "";
return name + suffix;
}
/**
* Normalizes a string by replacing special characters and avoiding TypeScript keywords.
* @param text - Input text to normalize.
* @returns - The normalized string.
*/
static normalize(text) {
if (typescriptKeywords.has(text)) {
text += "_";
}
return text.replace(/[/\-_{}():\s`,*<>$#.]/gm, "_").replace(/^\d./gm, "").replaceAll("...", "");
}
/**
* Capitalizes the first character of a string.
* @param text - Input string.
* @returns - Capitalized string.
*/
static capitalize(text) {
text = text.trim();
return `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
}
/**
* Converts a string to camelCase.
* @param text - Input string.
* @returns - CamelCase string.
*/
static camelCase(text) {
text = text.trim();
return text.split("_").filter(Boolean).map((t4, index) => index === 0 ? t4 : this.capitalize(t4)).join("");
}
/**
* Converts a string to UpperCamelCase.
* @param text - Input string.
* @returns - UpperCamelCase string.
*/
static upperCamelCase(text) {
return this.normalize(text).replaceAll("...", "").split("_").filter(Boolean).map(this.capitalize).join("");
}
/**
* Fetches documentation from a given URL.
* @param url - The URL to fetch the documentation from.
* @param requestInit - Additional request parameters.
* @returns - A promise resolving to the fetched documentation data.
*/
static async fetchDoc(url, requestInit = {}) {
const agent = new Agent({
connect: {
rejectUnauthorized: false
}
});
try {
const { body } = await request(url, {
method: "GET",
dispatcher: agent,
...requestInit
});
return body.json();
} catch (error) {
throw error;
}
}
/**
* Determines the media type from a given media type string.
* @param mediaType - The media type string to evaluate.
* @returns - The matched MediaTypes or null.
*/
static getMediaType(mediaType) {
for (const type in Object.values(MediaTypes)) {
if (new RegExp(type).test(mediaType)) {
return type;
}
}
return;
}
/**
* Checks if a schema is a valid enum type that isn't boolean.
* @param a - The schema object to evaluate.
* @returns - True if the schema is a valid non-boolean enum.
*/
static isValidEnumType(a) {
return a.type !== "boolean" && !this.isBooleanEnum(a);
}
/**
* Checks if a schema represents a boolean enum.
* @param a - The schema object to evaluate.
* @returns - True if the schema is a boolean enum.
*/
static isBooleanEnum(a) {
return a.type === "boolean" || !!a.enum?.some(
(member) => typeof member === "boolean"
);
}
/**
* Checks if two enum schemas are identical.
* @param a - First enum schema to compare.
* @param b - Second enum schema to compare.
* @returns - True if the enums are identical.
*/
static isSameEnum(a, b) {
return a.enum.length === b.enum.length && a.enum.sort().every((v, index) => v === b.enum.sort()[index]);
}
/**
* Filters out duplicate enum schemas from an array.
* @param enums - Array of enum schemas to process.
* @returns - Array of unique enum schemas.
*/
static uniqueEnums(enums) {
const uniqueEnums_ = [];
for (const enumObject of enums) {
if (uniqueEnums_.length === 0) {
uniqueEnums_.push(enumObject);
} else {
if (!uniqueEnums_.some((a) => this.isSameEnum(a, enumObject))) {
uniqueEnums_.push(enumObject);
}
}
}
return uniqueEnums_;
}
/**
* Finds the first occurrence of a matching enum schema in an array.
* @param a - The enum schema to find.
* @param enums - Array of enum schemas to search.
* @returns - The found schema or undefined.
*/
static findSameSchema(a, enums) {
return enums.find((b) => this.isSameEnum(b, a));
}
/**
* Checks if an object is a reference object.
* @param schema - The object to check.
* @returns - True if the object is a reference.
*/
static isRef(schema) {
return "$ref" in schema && typeof schema.$ref === "string";
}
};
// src/core/base/Provider.ts
var Provider = class {
/** collection of enum schemas */
enums = [];
/** collection of schemas indexed by name */
schemas = {};
/** collection of parameters indexed by name */
parameters = {};
/** collection of API responses indexed by name */
responses = {};
/** collection of request bodies indexed by name */
requestBodies = {};
/** collection of API endpoints (operations) indexed by path */
apis = {};
/** URL for fetching API documentation */
docURL;
/** base URL for API endpoints */
baseURL;
/** output directory for generated code */
output;
/** request options for API documentation fetch */
requestOptions;
/** source path for imported client */
importClientSource;
/**
* Provider Constructor.
* @param {ProviderInitOptions} initOptions - Initial configuration for the provider.
* @param {unknown} doc - Raw API documentation data to be parsed.
*/
constructor(initOptions, doc) {
this.docURL = initOptions.docURL;
this.baseURL = initOptions.baseURL ?? "";
this.output = initOptions.output ?? ".";
this.requestOptions = initOptions.requestOptions ?? {};
this.importClientSource = initOptions.importClientSource ?? "";
const { enums, schemas, requestBodies, responses, parameters, apis } = this.parse(doc);
this.enums = enums;
this.schemas = schemas;
this.responses = responses;
this.parameters = parameters;
this.requestBodies = requestBodies;
this.apis = apis;
}
};
// src/core/generator/index.ts
import { writeFile } from "fs/promises";
import { format } from "prettier";
import {
addSyntheticLeadingComment,
createPrinter,
factory as t,
NodeFlags,
SyntaxKind
} from "typescript";
var Generator = class _Generator {
/**
* Converts an array of TypeScript statements into a formatted string of code.
*
* @param statements - The array of TypeScript statement nodes.
* @returns Formatted code as a string.
* @throws {Error} If no valid statements are provided.
*/
static toCode(statements) {
if (statements.length === 0) {
return "// No api declaration found.";
}
const sourceFile = t.createSourceFile(
statements,
t.createToken(SyntaxKind.EndOfFileToken),
NodeFlags.None
);
return createPrinter().printFile(sourceFile);
}
static async write(code, filepath) {
try {
await writeFile(filepath, code);
} catch (error) {
console.error(error);
}
}
/**
* Converts a path string with parameters into a TypeScript template expression.
* Handles query parameters and path placeholders.
*
* @param path - The base path string containing placeholders.
* @param parameters - Array of parameter objects defining the parameters.
* @param basePath - Optional base path to prepend (default: "").
* @returns A TypeScript template expressi
*/
static toUrlTemplate(path, parameters, basePath = "") {
const queryParameters = parameters.filter(
(p) => p.in === "query" /* query */
);
if (queryParameters.length > 0) {
const queryString = queryParameters.map(
(qp, index) => `${index === 0 ? "?" : "&"}${encodeURIComponent(qp.name)}={${Base.camelCase(Base.normalize(qp.name))}}`
).join("");
path += queryString;
}
const pathSegments = path.replaceAll("{", "${").split("$").filter(Boolean);
if (pathSegments.length === 1) {
return t.createNoSubstitutionTemplateLiteral(basePath + path);
}
return t.createTemplateExpression(
t.createTemplateHead(basePath + pathSegments[0]),
pathSegments.slice(1).map((segment, index) => {
const match = /^{(.+)}(.+)?/gm.exec(segment);
const isLastSegment = index === pathSegments.length - 2;
if (!match) {
throw new Error(`Invalid path segment: ${segment}`);
}
return t.createTemplateSpan(
t.createIdentifier(match[1]),
!isLastSegment ? t.createTemplateMiddle(match[2]) : t.createTemplateTail(match[2] || "")
);
})
);
}
/**
* Adds synthetic comments to a TypeScript AST node.
*
* @param node - The target AST node.
* @param comments - Array of comment objects to add.
*/
static addComments(node, comments) {
if (!Array.isArray(comments) || comments.filter(Boolean).length === 0)
return;
const formatComment = (comment) => {
return comment.tag ? ` @${comment.tag} ${comment.comment ?? ""}` : ` ${comment.comment}`;
};
const formattedComments = "*\n" + comments.map(formatComment).join("\n").trim() + "\n";
addSyntheticLeadingComment(
node,
SyntaxKind.MultiLineCommentTrivia,
formattedComments,
true
);
}
/**
* Checks if a schema represents a binary type.
*
* @param schema - The schema object to check.
* @returns true if the schema is a binary type, false otherwise.
*/
static isBinarySchema(schema) {
if (schema.type === "array") {
const arraySchema = schema;
return this.isBinarySchema(arraySchema.items);
}
const nonArraySchema = schema;
return nonArraySchema.format === "blob" /* blob */ || nonArraySchema.format === "binary" /* binary */ || nonArraySchema.type === "file" /* file */;
}
static toRequestBodyTypeNode(schema) {
return t.createParameterDeclaration(
void 0,
void 0,
t.createIdentifier("req"),
void 0,
this.toTypeNode(schema)
);
}
static toTypeNode(schema) {
const { type, ref } = schema;
if (ref) {
const identify = Base.ref2name(ref);
return t.createTypeReferenceNode(
t.createIdentifier(
identify === "unknown" ? identify : Base.upperCamelCase(identify)
)
);
}
switch (type) {
case "array" /* array */:
const { items } = schema;
return t.createArrayTypeNode(this.toTypeNode(items));
case "object" /* object */:
const propsCount = Object.keys(schema.properties ?? {}).length;
if (!schema.properties || propsCount === 0) {
return t.createTypeReferenceNode(t.createIdentifier("Record"), [
t.createToken(SyntaxKind.StringKeyword),
t.createToken(SyntaxKind.UnknownKeyword)
]);
}
const props = Object.keys(schema.properties);
return t.createTypeLiteralNode(
props.map((propKey) => {
const propSchema = schema.properties[propKey];
return t.createPropertySignature(
void 0,
t.createStringLiteral(propKey),
// When field is required, a refrence or binary value, don't add question mark.
schema.required || schema.ref || this.isBinarySchema(schema) ? void 0 : t.createToken(SyntaxKind.QuestionToken),
this.toTypeNode(propSchema)
);
})
);
case "integer" /* integer */:
case "number" /* number */:
if (schema.enum) {
return t.createUnionTypeNode(
schema.enum.map(
(e) => t.createLiteralTypeNode(t.createNumericLiteral(e))
)
);
}
return t.createToken(SyntaxKind.NumberKeyword);
// case NonArraySchemaType.string:
case "boolean" /* boolean */:
return t.createToken(SyntaxKind.BooleanKeyword);
case "file" /* file */:
return t.createTypeReferenceNode(t.createIdentifier("Blob"));
default:
const {
format: format2,
oneOf,
allOf,
anyOf,
type: type2,
enum: enum_
} = schema;
switch (format2) {
case "number" /* number */:
return t.createToken(SyntaxKind.NumberKeyword);
case "string" /* string */:
return t.createToken(SyntaxKind.StringKeyword);
case "boolean" /* boolean */:
return t.createToken(SyntaxKind.BooleanKeyword);
case "blob" /* blob */:
case "binary" /* binary */:
return t.createTypeReferenceNode(t.createIdentifier("Blob"));
default:
}
if (enum_) {
return t.createUnionTypeNode(
enum_.map(
(e) => t.createLiteralTypeNode(t.createStringLiteral(e))
)
);
}
if (type2 === "string" /* string */) {
return t.createToken(SyntaxKind.StringKeyword);
}
if (oneOf) {
return t.createUnionTypeNode(
oneOf.map((schema2) => this.toTypeNode(schema2))
);
}
if (anyOf) {
return t.createUnionTypeNode(
anyOf.map((schema2) => this.toTypeNode(schema2))
);
}
if (allOf) {
return t.createIntersectionTypeNode(
allOf.map((schema2) => this.toTypeNode(schema2))
);
}
if (type2 && typeof type2 === "string") {
return t.createTypeReferenceNode(
type2 !== "unknown" && type2 !== "null" ? t.createIdentifier(Base.upperCamelCase(type2)) : type2
);
}
}
return t.createToken(SyntaxKind.UnknownKeyword);
}
static toDeclarationNode(parameters) {
const objectElements = [];
const typeObjectElements = [];
for (const parameter of parameters) {
if (parameter.ref) {
t.createParameterDeclaration(
void 0,
void 0,
t.createIdentifier(
Base.camelCase(Base.normalize(Base.ref2name(parameter.ref)))
),
void 0,
t.createTypeReferenceNode(
t.createIdentifier(
Base.upperCamelCase(Base.normalize(Base.ref2name(parameter.ref)))
)
),
void 0
);
} else {
const { name, schema, required } = parameter;
objectElements.push(
t.createBindingElement(
void 0,
void 0,
t.createIdentifier(Base.camelCase(Base.normalize(name)))
)
);
typeObjectElements.push(
t.createPropertySignature(
[],
t.createIdentifier(Base.camelCase(Base.normalize(name))),
required ? void 0 : t.createToken(SyntaxKind.QuestionToken),
!schema ? t.createToken(SyntaxKind.UnknownKeyword) : this.toTypeNode(schema)
)
);
}
}
return t.createParameterDeclaration(
void 0,
void 0,
t.createObjectBindingPattern(objectElements),
void 0,
t.createTypeLiteralNode(typeObjectElements),
void 0
);
}
static toFormDataStatement(parameters, requestBody) {
const statements = [];
const fdDeclaration = t.createVariableStatement(
void 0,
t.createVariableDeclarationList(
[
t.createVariableDeclaration(
t.createIdentifier("fd"),
void 0,
void 0,
t.createNewExpression(
t.createIdentifier("FormData"),
void 0,
[]
)
)
],
NodeFlags.Const
)
);
statements.push(fdDeclaration);
parameters.forEach((parameter) => {
statements.push(
t.createExpressionStatement(
t.createBinaryExpression(
t.createIdentifier(parameter.name),
t.createToken(SyntaxKind.AmpersandAmpersandToken),
t.createCallExpression(
t.createPropertyAccessExpression(
t.createIdentifier("fd"),
t.createIdentifier("append")
),
void 0,
[
t.createStringLiteral(parameter.name),
t.createIdentifier(parameter.name)
]
)
)
)
);
});
if (requestBody && requestBody.type === "object" && requestBody.properties && Object.keys(requestBody.properties).length !== 0) {
Object.keys(requestBody.properties).forEach((key) => {
const schemaByKey = requestBody.properties[key];
if (schemaByKey.type === "array" /* array */ && this.isBinarySchema(schemaByKey)) {
statements.push(
t.createForOfStatement(
void 0,
t.createVariableDeclarationList(
[t.createVariableDeclaration("file")],
NodeFlags.Const
),
t.createElementAccessExpression(
t.createIdentifier("req"),
t.createStringLiteral(key)
),
t.createBlock([
t.createExpressionStatement(
t.createCallExpression(
t.createPropertyAccessExpression(
t.createIdentifier("fd"),
t.createIdentifier("append")
),
[],
[
t.createStringLiteral(key),
t.createIdentifier("file"),
t.createPropertyAccessExpression(
t.createAsExpression(
t.createIdentifier("file"),
t.createTypeReferenceNode(
t.createIdentifier("File"),
void 0
)
),
t.createIdentifier("name")
)
]
)
)
])
)
);
} else {
if (schemaByKey.required) {
statements.push(
t.createExpressionStatement(
t.createCallExpression(
t.createPropertyAccessExpression(
t.createIdentifier("fd"),
t.createIdentifier("append")
),
void 0,
[
t.createStringLiteral(key),
schemaByKey.type === "string" ? t.createElementAccessExpression(
t.createIdentifier("req"),
t.createStringLiteral(key)
) : t.createCallExpression(
t.createIdentifier("String"),
void 0,
[
t.createElementAccessExpression(
t.createIdentifier("req"),
t.createStringLiteral(key)
)
]
)
]
)
)
);
} else {
statements.push(
t.createExpressionStatement(
t.createBinaryExpression(
t.createElementAccessExpression(
t.createIdentifier("req"),
t.createStringLiteral(key)
),
t.createToken(SyntaxKind.AmpersandAmpersandToken),
t.createCallExpression(
t.createPropertyAccessExpression(
t.createIdentifier("fd"),
t.createIdentifier("append")
),
void 0,
[
t.createStringLiteral(key),
schemaByKey.type === "string" ? t.createElementAccessExpression(
t.createIdentifier("req"),
t.createStringLiteral(key)
) : t.createCallExpression(
t.createIdentifier("String"),
void 0,
[
t.createElementAccessExpression(
t.createIdentifier("req"),
t.createStringLiteral(key)
)
]
)
]
)
)
)
);
}
}
});
}
return statements;
}
static bodyBlock(uri, method, parameters, requestBody, response, adapter) {
const isFormDataRequest = requestBody && ["multipart/form-data", "application/x-www-form-urlencoded"].includes(
requestBody.type
);
const shouldParseResponseToJSON = "application/json" === response?.type;
const isRequestBodyBinary = requestBody?.schema && requestBody.schema.type === "array" /* array */ && this.isBinarySchema(requestBody.schema);
const parametersShouldPutInFormData = parameters.filter(
(p) => p.in === "formData" /* formData */ || p.schema && this.isBinarySchema(p.schema)
);
const parametersShouldNotPutInFormData = parameters.filter(
(p) => !parametersShouldPutInFormData.includes(p)
);
const isRequestBodyContainsBinary = requestBody?.schema && "properties" in requestBody.schema && Object.values(requestBody.schema?.properties ?? {}).some(
(p) => this.isBinarySchema(p)
);
const hasBinaryInParameters = parameters.some(
(p) => p?.schema && this.isBinarySchema(p.schema)
);
const shouldPutParametersOrBodyInFormData = isFormDataRequest || isRequestBodyBinary || hasBinaryInParameters || isRequestBodyContainsBinary || parametersShouldPutInFormData.length > 0;
return t.createBlock([
...shouldPutParametersOrBodyInFormData ? this.toFormDataStatement(
parametersShouldPutInFormData,
requestBody?.schema
) : [],
...adapter.client(
uri,
method,
parametersShouldNotPutInFormData,
requestBody,
response,
adapter,
shouldPutParametersOrBodyInFormData,
shouldParseResponseToJSON
)
]);
}
static schemaToStatemets(parsedDoc, adaptor, options) {
const statements = [];
const { apis, schemas = {}, enums } = parsedDoc;
const enumNames = [];
for (const enumObject of enums) {
enumNames.push(Base.capitalize(enumObject.name));
statements.push(
t.createEnumDeclaration(
[t.createToken(SyntaxKind.ExportKeyword)],
t.createIdentifier(Base.upperCamelCase(enumObject.name)),
enumObject.enum.map((member) => {
return t.createEnumMember(
t.createStringLiteral(
typeof member === "string" ? member : `${member}_`
),
typeof member === "string" ? t.createStringLiteral(member) : t.createNumericLiteral(member)
);
})
)
);
}
for (const schemaKey in schemas) {
if (Object.hasOwnProperty.call(schemas, schemaKey) && !enumNames.includes(Base.upperCamelCase(schemaKey))) {
const schema = schemas[schemaKey];
statements.push(
t.createTypeAliasDeclaration(
[t.createModifier(SyntaxKind.ExportKeyword)],
t.createIdentifier(Base.upperCamelCase(schemaKey)),
void 0,
this.toTypeNode(schema)
)
);
}
}
for (const uri in apis) {
const operations = apis[uri];
for (const operation of operations) {
const {
method,
operationId,
requestBody = [],
responses = [],
summary,
deprecated,
description
} = operation;
let { parameters = [] } = operation;
parameters = parameters.filter((p) => p.in !== "cookie");
if (requestBody.length === 0) {
requestBody.push({ type: "application/json" /* JSON */ });
}
const shouldAddExtraMethodNameSuffix = requestBody.length > 1;
for (const req of requestBody) {
const statement = t.createFunctionDeclaration(
[
t.createModifier(SyntaxKind.ExportKeyword),
t.createModifier(SyntaxKind.AsyncKeyword)
],
void 0,
Base.pathToFnName(uri, method, operationId) + (shouldAddExtraMethodNameSuffix ? Base.capitalize(req.type.split("/")[1]) : ""),
void 0,
[
parameters.length > 0 ? _Generator.toDeclarationNode(parameters) : void 0,
req?.schema ? _Generator.toRequestBodyTypeNode(req.schema) : void 0
].filter(Boolean),
void 0,
this.bodyBlock(
options.baseURL + uri,
method,
parameters,
req,
responses[0],
adaptor
)
);
this.addComments(
statement,
[
description && {
comment: description
},
summary && {
comment: summary
},
deprecated && {
tag: "deprecated"
}
].filter(Boolean)
);
statements.push(statement);
}
}
}
return statements;
}
static async prettier(code) {
return await format(code, {
parser: "typescript"
});
}
static async genCode(schema, initOptions, adaptor) {
const { importClientSource } = initOptions;
const statements = this.schemaToStatemets(schema, adaptor, {
baseURL: initOptions.baseURL ?? ""
});
let code = this.toCode(statements);
if (importClientSource) {
code = importClientSource + "\n\n" + code;
}
return await this.prettier(code);
}
};
// src/core/client/axios.ts
import { factory as t2 } from "typescript";
var AxiosAdapter = class extends Adapter {
/**
* Name of the field used to specify the HTTP method in the request configuration.
*/
methodFieldName = "method";
/**
* Name of the field used to specify the request body (data) in the request configuration.
*/
bodyFieldName = "data";
/**
* Name of the field used to specify the request headers in the request configuration.
*/
headersFieldName = "headers";
/**
* Name of the field used to specify the query parameters in the request configuration.
*/
queryFieldName = "params";
/**
* The name of the client this adapter is configured for, which is 'axios' in this case.
*/
name = "axios";
/**
* Method that should generate and return the client-specific configuration statements.
*
* @returns {Statement[]} An array of TypeScript statements that define the client configuration.
*
* @throws {Error} Indicates that the method is not yet implemented and needs to be filled in.
*/
client(uri, method, parameters, requestBody, response, adapter, shouldUseFormData) {
const statements = [];
const inBody = parameters.filter((p) => !p.in || p.in === "body");
const inHeader = parameters.filter((p) => p.in === "header");
const toLiterlExpression = () => {
return t2.createObjectLiteralExpression(
[
// Set the HTTP method
t2.createPropertyAssignment(
t2.createIdentifier(adapter.methodFieldName),
t2.createStringLiteral(method.toUpperCase())
)
].concat(
// Add headers if there are any
inHeader.length > 0 ? t2.createPropertyAssignment(
t2.createIdentifier(adapter.headersFieldName),
t2.createObjectLiteralExpression(
inHeader.map(
(p) => t2.createPropertyAssignment(
t2.createStringLiteral(p.name),
t2.createCallExpression(
t2.createIdentifier("encodeURIComponent"),
void 0,
[
t2.createCallExpression(
t2.createIdentifier("String"),
void 0,
[
t2.createIdentifier(
Base.camelCase(Base.normalize(p.name))
)
]
)
]
)
)
)
)
) : []
).concat(
// Add body if needed
shouldUseFormData || inBody.length > 0 || requestBody?.schema ? t2.createPropertyAssignment(
t2.createIdentifier(adapter.bodyFieldName),
shouldUseFormData ? t2.createIdentifier("fd") : inBody.length > 0 || requestBody?.schema && !Generator.isBinarySchema(requestBody.schema) ? t2.createIdentifier("req") : t2.createIdentifier("req")
) : []
),
true
);
};
statements.push(
t2.createReturnStatement(
t2.createCallExpression(
t2.createIdentifier(adapter.name),
response?.schema ? [
Generator.toTypeNode(
response.schema
)
] : void 0,
[Generator.toUrlTemplate(uri, parameters), toLiterlExpression()]
)
)
);
return statements;
}
};
// src/core/client/fetch.ts
import { factory as t3, SyntaxKind as SyntaxKind2 } from "typescript";
var FetchAdapter = class extends Adapter {
methodFieldName = "method";
bodyFieldName = "body";
headersFieldName = "headers";
queryFieldName = "";
name = "fetch";
/**
* Generates client code for making API requests using the Fetch API.
* @param uri - The API endpoint URI
* @param method - The HTTP method (GET, POST, etc.)
* @param parameters - Array of parameters to include in the request
* @param requestBody - The request body media type definition
* @param response - The response media type definition
* @param adapter - The adapter instance
* @param shouldUseFormData - Flag to use FormData for the request body
* @param shouldUseJSONResponse - Flag to use JSON parsing for the response
* @return - An array of generated TypeScript statements
*/
client(uri, method, parameters, requestBody, response, adapter, shouldUseFormData, shouldUseJSONResponse) {
const statements = [];
const inBody = parameters.filter((p) => !p.in || p.in === "body");
const inHeader = parameters.filter((p) => p.in === "header");
const toLiterlExpression = () => {
return t3.createObjectLiteralExpression(
[
// Set the HTTP method
t3.createPropertyAssignment(
t3.createIdentifier(adapter.methodFieldName),
t3.createStringLiteral(method.toUpperCase())
)
].concat(
// Add headers if there are any
inHeader.length > 0 ? t3.createPropertyAssignment(
t3.createIdentifier(adapter.headersFieldName),
t3.createObjectLiteralExpression(
inHeader.map(
(p) => t3.createPropertyAssignment(
t3.createStringLiteral(p.name),
t3.createCallExpression(
t3.createIdentifier("encodeURIComponent"),
void 0,
[
t3.createCallExpression(
t3.createIdentifier("String"),
void 0,
[
t3.createIdentifier(
Base.camelCase(Base.normalize(p.name))
)
]
)
]
)
)
)
)
) : []
).concat(
// Add body if needed
shouldUseFormData || inBody.length > 0 || requestBody?.schema ? t3.createPropertyAssignment(
t3.createIdentifier(adapter.bodyFieldName),
shouldUseFormData ? t3.createIdentifier("fd") : inBody.length > 0 || requestBody?.schema && !Generator.isBinarySchema(requestBody.schema) ? t3.createCallExpression(
t3.createPropertyAccessExpression(
t3.createIdentifier("JSON"),
t3.createIdentifier("stringify")
),
[],
[
requestBody ? t3.createIdentifier("req") : t3.createObjectLiteralExpression(
inBody.map(
(b) => t3.createShorthandPropertyAssignment(
t3.createIdentifier(b.name)
)
),
true
)
]
) : (
// One File parameter
t3.createIdentifier("req")
)
) : []
),
true
);
};
statements.push(
t3.createReturnStatement(
shouldUseJSONResponse ? (
// Handle JSON response with proper type checking
t3.createCallExpression(
t3.createPropertyAccessExpression(
t3.createCallExpression(
t3.createIdentifier(adapter.name),
void 0,
[
Generator.toUrlTemplate(uri, parameters),
toLiterlExpression()
]
),
t3.createIdentifier("then")
),
void 0,
[
t3.createArrowFunction(
[t3.createModifier(SyntaxKind2.AsyncKeyword)],
[],
[
t3.createParameterDeclaration(
void 0,
void 0,
t3.createIdentifier("response")
)
],
void 0,
t3.createToken(SyntaxKind2.EqualsGreaterThanToken),
response?.schema ? t3.createAsExpression(
t3.createParenthesizedExpression(
t3.createAwaitExpression(
t3.createCallExpression(
t3.createPropertyAccessExpression(
t3.createIdentifier("response"),
t3.createIdentifier("json")
),
void 0,
[]
)
)
),
response?.schema ? Generator.toTypeNode(response.schema) : t3.createToken(SyntaxKind2.UnknownKeyword)
) : t3.createParenthesizedExpression(
t3.createAwaitExpression(
t3.createCallExpression(
t3.createPropertyAccessExpression(
t3.createIdentifier("response"),
t3.createIdentifier("json")
),
void 0,
[]
)
)
)
)
]
)
) : (
// Simple fetch call without JSON parsing
t3.createCallExpression(
t3.createIdentifier(adapter.name),
void 0,
[Generator.toUrlTemplate(uri, parameters), toLiterlExpression()]
)
)
)
);
return statements;
}
};
// src/openapi/index.ts
import { createScopedLogger } from "@moccona/logger";
// src/openapi/V2.ts
var V2 = class {
doc;
constructor(doc) {
this.doc = doc;
}
/**
* Is array schema.
*/
isOpenAPIArraySchema(schema) {
return typeof schema === "object" && schema.type === "array";
}
/**
* OpenAPI schema to base schema.
*/
getSchemaByRef(schema, reserveRef = false, enums = [], upLevelSchemaKey = "") {
let refName = "";
if (Base.isRef(schema)) {
refName = Base.upperCamelCase(Base.ref2name(schema.$ref));
if (reserveRef) {
return {
type: upLevelSchemaKey + refName
};
}
if (!this.doc.definitions) {
this.doc.definitions = {};
}
schema = this.doc.definitions[Base.ref2name(schema.$ref, this.doc)];
}
return this.toBaseSchema(schema, enums, "", upLevelSchemaKey + refName);
}
/**
* Transform all OpenAPI schema to Base Schema
*/
toBaseSchema(schema, enums = [], schemaKey = "", upLevelSchemaKey = "") {
if (!schema) {
return {
type: "unknown"
};
}
if (Base.isRef(schema)) {
return this.getSchemaByRef(schema, true);
}
if (this.isOpenAPIArraySchema(schema)) {
const { type, description, items, required } = schema;
return {
type,
required: !!required,
description,
items: this.toBaseSchema(items, enums, schemaKey, upLevelSchemaKey)
};
} else {
const {
required = [],
allOf,
anyOf,
description,
enum: enum_,
format: format2,
oneOf,
properties = {}
} = schema;
let { type } = schema;
if (enum_ && type !== "boolean") {
const name = Base.upperCamelCase(upLevelSchemaKey) + Base.upperCamelCase(schemaKey);
const enumObject = {
name,
enum: [...new Set(enum_)]
};
const sameObject = Base.findSameSchema(enumObject, enums);
if (!sameObject && Base.isValidEnumType(schema)) {
enums.push(enumObject);
}
return {
type: sameObject ? sameObject.name : Base.isBooleanEnum(schema) ? "boolean" : enumObject.name,
required,
description
};
}
if (type === void 0 && Object.keys(properties).length > 0) {
type = "object" /* object */;
}
return {
type,
required,
description,
enum: enum_,
format: format2,
allOf: allOf?.map(
(s) => Base.isRef(s) ? {
...s,
ref: s.$ref,
type: Base.upperCamelCase(Base.ref2name(s.$ref, this.doc))
} : this.toBaseSchema(s, enums)
),
anyOf: anyOf?.map(
(s) => Base.isRef(s) ? {
...s,
ref: s.$ref,
type: Base.upperCamelCase(Base.ref2name(s.$ref, this.doc))
} : this.toBaseSchema(s, enums)
),
oneOf: oneOf?.map(
(s) => Base.isRef(s) ? {
...s,
ref: s.$ref,
type: Base.upperCamelCase(Base.ref2name(s.$ref, this.doc))
} : this.toBaseSchema(s, enums)
),
properties: Object.keys(properties).reduce((acc, p) => {
const propSchema = properties[p];
return {
...acc,
[p]: Base.isRef(propSchema) ? {
type: Base.upperCamelCase(
Base.ref2name(propSchema.$ref, this.doc)
)
} : this.toBaseSchema(propSchema, enums, p, upLevelSchemaKey)
};
}, {})
};
}
}
/**
* OpenAPI parameter to base parameter.
*/
getParameterByRef(parameter, enums = [], upLevelSchemaKey = "") {
if (Base.isRef(parameter)) {
parameter = this.doc.parameters[Base.ref2name(parameter.$ref, this.doc)];
}
const {
name,
required,
description,
type,
items,
enum: enum_,
properties,
schema
} = parameter;
if (enum_) {
const type2 = Base.upperCamelCase(upLevelSchemaKey) + Base.upperCamelCase(name);
const enumSchema = {
name: type2,
enum: [...new Set(enum_)]
};
const sameEnum = Base.findSameSchema(enumSchema, enums);
if (!sameEnum && Base.isValidEnumType({ type: type2, enum: enums })) {
enums.push(enumSchema);
}
return {
name,
required,
description,
in: parameter.in,
schema: {
type: sameEnum?.name ?? type2
}
};
}
if (items) {
return {
name,
required,
description,
in: parameter.in,
schema: {
type,
items
}
};
}
if (schema && Base.isRef(schema)) {
return {
name,
required,
description,
in: parameter.in,
schema: {
type: Base.upperCamelCase(Base.ref2name(schema.$ref))
}
};
}
return {
name,
required,
description,
in: parameter.in,
schema: {
type,
properties
}
};
}
/**
* OpenAPI schema to base response
*/
getResponseByRef(schema) {
if (Base.isRef(schema)) {
schema = this.doc.responses[Base.ref2name(schema.$ref, this.doc)];
}
const { schema: responseSchema } = schema;
return [
{
type: "application/json" /* JSON */,
schema: responseSchema && this.getSchemaByRef(responseSchema, true)
}
];
}
init() {
const { definitions = {}, responses = {}, paths = {} } = this.doc;
const enums = [];
const definitions_ = Object.keys(definitions).reduce((acc, key) => {
const schema = definitions[key];
return {
...acc,
[key]: this.getSchemaByRef(schema, false, enums, key)
};
}, {});
const responses_ = Object.keys(responses).reduce((acc, key) => {
const response = responses[key];
return {
...acc,
[key]: this.getResponseByRef(response)
};
}, {});
const apis = Object.keys(paths).reduce((acc, path) => {
const pathObject = paths[path] ?? {};
const { $ref } = pathObject;
const methodApis = [];
if ($ref) {
} else {
const { parameters = [] } = pathObject;
Object.values(HttpMethods).forEach((method) => {
const methodObject = pathObject[method];
if (methodObject) {
const {
deprecated,
operationId,
summary: summary_,
description: description_,
responses: responses2 = {}
} = methodObject;
const { parameters: parameters_ = [] } = methodObject;
const baseParameters = [...parameters, ...parameters_].map(
(parameter) => this.getParameterByRef(parameter, enums)
);
const uniqueParameterName = [
...new Set(baseParameters.map((p) => p.name))
];
if (Object.keys(responses2).length === 0) {
Object.assign(responses2, {
200: {
description: "Successful response"
}
});
}
const inBody = baseParameters.filter(
(p) => p.in === "body" || p.in === "formData"
);
const notInBody = baseParameters.filter(
(p) => p.in !== "body" && p.in !== "formData"
);
const httpCodes = Object.keys(responses2);
for (const code of httpCodes) {
if (code in responses2) {
const response = responses2[code];
const responseSchema = this.getResponseByRef(response);
const inBodyOnlyHasBody = inBody && inBody.length === 1 && inBody[0].in === "body" && inBody[0].name === "body";
methodApis.push({
method,
operationId,
summary: summary_,
deprecated,
description: description_,
parameters: uniqueParameterName.map((name) => notInBody.find((p) => p.name === name)).filter(Boolean),
responses: responseSchema,
requestBody: inBody.length > 0 ? inBodyOnlyHasBody ? [
{
type: "application/json" /* JSON */,
schema: inBody[0].schema
}
] : [
{
type: "application/json" /* JSON */,
schema: {
type: "object" /* object */,
properties: inBody.reduce((a, p) => {
return {
...a,
[p.name]: {
type: p.schema?.type ?? "unknown",
required: p.schema?.required,
// @ts-expect-error items can be undefined
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
items: p.schema?.items,
description: p.schema?.description
}
};
}, {})
}
}
] : void 0
});
break;
}
}
}
});
}
return {
...acc,
[path]: methodApis
};
}, {});
return {
enums: Base.uniqueEnums(enums),
schemas: definitions_,
responses: responses_,
parameters: {},
requestBodies: {},
apis
};
}
};
// src/openapi/V3.ts
var V3 = class {
doc;
constructor(doc) {
this.doc = d