UNPKG

4.72 kBJavaScriptView Raw
1import { contains } from './array.js';
2import { 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
28export 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
55export 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
95export 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
107export 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
120export 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}
130export function isOptionalDependency(dependency) {
131 return dependency && dependency[0] === '?';
132}
133export function stripOptionalNotation(dependency) {
134 return dependency && dependency[0] === '?' ? dependency.slice(1) : dependency;
135}
\No newline at end of file