1 | import { contains } from './array'
|
2 | import { 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 | */
|
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 | 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 | */
|
56 | export 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
|
103 | export 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 | */
|
116 | export 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 | */
|
131 | export 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 |
|
145 | export function isOptionalDependency (dependency) {
|
146 | return dependency && dependency[0] === '?'
|
147 | }
|
148 |
|
149 | export function stripOptionalNotation (dependency) {
|
150 | return dependency && dependency[0] === '?'
|
151 | ? dependency.slice(1)
|
152 | : dependency
|
153 | }
|