ng-openapi-gen
Version:
An OpenAPI 3 codegen for Angular 12+
396 lines • 18.1 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.filterPaths = exports.runNgOpenApiGen = exports.NgOpenApiGen = void 0;
const json_schema_ref_parser_1 = __importDefault(require("@apidevtools/json-schema-ref-parser"));
const eol_1 = __importDefault(require("eol"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const cmd_args_1 = require("./cmd-args");
const gen_utils_1 = require("./gen-utils");
const globals_1 = require("./globals");
const handlebars_manager_1 = require("./handlebars-manager");
const logger_1 = require("./logger");
const model_1 = require("./model");
const operation_1 = require("./operation");
const service_1 = require("./service");
const templates_1 = require("./templates");
const model_index_1 = require("./model-index");
/**
* Main generator class
*/
class NgOpenApiGen {
constructor(openApi, options) {
this.openApi = openApi;
this.options = options;
this.models = new Map();
this.services = new Map();
this.operations = new Map();
this.logger = new logger_1.Logger(options.silent);
this.outDir = this.options.output || 'src/app/api';
// Make sure the output path doesn't end with a slash
if (this.outDir.endsWith('/') || this.outDir.endsWith('\\')) {
this.outDir = this.outDir.substring(0, this.outDir.length - 1);
}
this.tempDir = this.outDir + '$';
this.initTempDir();
this.initHandlebars();
this.readTemplates();
this.readModels();
this.readServices();
// Ignore the unused models if not set to false in options
if (this.options.ignoreUnusedModels !== false) {
this.ignoreUnusedModels();
}
}
/**
* Set the temp dir to a system temporary directory if option useTempDir is set
*/
initTempDir() {
if (this.options.useTempDir === true) {
const systemTempDir = path_1.default.join(os_1.default.tmpdir(), `ng-openapi-gen-${path_1.default.basename(this.outDir)}$`);
this.tempDir = systemTempDir;
}
}
/**
* Actually generates the files
*/
generate() {
var _a;
// Make sure the temporary directory is empty before starting
(0, gen_utils_1.deleteDirRecursive)(this.tempDir);
fs_extra_1.default.mkdirsSync(this.tempDir);
try {
// Generate each model
const models = [...this.models.values()];
for (const model of models) {
this.write('model', model, model.fileName, 'models');
if (this.options.enumArray && model.enumArrayFileName) {
this.write('enumArray', model, model.enumArrayFileName, 'models');
}
}
// Generate each service and function
const generateServices = (_a = this.options.services) !== null && _a !== void 0 ? _a : true;
const services = [...this.services.values()];
for (const service of services) {
if (generateServices) {
this.write('service', service, service.fileName, 'services');
}
for (const op of service.operations) {
for (const variant of op.variants) {
this.write('fn', variant, variant.importFile, variant.importPath);
}
}
}
// Context object passed to general templates
const general = {
services: services,
models: models
};
// Generate the general files
this.write('configuration', general, this.globals.configurationFile);
this.write('response', general, this.globals.responseFile);
this.write('requestBuilder', general, this.globals.requestBuilderFile);
if (generateServices) {
this.write('baseService', general, this.globals.baseServiceFile);
}
if (this.globals.apiServiceFile) {
this.write('apiService', general, this.globals.apiServiceFile);
}
if (generateServices && this.globals.moduleClass && this.globals.moduleFile) {
this.write('module', general, this.globals.moduleFile);
}
const modelIndex = this.globals.modelIndexFile || this.options.indexFile ? new model_index_1.ModelIndex(models, this.options) : null;
if (this.globals.modelIndexFile) {
this.write('modelIndex', Object.assign(Object.assign({}, general), { modelIndex }), this.globals.modelIndexFile);
}
if (generateServices && this.globals.serviceIndexFile) {
this.write('serviceIndex', general, this.globals.serviceIndexFile);
}
if (this.options.indexFile) {
this.write('index', Object.assign(Object.assign({}, general), { modelIndex }), 'index');
}
// Now synchronize the temp to the output folder
(0, gen_utils_1.syncDirs)(this.tempDir, this.outDir, this.options.removeStaleFiles !== false, this.logger);
this.logger.info(`Generation from ${this.options.input} finished with ${models.length} models and ${services.length} services.`);
}
finally {
// Always remove the temporary directory
(0, gen_utils_1.deleteDirRecursive)(this.tempDir);
}
}
write(template, model, baseName, subDir) {
const ts = this.setEndOfLine(this.templates.apply(template, model));
const file = path_1.default.join(this.tempDir, subDir || '.', `${baseName}.ts`);
const dir = path_1.default.dirname(file);
fs_extra_1.default.mkdirpSync(dir);
fs_extra_1.default.writeFileSync(file, ts, { encoding: 'utf-8' });
}
initHandlebars() {
this.handlebarsManager = new handlebars_manager_1.HandlebarsManager();
this.handlebarsManager.readCustomJsFile(this.options);
}
readTemplates() {
const hasLib = __dirname.endsWith(path_1.default.sep + 'lib');
const builtInDir = path_1.default.join(__dirname, hasLib ? '../templates' : 'templates');
const customDir = this.options.templates || '';
this.globals = new globals_1.Globals(this.options);
this.globals.rootUrl = this.readRootUrl();
this.templates = new templates_1.Templates(builtInDir, customDir, this.handlebarsManager.instance);
this.templates.setGlobals(this.globals);
}
readRootUrl() {
if (!this.openApi.servers || this.openApi.servers.length === 0) {
return '';
}
const server = this.openApi.servers[0];
let rootUrl = server.url;
if (rootUrl == null || rootUrl.length === 0) {
return '';
}
const vars = server.variables || {};
for (const key of Object.keys(vars)) {
const value = String(vars[key].default);
rootUrl = rootUrl.replace(`{${key}}`, value);
}
return rootUrl;
}
readModels() {
const schemas = (this.openApi.components || {}).schemas || {};
for (const name of Object.keys(schemas)) {
const schema = schemas[name];
const model = new model_1.Model(this.openApi, name, schema, this.options);
this.models.set(name, model);
}
}
readServices() {
const defaultTag = this.options.defaultTag || 'Api';
// First read all operations, as tags are by operation
const operationsByTag = new Map();
for (const opPath of Object.keys(this.openApi.paths)) {
const pathSpec = this.openApi.paths[opPath];
for (const method of gen_utils_1.HTTP_METHODS) {
const methodSpec = pathSpec[method];
if (methodSpec) {
let id = methodSpec.operationId;
if (id) {
// Make sure the id is valid
id = (0, gen_utils_1.methodName)(id);
}
else {
// Generate an id
id = (0, gen_utils_1.methodName)(`${opPath}.${method}`);
this.logger.warn(`Operation '${opPath}.${method}' didn't specify an 'operationId'. Assuming '${id}'.`);
}
if (this.operations.has(id)) {
// Duplicated id. Add a suffix
let suffix = 0;
let newId = id;
while (this.operations.has(newId)) {
newId = `${id}_${++suffix}`;
}
this.logger.warn(`Duplicate operation id '${id}'. Assuming id ${newId} for operation '${opPath}.${method}'.`);
id = newId;
}
const operation = new operation_1.Operation(this.openApi, opPath, pathSpec, method, id, methodSpec, this.options);
// Set a default tag if no tags are found
if (operation.tags.length === 0) {
this.logger.warn(`No tags set on operation '${opPath}.${method}'. Assuming '${defaultTag}'.`);
operation.tags.push(defaultTag);
}
for (const tag of operation.tags) {
let operations = operationsByTag.get(tag);
if (!operations) {
operations = [];
operationsByTag.set(tag, operations);
}
operations.push(operation);
}
// Store the operation
this.operations.set(id, operation);
}
}
}
// Then create a service per operation, as long as the tag is included
const includeTags = this.options.includeTags || [];
const excludeTags = this.options.excludeTags || [];
const tags = this.openApi.tags || [];
for (const tagName of operationsByTag.keys()) {
if (includeTags.length > 0 && !includeTags.includes(tagName)) {
this.logger.info(`Ignoring tag ${tagName} because it is not listed in the 'includeTags' option`);
continue;
}
if (excludeTags.length > 0 && excludeTags.includes(tagName)) {
this.logger.info(`Ignoring tag ${tagName} because it is listed in the 'excludeTags' option`);
continue;
}
const operations = operationsByTag.get(tagName) || [];
const tag = tags.find(t => t.name === tagName) || { name: tagName };
const service = new service_1.Service(tag, operations, this.options);
this.services.set(tag.name, service);
}
}
ignoreUnusedModels() {
// First, collect all type names used by services
const usedNames = new Set();
for (const service of this.services.values()) {
for (const imp of service.imports) {
if (imp.path.includes('/models/')) {
usedNames.add(imp.typeName);
}
}
for (const op of service.operations) {
for (const variant of op.variants) {
for (const imp of variant.imports) {
if (imp.path.includes('/models/')) {
usedNames.add(imp.typeName);
}
}
}
}
for (const imp of service.additionalDependencies) {
usedNames.add(imp);
}
}
// Collect dependencies on models themselves
const referencedModels = Array.from(usedNames);
usedNames.clear();
referencedModels.forEach(name => this.collectDependencies(name, usedNames));
// Then delete all unused models
for (const model of this.models.values()) {
if (!usedNames.has(model.name)) {
this.logger.debug(`Ignoring model ${model.name} because it is not used anywhere`);
this.models.delete(model.name);
}
}
}
collectDependencies(name, usedNames) {
const model = this.models.get(name);
if (!model || usedNames.has(model.name)) {
return;
}
// Add the model name itself
usedNames.add(model.name);
// Then find all referenced names and recurse
this.allReferencedNames(model.schema).forEach(n => this.collectDependencies(n, usedNames));
}
allReferencedNames(schema) {
if (!schema) {
return [];
}
if (schema.$ref) {
return [(0, gen_utils_1.simpleName)(schema.$ref)];
}
schema = schema;
const result = [];
(schema.allOf || []).forEach(s => Array.prototype.push.apply(result, this.allReferencedNames(s)));
(schema.anyOf || []).forEach(s => Array.prototype.push.apply(result, this.allReferencedNames(s)));
(schema.oneOf || []).forEach(s => Array.prototype.push.apply(result, this.allReferencedNames(s)));
if (schema.properties) {
for (const prop of Object.keys(schema.properties)) {
Array.prototype.push.apply(result, this.allReferencedNames(schema.properties[prop]));
}
}
if (typeof schema.additionalProperties === 'object') {
Array.prototype.push.apply(result, this.allReferencedNames(schema.additionalProperties));
}
if (schema.items) {
Array.prototype.push.apply(result, this.allReferencedNames(schema.items));
}
return result;
}
setEndOfLine(text) {
switch (this.options.endOfLineStyle) {
case 'cr':
return eol_1.default.cr(text);
case 'lf':
return eol_1.default.lf(text);
case 'crlf':
return eol_1.default.crlf(text);
default:
return eol_1.default.auto(text);
}
}
}
exports.NgOpenApiGen = NgOpenApiGen;
/**
* Parses the command-line arguments, reads the configuration file and run the generation
*/
function runNgOpenApiGen() {
return __awaiter(this, void 0, void 0, function* () {
const options = (0, cmd_args_1.parseOptions)();
const refParser = new json_schema_ref_parser_1.default();
const input = options.input;
try {
const openApi = yield refParser.bundle(input, {
dereference: {
circular: false
},
resolve: {
http: {
timeout: options.fetchTimeout == null ? 20000 : options.fetchTimeout
}
}
});
const { excludeTags = [], excludePaths = [], includeTags = [] } = options;
openApi.paths = filterPaths(openApi.paths, excludeTags, excludePaths, includeTags);
const gen = new NgOpenApiGen(openApi, options);
gen.generate();
}
catch (err) {
console.log(`Error on API generation from ${input}: ${err}`);
process.exit(1);
}
});
}
exports.runNgOpenApiGen = runNgOpenApiGen;
function filterPaths(paths, excludeTags = [], excludePaths = [], includeTags = []) {
var _a, _b, _c;
paths = JSON.parse(JSON.stringify(paths));
const filteredPaths = {};
for (const key in paths) {
if (!paths.hasOwnProperty(key))
continue;
if (excludePaths === null || excludePaths === void 0 ? void 0 : excludePaths.includes(key)) {
console.log(`Path ${key} is excluded by excludePaths`);
continue;
}
let shouldRemovePath = false;
for (const method of Object.keys(paths[key])) {
const tags = ((_b = (_a = paths[key]) === null || _a === void 0 ? void 0 : _a[method]) === null || _b === void 0 ? void 0 : _b.tags) || [];
// if tag on method in includeTags then continue
if (tags.some(tag => includeTags.includes(tag))) {
continue;
}
// if tag on method in excludeTags then remove the method
if (tags.some(tag => excludeTags.includes(tag)) || !!(includeTags === null || includeTags === void 0 ? void 0 : includeTags.length)) {
console.log(`Path ${key} is excluded by excludeTags`);
(_c = paths[key]) === null || _c === void 0 ? true : delete _c[method];
// if path has no method left then "should remove"
if (Object.keys(paths[key]).length === 0) {
shouldRemovePath = true;
break;
}
}
}
if (shouldRemovePath) {
continue;
}
filteredPaths[key] = paths[key];
}
return filteredPaths;
}
exports.filterPaths = filterPaths;
//# sourceMappingURL=ng-openapi-gen.js.map