reclass-doc
Version:
Reclass model documentation generator.
470 lines (469 loc) • 17.9 kB
JavaScript
"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;