UNPKG

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