var _ = require("./lodash-wrapper.js");
var DepGraph = require("dependency-graph").DepGraph;
/** @todo Mutator and Generator should be derived from a common class... too much cut-and-paste code right now */
class MorGBase extends Function {
constructor(name, fn, opts) {
super();
// check args
if (typeof name !== "string") {
throw new TypeError("Mutator constructor expected 'name' to be string, got:" + name);
}
if (typeof fn !== "function") {
throw new TypeError("Mutator constructor expected 'fn' to be function, got:" + typeof fn);
}
opts = opts || {};
this.tags = [];
this.fn = fn;
// can't assign to read-only 'name'...
// ...but we can sure redefine it!
Object.defineProperty(this, "name", {
enumerable: false,
configurable: true,
writable: false,
value: name
});
// if we are called, turn around and call fn
return new Proxy(this, {
apply: (target, thisArg, argumentsList) => {
var ctx = new MandGTypeManager().getContext();
// console.log ("inserting ctx:", ctx);
this.fn.apply(ctx, argumentsList);
}
});
}
fn(thing) {
throw new Error(`Mutating ${thing} in ${this.name}: mutator not implemented`);
}
}
/**
* Generic Mutator class
*/
class Mutator extends MorGBase {
constructor(name, fn, opts) {
// check args
if (fn.length !== 1) {
throw new TypeError("Mutator constructor expected 'fn' to have one arg, has: " + fn.length);
}
super(name, fn, opts);
}
}
/**
* Generic Generator class
*/
class Generator extends MorGBase {
constructor(name, fn, opts) {
// check args
if (fn.length !== 0) {
throw new TypeError("Generator constructor expected 'fn' to have zero args, has: " + fn.length);
}
super(name, fn, opts);
}
}
/**
* Mutators and Generators (MandG) container class
* has all the logic around the specific mutators and generators for a specific type
*/
class MandG {
constructor(type, check, opts) {
if (typeof type !== "string") {
throw new TypeError("MandG constructor expected type, got: " + type);
}
if (typeof check !== "function") {
throw new TypeError("MandG constructor expected 'check' to be a function, got: " + typeof check);
}
// set default options
opts = opts || {};
var defaults = {};
_.defaultsDeep(opts, defaults);
if (!Array.isArray(opts.depends) && opts.depends !== undefined) {
throw new TypeError("Expected dependencies to be array, got: " + typeof opts.depends);
}
if (typeof opts.parent !== "string" && opts.parent !== undefined) {
throw new TypeError("Expected parent to be string, got: " + typeof opts.parent);
}
this.depends = opts.depends || [];
this.parent = opts.parent || undefined;
// configure options
this.type = type;
this.name = type;
this.check = check;
// for future use
// TODO: use Maps? using _.sample() later may not be map friendly
this.mutator = {};
this.generator = {};
this.subtype = {};
}
check(thing) {
throw new Error(`Adding ${thing}: check not implemented`);
}
addMutator(m) {
// if mutator is a function, convert it to the mutator class
if (typeof m === "function" && !(m instanceof Mutator)) {
if (!m.name) {
throw new TypeError("attempting to add Mutator from function without a name");
}
m = new Mutator(m.name, m);
}
// check typeof Mutator
if (!(m instanceof Mutator)) {
throw new TypeError("addMutator expected type Mutator, got: " + typeof m);
}
// TODO: check to make sure mutator doesn't already exist?
var name = m.name;
this.mutator[name] = m;
}
addGenerator(g) {
// if generator is a function, convert it to the generator class
if (typeof g === "function" && !(g instanceof Generator)) {
if (!g.name) {
throw new TypeError("attempting to add Generator from function without a name");
}
g = new Generator(g.name, g);
}
// check typeof Generator
if (!(g instanceof Generator)) {
throw new TypeError("addGenerator expected type Generator, got: " + typeof g);
}
// TODO: check to make sure generator doesn't already exist?
var name = g.name;
this.generator[name] = g;
}
/**
* Reuses a generator that has already been implemented by another module
*/
borrowGenerator(module, name) {
// add module as dependency
// add string as "function" in module list
// proxy should resolve string to function later...
}
/**
* Reuses a mutator that has already been implemented by another module
*/
borrowMutator(module, name) {
// add module as dependency
// add string as "function" in module list
// proxy should resolve string to function later...
}
addSubtype(s) {
// check typeof Subtype
if (!(s instanceof MandG)) {
throw new TypeError("addSubtype expected type MandG, got: " + typeof s);
}
if (s === this) {
throw new TypeError("attempting to add MandG to itself: recursion not allowed");
}
// TODO: check to make sure type doesn't already exist
var name = s.name;
if (this.subtype[name] !== undefined) {
throw new TypeError("trying to add duplicate subtype: " + name);
}
this.subtype[name] = s;
}
/**
* filters mutators and generators based on a specific tag
* @todo not implemented
*/
filter(tag) {
throw new Error ("MandG filter not implemented");
}
}
/**
* A singleton for managing all MandG types
*/
var mandgTypeManagerSingleton;
class MandGTypeManager {
constructor(opts) {
// class is a singleton
if (mandgTypeManagerSingleton) return mandgTypeManagerSingleton;
mandgTypeManagerSingleton = this;
// set default options
opts = opts || {};
var defaults = {
mandgModuleList: ["array", "boolean", "date", "function", "null", "number", "object", "regexp", "string", "undef"]
};
_.defaultsDeep(opts, defaults);
// the context for all MandG function calls
this.callContext = {};
// a flat list of all types
this.typeIndex = {};
// root of the heirarchy of types
this.root = new MandG("everything", function() {
return true;
});
this.typeIndex.everything = this.root;
// load all the built-in types
if (opts.mandgReplacementModules) {
this.mandgModuleList = opts.mandgReplacementModules;
} else {
this.mandgModuleList = opts.mandgModuleList;
}
var mandg;
var dependencyGraph = new DepGraph();
for (let idx in this.mandgModuleList) {
mandg = null;
// try to load a module that is available in `node_modules` or similar
try {
mandg = require(this.mandgModuleList[idx]);
} catch (err) {}
// try to load the module out of our local types directory
if (!mandg) try {
mandg = require(`./types/${this.mandgModuleList[idx]}.js`);
} catch (err) {}
// if (!(mandg instanceof MandG)) { // XXX TODO for some reason this is broken
if (!mandg) {
throw TypeError("Error loading mandg module: " + this.mandgModuleList[idx]);
}
// add the node to the dependency graph
// note that all nodes have to exist before dependencies can be added
dependencyGraph.addNode(mandg.type, mandg);
}
// map out dependencies
// TODO: save this order? replace module list?
var list = dependencyGraph.overallOrder();
for (let node of list) {
mandg = dependencyGraph.getNodeData(node);
if (typeof mandg.parent !== "string" && mandg.parent !== undefined) {
throw new TypeError (`Expected parent to be string when building dependencies for ${mandg.type}. Got: ${typeof mandg.parent}`);
}
// try to add parent as a dependency
if (mandg.parent) {
try {
dependencyGraph.addDependency(mandg.type, mandg.parent);
} catch (err) {
throw new TypeError(`parent '${mandg.parent}' not found when loading '${mandg.type}'`);
}
}
// try to add all depends as dependencies
for (let depName of mandg.depends) {
try {
dependencyGraph.addDependency(mandg.type, depName);
} catch (err) {
throw new TypeError(`dependency '${depName}' not found when loading '${mandg.type}'`);
}
}
}
for (let node of dependencyGraph.overallOrder()) {
mandg = dependencyGraph.getNodeData(node);
this.registerType(mandg);
}
this.addToContext ("utils", this.typeIndex);
}
/**
* Kills the singleton so that a new one will be created.
* Mostly useful for testing, but maybe other things too.
*/
forceReset() {
// console.log ("RESETTING MANDGTYPEMANAGER");
mandgTypeManagerSingleton = null;
}
/**
* registers a new mandg type
* @param newType {MandG} - the new MandG type to register
*/
registerType(newType) {
if (!(newType instanceof MandG)) {
throw new TypeError("expected 'newType' to be of type MandG, was: " + typeof mandg);
}
var name = newType.type;
var parent = newType.parent || "everything";
if (this.typeIndex[name] !== undefined) {
throw new TypeError("can't register type twice: " + name);
}
this.typeIndex[name] = newType;
var parentType = this.typeIndex[parent];
if (parentType === undefined) {
throw new TypeError(`registerType couldn't find parent '${parent}' when adding type ${newType.type}`);
}
parentType.addSubtype(newType);
newType.parent = parentType;
// console.log (`Added ${newType.type} to ${parentType.type}`)
}
/**
* Turns a thing (string, object, null, etc.) into a mandg type
* @param thing {any} - the thing to find the type of, using the registered types
*/
resolveType(thing) {
// recursively see if some 'type' in 'typeList' returns true when checking 'obj'
// if it does, that's our type... check the subtypes to make sure there's not something
// more specific
function findType(typeList, obj) {
var mandgList = _.map(typeList, function(m, t) {
if (m === undefined || t === undefined) return;
// console.log ("typeof m:", typeof m);
// console.log ("m:", m);
if (m.check(obj)) {
return findType(m.subtype, obj) || m;
}
});
// type = _.flattenDeep (_.remove(type, _.isUndefined));
var mandg;
_.remove(mandgList, _.isUndefined);
if (mandgList.length > 1) {
throw new Error("found too many matching types");
}
if (mandgList.length === 0) mandg = undefined;
if (Array.isArray(mandgList)) {
mandg = _.flattenDeep(mandgList)[0];
}
return mandg;
}
return findType(this.root.subtype, thing);
}
/**
* Returns a context that every function should be called with
*/
getContext() {
return this.callContext;
}
/**
* Adds an item to the calling context for all MandG function calls
* @param prop {string} - the new property in the context
* @param val {any} - the value to assign to the property in the context
*/
addToContext(prop, val) {
Object.defineProperty(this.callContext, prop, {
writeable: false,
configurable: true,
enumerable: true,
value: val
});
// this.callContext[prop] = val;
// TODO: configure as read only?
}
}
module.exports = {
// classes
MandGTypeManager: MandGTypeManager,
MandG: MandG,
Mutator: Mutator,
Generator: Generator
};