ng-openapi-gen
Version:
An OpenAPI 3.0 and 3.1 codegen for Angular 16+
490 lines • 19.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HTTP_METHODS = void 0;
exports.simpleName = simpleName;
exports.unqualifiedName = unqualifiedName;
exports.qualifiedName = qualifiedName;
exports.modelFile = modelFile;
exports.namespace = namespace;
exports.ensureNotReserved = ensureNotReserved;
exports.typeName = typeName;
exports.enumName = enumName;
exports.methodName = methodName;
exports.fileName = fileName;
exports.toBasicChars = toBasicChars;
exports.tsComments = tsComments;
exports.modelClass = modelClass;
exports.serviceClass = serviceClass;
exports.escapeId = escapeId;
exports.tsType = tsType;
exports.resolveRef = resolveRef;
exports.deleteDirRecursive = deleteDirRecursive;
exports.syncDirs = syncDirs;
const fs_extra_1 = __importDefault(require("fs-extra"));
const jsesc_1 = __importDefault(require("jsesc"));
const lodash_1 = require("lodash");
const path_1 = __importDefault(require("path"));
const openapi_typings_1 = require("./openapi-typings");
exports.HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
/**
* Returns the simple name, that is, the last part after '/'
*/
function simpleName(name) {
const pos = name.lastIndexOf('/');
return name.substring(pos + 1);
}
/**
* Returns the unqualified model class name, that is, the last part after '.'
*/
function unqualifiedName(name, options) {
const pos = name.lastIndexOf('.');
return modelClass(name.substring(pos + 1), options);
}
/**
* Returns the qualified model class name, that is, the camelized namespace (if any) plus the unqualified name
*/
function qualifiedName(name, options) {
const ns = namespace(name);
const unq = unqualifiedName(name, options);
return ns ? typeName(ns) + unq : unq;
}
/**
* Returns the filename where to write a model with the given name
*/
function modelFile(name, options) {
let result = '';
const ns = namespace(name);
if (ns) {
result += `/${ns}`;
}
const file = unqualifiedName(name, options);
return result += '/' + fileName(file);
}
/**
* Returns the namespace path, that is, the part before the last '.' split by '/' instead of '.'.
* If there's no namespace, returns undefined.
*/
function namespace(name) {
name = name.replace(/^\.+/g, '');
name = name.replace(/\.+$/g, '');
const pos = name.lastIndexOf('.');
return pos < 0 ? undefined : name.substring(0, pos).replace(/\./g, '/');
}
const RESERVED_KEYWORDS = ['abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final', 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while', 'with', 'yield'];
/**
* If the given name is a JS reserved keyword, suffix it with a `$` character
*/
function ensureNotReserved(name) {
return RESERVED_KEYWORDS.includes(name) ? name + '$' : name;
}
/**
* Returns the type (class) name for a given regular name
*/
function typeName(name, options) {
if (options?.camelizeModelNames === false) {
return (0, lodash_1.upperFirst)(toBasicChars(name, true));
}
else {
return (0, lodash_1.upperFirst)(methodName(name));
}
}
/**
* Returns the name of the enum constant for a given value
*/
function enumName(value, options) {
let name = toBasicChars(value, true);
if (options.enumStyle === 'ignorecase') {
return name;
}
else if (options.enumStyle === 'upper') {
name = (0, lodash_1.upperCase)(name).replace(/\s+/g, '_');
}
else {
name = (0, lodash_1.upperFirst)((0, lodash_1.camelCase)(name));
}
if (/^\d/.test(name)) {
name = '$' + name;
}
return name;
}
/**
* Returns a suitable method name for the given name
* @param name The raw name
*/
function methodName(name) {
return (0, lodash_1.camelCase)(toBasicChars(name, true));
}
/**
* Returns the file name for a given type name
*/
function fileName(text) {
return (0, lodash_1.kebabCase)(toBasicChars(text));
}
/**
* Converts a text to a basic, letters / numbers / underscore representation.
* When firstNonDigit is true, prepends the result with an uderscore if the first char is a digit.
*/
function toBasicChars(text, firstNonDigit = false) {
text = (0, lodash_1.deburr)((text || '').trim());
text = text.replace(/[^\w$]+/g, '_');
if (firstNonDigit && /[0-9]/.test(text.charAt(0))) {
text = '_' + text;
}
return text;
}
/**
* Returns the TypeScript comments for the given schema description, in a given indentation level
*/
function tsComments(description, level, deprecated) {
const indent = ' '.repeat(level);
if (description === undefined || description.length === 0) {
return indent + (deprecated ? '/** @deprecated */' : '');
}
const lines = description.trim().split('\n');
let result = '\n' + indent + '/**\n';
lines.forEach(line => {
result += indent + ' *' + (line === '' ? '' : ' ' + line.replace(/\*\//g, '* / ')) + '\n';
});
if (deprecated) {
result += indent + ' *\n' + indent + ' * @deprecated\n';
}
result += indent + ' */\n' + indent;
return result;
}
/**
* Applies the prefix and suffix to a model class name
*/
function modelClass(baseName, options) {
return `${options.modelPrefix || ''}${typeName(baseName, options)}${options.modelSuffix || ''}`;
}
/**
* Applies the prefix and suffix to a service class name
*/
function serviceClass(baseName, options) {
return `${options.servicePrefix || ''}${typeName(baseName, options)}${options.serviceSuffix || 'Service'}`;
}
/**
* Escapes the name of a property / parameter if not valid JS identifier
*/
function escapeId(name) {
if (/^[a-zA-Z]\w*$/.test(name)) {
return name;
}
else {
return `'${name.replace(/\'/g, '\\\'')}'`;
}
}
/**
* Appends | null to the given type
*/
function maybeAppendNull(type, nullable) {
if (` ${type} `.includes('null') || !nullable) {
// The type itself already includes null
return type;
}
return (type.includes(' ') ? `(${type})` : type) + (nullable ? ' | null' : '');
}
function rawTsType(schema, options, openApi, container) {
// An union of types
const union = schema.oneOf || schema.anyOf || [];
if (union.length > 0) {
if (union.length > 1) {
return `(${union.map(u => tsType(u, options, openApi, container)).join(' | ')})`;
}
else {
return union.map(u => tsType(u, options, openApi, container)).join(' | ');
}
}
const type = (0, openapi_typings_1.getSchemaType)(schema);
// Handle OpenAPI 3.1 union types (type array)
if (Array.isArray(type)) {
const nonNullTypes = type.filter(t => t !== 'null');
const hasNull = type.includes('null');
if (nonNullTypes.length > 1) {
// Generate union of the different types
const unionTypes = nonNullTypes.map(t => {
// Create a schema object with single type for recursive processing
const singleTypeSchema = { ...schema, type: t };
return rawTsType(singleTypeSchema, options, openApi, container);
}).filter(t => t !== null);
// Remove duplicates
const uniqueTypes = [...new Set(unionTypes)];
if (uniqueTypes.length === 1) {
return hasNull ? `(${uniqueTypes[0]} | null)` : uniqueTypes[0];
}
const unionType = uniqueTypes.join(' | ');
return hasNull ? `(${unionType} | null)` : `(${unionType})`;
}
else if (nonNullTypes.length === 1) {
// Single non-null type, process normally
const singleType = nonNullTypes[0];
const singleTypeSchema = { ...schema, type: singleType };
const result = rawTsType(singleTypeSchema, options, openApi, container);
return hasNull ? `(${result} | null)` : result;
}
else if (hasNull) {
// Only null type
return 'null';
}
// Fallback to any if no valid types
return 'any';
}
// An array
if (type === 'array' || (0, openapi_typings_1.isArraySchemaObject)(schema)) {
// Check for OpenAPI 3.1 prefixItems (tuple types)
if ('prefixItems' in schema && Array.isArray(schema.prefixItems)) {
const prefixItems = schema.prefixItems;
const tupleTypes = prefixItems.map((item) => tsType(item, options, openApi, container));
// Check if additional items are allowed
const additionalItems = schema.items;
if (additionalItems === false || additionalItems === undefined) {
// Exact tuple - no additional items
return `[${tupleTypes.join(', ')}]`;
}
else if (additionalItems) {
// Tuple with additional items of specific type
const additionalType = tsType(additionalItems, options, openApi, container);
return `[${tupleTypes.join(', ')}, ...${additionalType}[]]`;
}
else {
// Tuple with any additional items
return `[${tupleTypes.join(', ')}, ...any[]]`;
}
}
const items = (0, openapi_typings_1.isArraySchemaObject)(schema) && 'items' in schema ? schema.items : {};
const itemsType = tsType(items, options, openApi, container);
return `Array<${itemsType}>`;
}
// All the types
const allOf = schema.allOf || [];
let intersectionType = [];
if (allOf.length > 0) {
intersectionType = allOf.map(u => tsType(u, options, openApi, container));
}
// An object
if (type === 'object' || schema.properties) {
let result = '{\n';
const properties = schema.properties || {};
const required = schema.required;
for (const baseSchema of allOf) {
const discriminators = findAllDiscriminators(baseSchema, schema, openApi);
for (const discriminator of discriminators) {
result += `'${discriminator.propName}': '${discriminator.value}';\n`;
}
}
for (const propName of Object.keys(properties)) {
const property = properties[propName];
if (!property) {
continue;
}
if (property.description) {
result += tsComments(property.description, 0, property.deprecated);
}
result += `'${propName}'`;
const propRequired = required && required.includes(propName);
if (!propRequired) {
result += '?';
}
const propertyType = tsType(property, options, openApi, container);
result += `: ${propertyType};\n`;
}
if (schema.additionalProperties) {
const additionalProperties = schema.additionalProperties === true ? {} : schema.additionalProperties;
result += `[key: string]: ${tsType(additionalProperties, options, openApi, container)};\n`;
}
result += '}';
intersectionType.push(result);
}
if (intersectionType.length > 0) {
return intersectionType.join(' & ');
}
// Inline enum
const enumValues = schema.enum || (schema.const ? [schema.const] : []);
if (enumValues.length > 0) {
if (type === 'number' || type === 'integer' || type === 'boolean') {
return enumValues.join(' | ');
}
else {
return enumValues.map(v => `'${(0, jsesc_1.default)(v)}'`).join(' | ');
}
}
// A Blob
if (type === 'string' && schema.format === 'binary') {
return 'Blob';
}
// A simple type (integer doesn't exist as type in JS, use number instead)
if (type) {
const finalType = type === 'integer' ? 'number' : type;
return finalType;
}
// If no type is specified, default to 'any'
return 'any';
}
/**
* Returns the TypeScript type for the given type and options
*/
function tsType(schemaOrRef, options, openApi, container) {
if (!schemaOrRef) {
// No schema
return 'any';
}
if ((0, openapi_typings_1.isReferenceObject)(schemaOrRef)) {
// A reference
const resolved = resolveRef(openApi, schemaOrRef.$ref);
const name = simpleName(schemaOrRef.$ref);
// When referencing the same container, use its type name
if (container && container.name === name) {
return maybeAppendNull(container.typeName, (0, openapi_typings_1.isNullable)(resolved));
}
// Check if the container has an import alias for this reference
if (container && typeof container.getImportTypeName === 'function') {
const aliasedTypeName = container.getImportTypeName(name);
return maybeAppendNull(aliasedTypeName, (0, openapi_typings_1.isNullable)(resolved));
}
// Fallback to qualified name
return maybeAppendNull(qualifiedName(name, options), (0, openapi_typings_1.isNullable)(resolved));
}
// Resolve the actual type (maybe nullable)
const schema = schemaOrRef;
const type = rawTsType(schema, options, openApi, container);
return maybeAppendNull(type, (0, openapi_typings_1.isNullable)(schema));
}
/**
* Resolves a reference
* @param ref The reference name, such as #/components/schemas/Name, or just Name
*/
function resolveRef(openApi, ref) {
if (!ref.includes('/')) {
ref = `#/components/schemas/${ref}`;
}
let current = null;
for (let part of ref.split('/')) {
part = part.trim();
if (part === '#' || part === '') {
current = openApi;
}
else if (current == null) {
break;
}
else {
current = current[part];
}
}
if (current == null || typeof current !== 'object') {
throw new Error(`Couldn't resolve reference ${ref}`);
}
return current;
}
/**
* Recursively deletes a directory
*/
function deleteDirRecursive(dir) {
if (fs_extra_1.default.existsSync(dir)) {
fs_extra_1.default.readdirSync(dir).forEach((file) => {
const curPath = path_1.default.join(dir, file);
if (fs_extra_1.default.lstatSync(curPath).isDirectory()) { // recurse
deleteDirRecursive(curPath);
}
else { // delete file
fs_extra_1.default.unlinkSync(curPath);
}
});
fs_extra_1.default.rmdirSync(dir);
}
}
/**
* Synchronizes the files from the source to the target directory. Optionally remove stale files.
*/
function syncDirs(srcDir, destDir, removeStale, logger) {
fs_extra_1.default.ensureDirSync(destDir);
const srcFiles = fs_extra_1.default.readdirSync(srcDir);
const destFiles = fs_extra_1.default.readdirSync(destDir);
for (const file of srcFiles) {
const srcFile = path_1.default.join(srcDir, file);
const destFile = path_1.default.join(destDir, file);
if (fs_extra_1.default.lstatSync(srcFile).isDirectory()) {
// A directory: recursively sync
syncDirs(srcFile, destFile, removeStale, logger);
}
else {
// Read the content of both files and update if they differ
const srcContent = fs_extra_1.default.readFileSync(srcFile, { encoding: 'utf-8' });
const destContent = fs_extra_1.default.existsSync(destFile) ? fs_extra_1.default.readFileSync(destFile, { encoding: 'utf-8' }) : null;
if (srcContent !== destContent) {
fs_extra_1.default.writeFileSync(destFile, srcContent, { encoding: 'utf-8' });
logger.debug('Wrote ' + destFile);
}
}
}
if (removeStale) {
for (const file of destFiles) {
const srcFile = path_1.default.join(srcDir, file);
const destFile = path_1.default.join(destDir, file);
if (!fs_extra_1.default.existsSync(srcFile) && fs_extra_1.default.lstatSync(destFile).isFile()) {
fs_extra_1.default.unlinkSync(destFile);
logger.debug('Removed stale file ' + destFile);
}
}
}
}
/**
* Recursively finds all discriminators from a base schema and its inheritance chain for a derived schema.
*/
function findAllDiscriminators(baseSchemaOrRef, derivedSchema, openApi) {
const discriminators = [];
const visited = new Set();
function collectDiscriminators(currentSchemaOrRef) {
const currentSchema = ((0, openapi_typings_1.isReferenceObject)(currentSchemaOrRef) ? resolveRef(openApi, currentSchemaOrRef.$ref) : currentSchemaOrRef);
// Avoid infinite recursion
const schemaKey = (0, openapi_typings_1.isReferenceObject)(currentSchemaOrRef) ? currentSchemaOrRef.$ref : JSON.stringify(currentSchema);
if (visited.has(schemaKey)) {
return;
}
visited.add(schemaKey);
// Check if current schema has a discriminator
const discriminator = tryGetDiscriminator(currentSchemaOrRef, derivedSchema, openApi);
if (discriminator) {
discriminators.push(discriminator);
}
// Recursively check allOf schemas
if (currentSchema.allOf) {
for (const allOfSchema of currentSchema.allOf) {
collectDiscriminators(allOfSchema);
}
}
}
collectDiscriminators(baseSchemaOrRef);
return discriminators;
}
/**
* Tries to get a discriminator info from a base schema and for a derived one.
*/
function tryGetDiscriminator(baseSchemaOrRef, derivedSchema, openApi) {
const baseSchema = ((0, openapi_typings_1.isReferenceObject)(baseSchemaOrRef) ? resolveRef(openApi, baseSchemaOrRef.$ref) : baseSchemaOrRef);
const discriminatorProp = baseSchema.discriminator?.propertyName;
if (discriminatorProp) {
const discriminatorValue = tryGetDiscriminatorValue(baseSchema, derivedSchema, openApi);
if (discriminatorValue) {
return {
propName: discriminatorProp,
value: discriminatorValue
};
}
}
return undefined;
}
/**
* Tries to get a discriminator value from a base schema and for a derived one.
*/
function tryGetDiscriminatorValue(baseSchema, derivedSchema, openApi) {
const mapping = baseSchema.discriminator?.mapping;
if (mapping) {
const mappingIndex = Object.values(mapping).findIndex((ref) => resolveRef(openApi, ref) === derivedSchema);
return Object.keys(mapping)[mappingIndex] ?? null;
}
return null;
}
//# sourceMappingURL=gen-utils.js.map