UNPKG

ng-openapi-gen

Version:
396 lines 18.1 kB
"use strict"; 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