simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

factory.js (4720B)


      1 import { contains } from './array.js';
      2 import { pickShallow } from './object.js';
      3 /**
      4  * Create a factory function, which can be used to inject dependencies.
      5  *
      6  * The created functions are memoized, a consecutive call of the factory
      7  * with the exact same inputs will return the same function instance.
      8  * The memoized cache is exposed on `factory.cache` and can be cleared
      9  * if needed.
     10  *
     11  * Example:
     12  *
     13  *     const name = 'log'
     14  *     const dependencies = ['config', 'typed', 'divideScalar', 'Complex']
     15  *
     16  *     export const createLog = factory(name, dependencies, ({ typed, config, divideScalar, Complex }) => {
     17  *       // ... create the function log here and return it
     18  *     }
     19  *
     20  * @param {string} name           Name of the function to be created
     21  * @param {string[]} dependencies The names of all required dependencies
     22  * @param {function} create       Callback function called with an object with all dependencies
     23  * @param {Object} [meta]         Optional object with meta information that will be attached
     24  *                                to the created factory function as property `meta`.
     25  * @returns {function}
     26  */
     27 
     28 export function factory(name, dependencies, create, meta) {
     29   function assertAndCreate(scope) {
     30     // we only pass the requested dependencies to the factory function
     31     // to prevent functions to rely on dependencies that are not explicitly
     32     // requested.
     33     var deps = pickShallow(scope, dependencies.map(stripOptionalNotation));
     34     assertDependencies(name, dependencies, scope);
     35     return create(deps);
     36   }
     37 
     38   assertAndCreate.isFactory = true;
     39   assertAndCreate.fn = name;
     40   assertAndCreate.dependencies = dependencies.slice().sort();
     41 
     42   if (meta) {
     43     assertAndCreate.meta = meta;
     44   }
     45 
     46   return assertAndCreate;
     47 }
     48 /**
     49  * Sort all factories such that when loading in order, the dependencies are resolved.
     50  *
     51  * @param {Array} factories
     52  * @returns {Array} Returns a new array with the sorted factories.
     53  */
     54 
     55 export function sortFactories(factories) {
     56   var factoriesByName = {};
     57   factories.forEach(factory => {
     58     factoriesByName[factory.fn] = factory;
     59   });
     60 
     61   function containsDependency(factory, dependency) {
     62     // TODO: detect circular references
     63     if (isFactory(factory)) {
     64       if (contains(factory.dependencies, dependency.fn || dependency.name)) {
     65         return true;
     66       }
     67 
     68       if (factory.dependencies.some(d => containsDependency(factoriesByName[d], dependency))) {
     69         return true;
     70       }
     71     }
     72 
     73     return false;
     74   }
     75 
     76   var sorted = [];
     77 
     78   function addFactory(factory) {
     79     var index = 0;
     80 
     81     while (index < sorted.length && !containsDependency(sorted[index], factory)) {
     82       index++;
     83     }
     84 
     85     sorted.splice(index, 0, factory);
     86   } // sort regular factory functions
     87 
     88 
     89   factories.filter(isFactory).forEach(addFactory); // sort legacy factory functions AFTER the regular factory functions
     90 
     91   factories.filter(factory => !isFactory(factory)).forEach(addFactory);
     92   return sorted;
     93 } // TODO: comment or cleanup if unused in the end
     94 
     95 export function create(factories) {
     96   var scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
     97   sortFactories(factories).forEach(factory => factory(scope));
     98   return scope;
     99 }
    100 /**
    101  * Test whether an object is a factory. This is the case when it has
    102  * properties name, dependencies, and a function create.
    103  * @param {*} obj
    104  * @returns {boolean}
    105  */
    106 
    107 export function isFactory(obj) {
    108   return typeof obj === 'function' && typeof obj.fn === 'string' && Array.isArray(obj.dependencies);
    109 }
    110 /**
    111  * Assert that all dependencies of a list with dependencies are available in the provided scope.
    112  *
    113  * Will throw an exception when there are dependencies missing.
    114  *
    115  * @param {string} name   Name for the function to be created. Used to generate a useful error message
    116  * @param {string[]} dependencies
    117  * @param {Object} scope
    118  */
    119 
    120 export function assertDependencies(name, dependencies, scope) {
    121   var allDefined = dependencies.filter(dependency => !isOptionalDependency(dependency)) // filter optionals
    122   .every(dependency => scope[dependency] !== undefined);
    123 
    124   if (!allDefined) {
    125     var missingDependencies = dependencies.filter(dependency => scope[dependency] === undefined); // TODO: create a custom error class for this, a MathjsError or something like that
    126 
    127     throw new Error("Cannot create function \"".concat(name, "\", ") + "some dependencies are missing: ".concat(missingDependencies.map(d => "\"".concat(d, "\"")).join(', '), "."));
    128   }
    129 }
    130 export function isOptionalDependency(dependency) {
    131   return dependency && dependency[0] === '?';
    132 }
    133 export function stripOptionalNotation(dependency) {
    134   return dependency && dependency[0] === '?' ? dependency.slice(1) : dependency;
    135 }