UNPKG

reclass-doc

Version:

Reclass model documentation generator.

470 lines (469 loc) 17.9 kB
"use strict"; /** * Reclass doc generator * * @author Jiri Hybek <jiri@hybek.cz> * @license Apache-2.0 (c) 2017 Jiri Hybek */ Object.defineProperty(exports, "__esModule", { value: true }); var fs = require("fs"); var crypto = require("crypto"); var YamlTokenizer_1 = require("./YamlTokenizer"); var Util_1 = require("./Util"); var MERGE_TYPE; (function (MERGE_TYPE) { MERGE_TYPE[MERGE_TYPE["ORIGIN"] = 0] = "ORIGIN"; MERGE_TYPE[MERGE_TYPE["MERGED"] = 1] = "MERGED"; MERGE_TYPE[MERGE_TYPE["REPLACED"] = 2] = "REPLACED"; })(MERGE_TYPE = exports.MERGE_TYPE || (exports.MERGE_TYPE = {})); /** * Resolver class * * Resolves class tree */ var Resolver = (function () { /** * Resolver constructor * * @param reclassRoot Reclass classes dir */ function Resolver(reclassRoot) { /** Classes sub-directory */ this.classesDir = '/classes'; /** Nodes sub-directory */ this.nodesDir = '/nodes'; /** Max depth limit */ this.depthLimit = 32; /** Cache of resolved classes */ this.cache = {}; /** Inverted dependency tree - class -> dependants */ this.dependencyTree = {}; this.reclassRoot = reclassRoot; this.tokenizer = new YamlTokenizer_1.YamlTokenizer(); } /** * Merges class param * * @param target Target param * @param source Source param */ Resolver.prototype.mergeParams = function (target, source) { //Clone sources var _sources = []; var lastSource; for (var i = 0; i < source.sources.length; i++) { if (i < source.sources.length - 1) { _sources.push(source.sources[i]); } else { lastSource = Util_1.clone(source.sources[i]); _sources.push(lastSource); } //Add comments if (source.comment.length > 0) { for (var c in source.comment) if (source.comment[c].length > 0 && !Util_1.deepContains(target.comment, source.comment[c])) target.comment.push(source.comment[c]); target.comment = target.comment.concat(source.comment); } } //MERGE MAP if (target.type === YamlTokenizer_1.TOKEN_TYPE.MAP && source.type === YamlTokenizer_1.TOKEN_TYPE.MAP) { lastSource.mergeType = MERGE_TYPE.MERGED; for (var i in source.value) { if (target.value[i]) this.mergeParams(target.value[i], source.value[i]); else target.value[i] = Util_1.clone(source.value[i]); } //MERGE SEQUENCE } else if (target.type === YamlTokenizer_1.TOKEN_TYPE.SEQUENCE && source.type === YamlTokenizer_1.TOKEN_TYPE.SEQUENCE) { lastSource.mergeType = MERGE_TYPE.MERGED; for (var i = 0; i < source.value.length; i++) if (!Util_1.deepContains(target.value, source.value[i], ["sources", "comment"])) target.value.push(Util_1.clone(source.value[i])); //REPLACE } else { lastSource.mergeType = MERGE_TYPE.REPLACED; target.value = Util_1.clone(source.value); } //Add sources target.sources = target.sources.concat(_sources); }; /** * Merges token(s) to param(s) * * @param target Target param * @param token Source token * @param name Source class name */ Resolver.prototype.parseTokenParams = function (token, className, classType) { var source = { className: className, classType: classType, token: token, type: token.type, mergeType: MERGE_TYPE.ORIGIN, value: null, comment: token.comment }; //Define param var param = { sources: [source], type: source.type, value: Util_1.clone(source.value), ref: null, comment: [source.comment] }; //Parse map if (token.type === YamlTokenizer_1.TOKEN_TYPE.MAP) { source.value = "[map]"; param.value = {}; for (var i in token.value) param.value[i] = this.parseTokenParams(token.value[i], className, classType); //Parse sequence } else if (token.type === YamlTokenizer_1.TOKEN_TYPE.SEQUENCE) { source.value = "[sequence]"; param.value = []; for (var i = 0; i < token.value.length; i++) param.value.push(this.parseTokenParams(token.value[i], className, classType)); //Parse vale } else { source.value = token.value; param.value = token.value; } return param; }; /** * Resolves file * * @param prefix Prefix directory * @param name Relative file / directory path without extension * @param depth Nesting depth * @param loadedClasses Already loaded classes */ Resolver.prototype.resolve = function (prefix, name, path, depth, loadedClasses) { if (depth === void 0) { depth = 0; } if (loadedClasses === void 0) { loadedClasses = []; } if (depth > this.depthLimit) throw new Error("Maximum class depth limit of " + this.depthLimit + " exceeded."); var classId = prefix + "/" + name; //Check cache if (this.cache[classId]) return this.cache[classId]; //Define path var _path = this.reclassRoot + prefix + "/" + path; var relativePath = prefix + "/" + name; var isInit = false; //Identify YAML if (fs.existsSync(_path) && fs.lstatSync(_path).isDirectory()) { _path += "/init"; relativePath += "/init"; isInit = true; } if (fs.existsSync(_path + ".yml")) { _path += ".yml"; relativePath += ".yml"; } else if (fs.existsSync(_path + ".yaml")) { _path += ".yaml"; relativePath += ".yaml"; } else { throw new Error("File '" + _path + "(init.yml|.yml|.yaml)' not found."); } //Load yaml var yaml = fs.readFileSync(_path, { encoding: 'utf-8' }); var stat = fs.statSync(_path); //Parse tokens var token; try { token = this.tokenizer.parse(yaml); } catch (err) { throw new Error("YAML parse error: " + String(err)); } if (!token) throw new Error("Class '" + name + "' file is empty."); //Prepare class instance var rClass = { id: classId, name: name, type: (prefix == this.nodesDir ? Util_1.CLASS_TYPE.NODE : Util_1.CLASS_TYPE.CLASS), filename: _path, relativePath: relativePath, isInit: isInit, classes: [], applications: {}, dependents: {}, params: { type: YamlTokenizer_1.TOKEN_TYPE.MAP, sources: [], value: {}, ref: null, comment: [] }, comment: token.comment, fingerprint: null, modified: stat.mtime.getTime(), resolvedClasses: [] }; var fingerprint = crypto.createHash('md5').update(_path + ":" + stat.mtime); //Parse classes if (token.value['classes']) { var _classesToken = token.value['classes']; if (_classesToken.type != YamlTokenizer_1.TOKEN_TYPE.SEQUENCE) throw new Error("Error parsing class '" + classId + "', 'classes' are not sequence type."); for (var i = 0; i < _classesToken.value.length; i++) { var _classToken = _classesToken.value[i]; var _classId = this.classesDir + "/" + _classToken.value; if (rClass.resolvedClasses.indexOf(_classToken.value) >= 0) continue; var _resolvedClass = { id: _classId, name: _classToken.value, classes: [], error: null }; rClass.resolvedClasses.push(_classToken.value); try { var _class = this.resolveClass(_classToken.value, depth + 1, loadedClasses); //Create dependency tree _class.dependents[classId] = { id: classId, type: rClass.type, name: rClass.name }; _resolvedClass.classes = _class.classes; if (!this.dependencyTree[_classId]) this.dependencyTree[_classId] = []; if (this.dependencyTree[_classId].indexOf(classId) < 0) this.dependencyTree[_classId].push(classId); //Merge params if not already merged elsewhere //if(loadedClasses.indexOf(_class.id) < 0){ //Merge applications for (var j in _class.applications) { if (!rClass.applications[j]) rClass.applications[j] = Util_1.clone(_class.applications[j]); else rClass.applications[j].sources = _class.applications[j].sources.concat(rClass.applications[j].sources); } //Merge params this.mergeParams(rClass.params, _class.params); //Add to loaded classes loadedClasses.push(_class.id); //Update fingerprint fingerprint.update(_class.fingerprint); //} } catch (err) { _resolvedClass.error = err; } rClass.classes.push(_resolvedClass); } } //Add applications if (token.value['applications']) { var _appsToken = token.value['applications']; if (_appsToken.type !== YamlTokenizer_1.TOKEN_TYPE.SEQUENCE) throw new Error("Error parsing class '" + classId + "', 'applications' are not sequence type."); if (_appsToken.value instanceof Array) for (var i = 0; i < _appsToken.value.length; i++) { var _appToken = _appsToken.value[i]; if (!rClass.applications[_appToken.value]) rClass.applications[_appToken.value] = { name: _appToken.value, sources: [] }; rClass.applications[_appToken.value].sources.push({ className: rClass.name, classType: rClass.type, token: _appToken, comment: _appToken.comment }); } } //Add params if (token.value['parameters']) { var _paramsToken = token.value['parameters']; if (_paramsToken.type !== YamlTokenizer_1.TOKEN_TYPE.MAP) throw new Error("Error parsing class '" + classId + "', 'parameters' are not map type."); var localParams = this.parseTokenParams(_paramsToken, rClass.name, rClass.type); this.mergeParams(rClass.params, localParams); } //Store fingerprint rClass.fingerprint = fingerprint.digest('hex'); this.cache[classId] = rClass; return rClass; }; /** * Parses params interpolation * * @param rootParam Root parameter * @param param Current parameter */ Resolver.prototype.parseInterpolation = function (rootParam, param) { if (param.type === YamlTokenizer_1.TOKEN_TYPE.MAP) { if (param.value instanceof Object) for (var i in param.value) this.parseInterpolation(rootParam, param.value[i]); } else if (param.type === YamlTokenizer_1.TOKEN_TYPE.SEQUENCE) { if (param.value instanceof Array) for (var i = 0; i < param.value.length; i++) this.parseInterpolation(rootParam, param.value[i]); } else if (typeof param.value === 'string') { var resolved = false; while (!resolved) { var pattern = new RegExp(/\${[^}]+}/g); var ip = void 0; var replacements = []; resolved = true; while ((ip = pattern.exec(param.value))) { var refKey = ip[0].substr(2, ip[0].length - 3); var refPath = refKey.split(":"); var stack = rootParam; var key = void 0; while ((key = refPath.shift())) { if (stack.value instanceof Object && stack.value[key] !== undefined) stack = stack.value[key]; else { stack = null; break; } } replacements.push({ key: ip[0], value: (stack ? (stack.value instanceof Object ? "#!ref!#" + ip[0].substr(2, ip[0].length - 3) : stack.value) : null) }); } if (replacements.length > 0) { if (param.ref === null) param.ref = []; resolved = false; } for (var i = 0; i < replacements.length; i++) { param.ref.push(replacements[i].key); if (param.value === replacements[i].key) param.value = replacements[i].value; else param.value = param.value.replace(replacements[i].key, replacements[i].value); } } } }; /** * Returns new class instance with interpolated params * * @param rClass Original class */ Resolver.prototype.interpolateClass = function (rClass) { //Define new class with cloned params var nClass = { id: rClass.id, name: rClass.name, type: rClass.type, filename: rClass.filename, relativePath: rClass.relativePath, isInit: rClass.isInit, classes: rClass.classes, applications: rClass.applications, dependents: rClass.dependents, params: Util_1.clone(rClass.params), //params: rClass.params, comment: rClass.comment, fingerprint: rClass.fingerprint, modified: rClass.modified, resolvedClasses: rClass.resolvedClasses }; this.parseInterpolation(nClass.params, nClass.params); return nClass; }; /** * Resolves class * * @param name Class name * @param depth Nesting depth * @param loadedClasses Already loaded classes */ Resolver.prototype.resolveClass = function (name, depth, loadedClasses) { if (depth === void 0) { depth = 0; } if (loadedClasses === void 0) { loadedClasses = []; } return this.resolve(this.classesDir, name, name.replace(/\./g, '/'), depth, loadedClasses); }; /** * Resolves node * * @param name Node name * @param interpolate If to interpolate params * @param depth Nesting depth */ Resolver.prototype.resolveNode = function (name, interpolate, depth) { if (interpolate === void 0) { interpolate = true; } if (depth === void 0) { depth = 0; } var rClass = this.resolve(this.nodesDir, name, name, depth); if (interpolate) return this.interpolateClass(rClass); else return rClass; }; /** * Invalidate class and it's dependents * * @param classId Class ID */ Resolver.prototype.invalidate = function (classId) { console.log("INVALIDATING", classId); //Delete from cache if (this.cache[classId]) delete this.cache[classId]; //Invalidate dependants if (this.dependencyTree[classId]) { for (var i = 0; i < this.dependencyTree[classId].length; i++) if (this.cache[this.dependencyTree[classId][i]]) this.invalidate(this.dependencyTree[classId][i]); } }; /** * Invalidates all modified classes in cache */ Resolver.prototype.invalidateModified = function () { for (var i in this.cache) { var _class = this.cache[i]; if (!_class) continue; var stat = void 0; if (fs.existsSync(_class.filename)) stat = fs.statSync(_class.filename); if (!stat || stat.mtime.getTime() != _class.modified) this.invalidate(_class.id); } }; /** * Returns dependency tree */ Resolver.prototype.getDependencyTree = function () { return this.dependencyTree; }; /** * Returns list of cached entities */ Resolver.prototype.getCacheList = function () { return Object.keys(this.cache); }; /** * Returns cached class by ID * * @param classId Class ID */ Resolver.prototype.getCachedClass = function (classId) { return this.cache[classId] || null; }; return Resolver; }()); exports.Resolver = Resolver;