factory.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  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. export function factory(name, dependencies, create, meta) {
  28. function assertAndCreate(scope) {
  29. // we only pass the requested dependencies to the factory function
  30. // to prevent functions to rely on dependencies that are not explicitly
  31. // requested.
  32. var deps = pickShallow(scope, dependencies.map(stripOptionalNotation));
  33. assertDependencies(name, dependencies, scope);
  34. return create(deps);
  35. }
  36. assertAndCreate.isFactory = true;
  37. assertAndCreate.fn = name;
  38. assertAndCreate.dependencies = dependencies.slice().sort();
  39. if (meta) {
  40. assertAndCreate.meta = meta;
  41. }
  42. return assertAndCreate;
  43. }
  44. /**
  45. * Sort all factories such that when loading in order, the dependencies are resolved.
  46. *
  47. * @param {Array} factories
  48. * @returns {Array} Returns a new array with the sorted factories.
  49. */
  50. export function sortFactories(factories) {
  51. var factoriesByName = {};
  52. factories.forEach(factory => {
  53. factoriesByName[factory.fn] = factory;
  54. });
  55. function containsDependency(factory, dependency) {
  56. // TODO: detect circular references
  57. if (isFactory(factory)) {
  58. if (contains(factory.dependencies, dependency.fn || dependency.name)) {
  59. return true;
  60. }
  61. if (factory.dependencies.some(d => containsDependency(factoriesByName[d], dependency))) {
  62. return true;
  63. }
  64. }
  65. return false;
  66. }
  67. var sorted = [];
  68. function addFactory(factory) {
  69. var index = 0;
  70. while (index < sorted.length && !containsDependency(sorted[index], factory)) {
  71. index++;
  72. }
  73. sorted.splice(index, 0, factory);
  74. }
  75. // sort regular factory functions
  76. factories.filter(isFactory).forEach(addFactory);
  77. // sort legacy factory functions AFTER the regular factory functions
  78. factories.filter(factory => !isFactory(factory)).forEach(addFactory);
  79. return sorted;
  80. }
  81. // TODO: comment or cleanup if unused in the end
  82. export function create(factories) {
  83. var scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  84. sortFactories(factories).forEach(factory => factory(scope));
  85. return scope;
  86. }
  87. /**
  88. * Test whether an object is a factory. This is the case when it has
  89. * properties name, dependencies, and a function create.
  90. * @param {*} obj
  91. * @returns {boolean}
  92. */
  93. export function isFactory(obj) {
  94. return typeof obj === 'function' && typeof obj.fn === 'string' && Array.isArray(obj.dependencies);
  95. }
  96. /**
  97. * Assert that all dependencies of a list with dependencies are available in the provided scope.
  98. *
  99. * Will throw an exception when there are dependencies missing.
  100. *
  101. * @param {string} name Name for the function to be created. Used to generate a useful error message
  102. * @param {string[]} dependencies
  103. * @param {Object} scope
  104. */
  105. export function assertDependencies(name, dependencies, scope) {
  106. var allDefined = dependencies.filter(dependency => !isOptionalDependency(dependency)) // filter optionals
  107. .every(dependency => scope[dependency] !== undefined);
  108. if (!allDefined) {
  109. var missingDependencies = dependencies.filter(dependency => scope[dependency] === undefined);
  110. // TODO: create a custom error class for this, a MathjsError or something like that
  111. throw new Error("Cannot create function \"".concat(name, "\", ") + "some dependencies are missing: ".concat(missingDependencies.map(d => "\"".concat(d, "\"")).join(', '), "."));
  112. }
  113. }
  114. export function isOptionalDependency(dependency) {
  115. return dependency && dependency[0] === '?';
  116. }
  117. export function stripOptionalNotation(dependency) {
  118. return dependency && dependency[0] === '?' ? dependency.slice(1) : dependency;
  119. }