UNPKG

25.2 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _namespace = require('./namespace');
8
9var _namespace2 = _interopRequireDefault(_namespace);
10
11function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
13_namespace2.default.namespace('ima');
14
15/**
16 * The Object Container is an enhanced dependency injector with support for
17 * aliases and constants, and allowing to reference classes in the application
18 * namespace by specifying their fully qualified names.
19 */
20class ObjectContainer {
21 /**
22 * Returns constant for plugin binding state.
23 *
24 * When the object container is in plugin binding state, it is impossible
25 * to register new aliases using the {@linkcode bind()} method and register
26 * new constant using the {@linkcode constant()} method, or override the
27 * default class dependencies of any already-configured class using the
28 * {@linkcode inject()} method (classes that were not configured yet may be
29 * configured using the {@linkcode inject()} method or {@linkcode provide()}
30 * method).
31 *
32 * This prevents the unpriviledged code (e.g. 3rd party plugins) from
33 * overriding the default dependency configuration provided by ima, or
34 * overriding the configuration of a 3rd party plugin by another 3rd party
35 * plugin.
36 *
37 * The application itself has always access to the unlocked object
38 * container.
39 *
40 * @return {string} The plugin binding state.
41 */
42 static get PLUGIN_BINDING_STATE() {
43 return 'plugin';
44 }
45
46 /**
47 * Returns constant for IMA binding state.
48 *
49 * When the object container is in ima binding state, it is possible
50 * to register new aliases using the {@linkcode bind()} method and register
51 * new constant using the {@linkcode constant()} method, or override the
52 * default class dependencies of any already-configured class using the
53 * {@linkcode inject()} method (classes that were not configured yet may be
54 * configured using the {@linkcode inject()} method or {@linkcode provide()}
55 * method).
56 *
57 * @return {string} The IMA binding state.
58 */
59 static get IMA_BINDING_STATE() {
60 return 'ima';
61 }
62
63 /**
64 * Returns constant for app binding state.
65 *
66 * When the object container is in app binding state, it is possible
67 * to register new aliases using the {@linkcode bind()} method and register
68 * new constant using the {@linkcode constant()} method, or override the
69 * default class dependencies of any already-configured class using the
70 * {@linkcode inject()} method (classes that were not configured yet may be
71 * configured using the {@linkcode inject()} method or {@linkcode provide()}
72 * method).
73 *
74 * @return {string} The app binding state.
75 */
76 static get APP_BINDING_STATE() {
77 return 'app';
78 }
79
80 /**
81 * Initializes the object container.
82 *
83 * @param {ima.Namespace} namespace The namespace container, used to
84 * access classes and values using their fully qualified names.
85 */
86 constructor(namespace) {
87 /**
88 * The namespace container, used to access classes and values using
89 * their fully qualified names.
90 *
91 * @type {ima.Namespace}
92 */
93 this._namespace = namespace;
94
95 /**
96 *
97 * @type {Map<(string|function(new: *, ...*)|function(...*): *), Entry<*>>}
98 */
99 this._entries = new Map();
100
101 /**
102 * The current binding state.
103 *
104 * The {@linkcode setBindingState()} method may be called for changing
105 * object container binding state only by the bootstrap script.
106 *
107 * @type {?string}
108 */
109 this._bindingState = null;
110 }
111
112 /**
113 * Binds the specified class or factory function and dependencies to the
114 * specified alias. Binding a class or factory function to an alias allows
115 * the class or function to be specied as a dependency by specifying the
116 * alias and creating new instances by referring to the class or function
117 * by the alias.
118 *
119 * Also note that the same class or function may be bound to several
120 * aliases and each may use different dependencies.
121 *
122 * The alias will use the default dependencies bound for the class if no
123 * dependencies are provided.
124 *
125 * @template T
126 * @param {string} name Alias name.
127 * @param {(function(new: T, ...*)|function(...*): T)} classConstructor The
128 * class constructor or a factory function.
129 * @param {?*[]} [dependencies] The dependencies to pass into the
130 * constructor or factory function.
131 * @return {ObjectContainer} This object container.
132 */
133 bind(name, classConstructor, dependencies) {
134 if ($Debug) {
135 if (this._bindingState === ObjectContainer.PLUGIN_BINDING_STATE) {
136 throw new Error(`ima.ObjectContainer:bind Object container ` + `is locked. You do not have the permission to ` + `create a new alias named ${name}.`);
137 }
138
139 if (typeof classConstructor !== 'function') {
140 throw new Error(`ima.ObjectContainer:bind The second ` + `argument has to be a class constructor function, ` + `but ${classConstructor} was provided. Fix alias ` + `${name} for your bind.js file.`);
141 }
142 }
143
144 let classConstructorEntry = this._entries.get(classConstructor);
145 let nameEntry = this._entries.get(name);
146 let entry = classConstructorEntry || nameEntry;
147
148 if (classConstructorEntry && !nameEntry && dependencies) {
149 let entry = this._createEntry(classConstructor, dependencies);
150 this._entries.set(name, entry);
151
152 return this;
153 }
154
155 if (entry) {
156 this._entries.set(name, entry);
157
158 if (dependencies) {
159 this._updateEntryValues(entry, classConstructor, dependencies);
160 }
161 } else {
162 let entry = this._createEntry(classConstructor, dependencies);
163 this._entries.set(classConstructor, entry);
164 this._entries.set(name, entry);
165 }
166
167 return this;
168 }
169
170 /**
171 * Defines a new constant registered with this object container. Note that
172 * this is the only way of passing {@code string} values to constructors
173 * because the object container treats strings as class, interface, alias
174 * or constant names.
175 *
176 * @param {string} name The constant name.
177 * @param {*} value The constant value.
178 * @return {ObjectContainer} This object container.
179 */
180 constant(name, value) {
181 if ($Debug) {
182 if (this._entries.has(name) || !!this._getEntryFromConstant(name)) {
183 throw new Error(`ima.ObjectContainer:constant The ${name} ` + `constant has already been declared and cannot be ` + `redefined.`);
184 }
185
186 if (this._bindingState === ObjectContainer.PLUGIN_BINDING_STATE) {
187 throw new Error(`ima.ObjectContainer:constant The ${name} ` + `constant can't be declared in plugin. ` + `The constant must be define in app/config/bind.js file.`);
188 }
189 }
190
191 let constantEntry = this._createEntry(() => value, [], {
192 writeable: false
193 });
194 constantEntry.sharedInstance = value;
195 this._entries.set(name, constantEntry);
196
197 return this;
198 }
199
200 /**
201 * Configures the object loader with the specified default dependencies for
202 * the specified class.
203 *
204 * New instances of the class created by this object container will receive
205 * the provided dependencies into constructor unless custom dependencies
206 * are provided.
207 *
208 * @template T
209 * @param {function(new: T, ...*)} classConstructor The class constructor.
210 * @param {?*[]} dependencies The dependencies to pass into the
211 * constructor function.
212 * @return {ObjectContainer} This object container.
213 */
214 inject(classConstructor, dependencies) {
215 if ($Debug) {
216 if (typeof classConstructor !== 'function') {
217 throw new Error(`ima.ObjectContainer:inject The first ` + `argument has to be a class constructor function, ` + `but ${classConstructor} was provided. Fix your ` + `bind.js file.`);
218 }
219
220 if (this._entries.has(classConstructor) && this._bindingState === ObjectContainer.PLUGIN_BINDING_STATE) {
221 throw new Error(`ima.ObjectContainer:inject The ` + `${classConstructor.name} has already had its ` + `default dependencies configured, and the object ` + `container is currently locked, therefore the ` + `dependency configuration cannot be override. The ` + `dependencies of the provided class must be ` + `overridden from the application's bind.js ` + `configuration file.`);
222 }
223 }
224
225 let classConstructorEntry = this._entries.get(classConstructor);
226 if (classConstructorEntry) {
227 if (dependencies) {
228 this._updateEntryValues(classConstructorEntry, classConstructor, dependencies);
229 }
230 } else {
231 classConstructorEntry = this._createEntry(classConstructor, dependencies);
232 this._entries.set(classConstructor, classConstructorEntry);
233 }
234
235 return this;
236 }
237
238 /**
239 * Configures the default implementation of the specified interface to use
240 * when an implementation provider of the specified interface is requested
241 * from this object container.
242 *
243 * The implementation constructor will obtain the provided default
244 * dependencies or the dependencies provided to the {@codelink create()}
245 * method.
246 *
247 * @template {Interface}
248 * @template {Implementation} extends Interface
249 * @param {function(new: Interface)} interfaceConstructor The constructor
250 * of the interface representing the service.
251 * @param {function(new: Implementation, ...*)} implementationConstructor
252 * The constructor of the class implementing the service interface.
253 * @param {?*[]} dependencies The dependencies to pass into the
254 * constructor function.
255 * @return {ObjectContainer} This object container.
256 */
257 provide(interfaceConstructor, implementationConstructor, dependencies) {
258 if ($Debug) {
259 if (this._entries.has(interfaceConstructor) && this._bindingState === ObjectContainer.PLUGIN_BINDING_STATE) {
260 throw new Error('ima.ObjectContainer:provide The ' + 'implementation of the provided interface ' + `(${interfaceConstructor.name}) has already been ` + `configured and cannot be overridden.`);
261 }
262
263 // check that implementation really extends interface
264 let prototype = implementationConstructor.prototype;
265 if (!(prototype instanceof interfaceConstructor)) {
266 throw new Error('ima.ObjectContainer:provide The specified ' + `class (${implementationConstructor.name}) does not ` + `implement the ${interfaceConstructor.name} ` + `interface.`);
267 }
268 }
269
270 let classConstructorEntry = this._entries.get(implementationConstructor);
271 if (classConstructorEntry) {
272 this._entries.set(interfaceConstructor, classConstructorEntry);
273
274 if (dependencies) {
275 this._updateEntryValues(classConstructorEntry, implementationConstructor, dependencies);
276 }
277 } else {
278 classConstructorEntry = this._createEntry(implementationConstructor, dependencies);
279 this._entries.set(implementationConstructor, classConstructorEntry);
280 this._entries.set(interfaceConstructor, classConstructorEntry);
281 }
282
283 return this;
284 }
285
286 /**
287 * Retrieves the shared instance or value of the specified constant, alias,
288 * class or factory function, interface, or fully qualified namespace path
289 * (the method checks these in this order in case of a name clash).
290 *
291 * The instance or value is created lazily the first time it is requested.
292 *
293 * @template T
294 * @param {(string|function(new: T, ...*)|function(...*): T)} name The name
295 * of the alias, class, interface, or the class, interface or a
296 * factory function.
297 * @return {T} The shared instance or value.
298 */
299 get(name) {
300 let entry = this._getEntry(name);
301
302 if (entry.sharedInstance === null) {
303 entry.sharedInstance = this._createInstanceFromEntry(entry);
304 }
305
306 return entry.sharedInstance;
307 }
308
309 /**
310 * Returns the class constructor function of the specified class.
311 *
312 * @template T
313 * @param {string|function(new: T, ...*)} name The name by which the class
314 * is registered with this object container.
315 * @return {function(new: T, ...*)} The constructor function.
316 */
317 getConstructorOf(name) {
318 let entry = this._getEntry(name);
319
320 return entry.classConstructor;
321 }
322
323 /**
324 * Returns {@code true} if the specified object, class or resource is
325 * registered with this object container.
326 *
327 * @template T
328 * @param {string|function(new: T, ...*)} name The resource name.
329 * @return {boolean} {@code true} if the specified object, class or
330 * resource is registered with this object container.
331 */
332 has(name) {
333 return this._entries.has(name) || !!this._getEntryFromConstant(name) || !!this._getEntryFromNamespace(name) || !!this._getEntryFromClassConstructor(name);
334 }
335
336 /**
337 * Creates a new instance of the class or retrieves the value generated by
338 * the factory function identified by the provided name, class, interface,
339 * or factory function, passing in the provided dependencies.
340 *
341 * The method uses the dependencies specified when the class, interface or
342 * factory function has been registered with the object container if no
343 * custom dependencies are provided.
344 *
345 * @template T
346 * @param {(string|function(new: T, ...*)|function(...*): T)} name The name
347 * of the alias, class, interface, or the class, interface or a
348 * factory function to use.
349 * @param {?*[]} dependencies The dependencies to pass into the
350 * constructor or factory function.
351 * @return {T} Created instance or generated value.
352 */
353 create(name, dependencies) {
354 let entry = this._getEntry(name);
355
356 return this._createInstanceFromEntry(entry, dependencies);
357 }
358
359 /**
360 * Clears all entries from this object container and resets the locking
361 * mechanism of this object container.
362 *
363 * @return {ObjectContainer} This object container.
364 */
365 clear() {
366 this._entries.clear();
367 this._bindingState = null;
368
369 return this;
370 }
371
372 /**
373 *
374 * @param {?string} bindingState
375 */
376 setBindingState(bindingState) {
377 if (this._bindingState === ObjectContainer.APP_BINDING_STATE) {
378 throw new Error(`ima.ObjectContainer:setBindingState The setBindingState() ` + `method has to be called only by the bootstrap script. Other ` + `calls are not allowed.`);
379 }
380
381 this._bindingState = bindingState;
382 }
383
384 /**
385 * Retrieves the entry for the specified constant, alias, class or factory
386 * function, interface, or fully qualified namespace path (the method
387 * checks these in this order in case of a name clash).
388 *
389 * The method retrieves an existing entry even if a qualified namespace
390 * path is provided (if the target class or interface has been configured
391 * in this object container).
392 *
393 * The method throws an {@codelink Error} if no such constant, alias,
394 * registry, interface implementation is known to this object container and
395 * the provided identifier is not a valid namespace path specifying an
396 * existing class, interface or value.
397 *
398 * @template T
399 * @param {string|function(new: T, ...*)} name Name of a constant or alias,
400 * factory function, class or interface constructor, or a fully
401 * qualified namespace path.
402 * @return {?Entry<T>} The retrieved entry.
403 * @throws {Error} If no such constant, alias, registry, interface
404 * implementation is known to this object container.
405 */
406 _getEntry(name) {
407 let entry = this._entries.get(name) || this._getEntryFromConstant(name) || this._getEntryFromNamespace(name) || this._getEntryFromClassConstructor(name);
408
409 if ($Debug) {
410 if (!entry) {
411 throw new Error(`ima.ObjectContainer:_getEntry There is no constant, ` + `alias, registered class, registered interface with ` + `configured implementation or namespace entry ` + `identified as ${name}. Check your bind.js file for ` + `typos or register ${name} with the object container.`);
412 }
413 }
414
415 return entry;
416 }
417
418 /**
419 * The method update classConstructor and dependencies for defined entry.
420 * The entry throw Error for constants and if you try override dependencies
421 * more than once.
422 *
423 * @template T
424 * @param {(function(new: T, ...*)|function(...*): T)} classConstructor The
425 * class constructor or factory function.
426 * @param {Entry} entry The entry representing the class that should
427 * have its instance created or factory faction to use to create a
428 * value.
429 * @param {*[]} dependencies The dependencies to pass into the
430 * constructor or factory function.
431 */
432 _updateEntryValues(entry, classConstructor, dependencies) {
433 entry.classConstructor = classConstructor;
434 entry.dependencies = dependencies;
435 }
436
437 /**
438 * Creates a new entry for the provided class or factory function, the
439 * provided dependencies and entry options.
440 *
441 * @template T
442 * @param {(function(new: T, ...*)|function(...*): T)} classConstructor The
443 * class constructor or factory function.
444 * @param {?*[]} [dependencies] The dependencies to pass into the
445 * constructor or factory function.
446 * @param {{ writeable: boolean }} options
447 * @return {T} Created instance or generated value.
448 */
449 _createEntry(classConstructor, dependencies, options) {
450 if ((!dependencies || dependencies.length === 0) && Array.isArray(classConstructor.$dependencies)) {
451 dependencies = classConstructor.$dependencies;
452 }
453
454 return new Entry(classConstructor, dependencies, options);
455 }
456
457 /**
458 * Creates a new instance of the class or retrieves the value generated by
459 * the factory function represented by the provided entry, passing in the
460 * provided dependencies.
461 *
462 * The method uses the dependencies specified by the entry if no custom
463 * dependencies are provided.
464 *
465 * @template T
466 * @param {Entry<T>} entry The entry representing the class that should
467 * have its instance created or factory faction to use to create a
468 * value.
469 * @param {*[]} [dependencies=[]] The dependencies to pass into the
470 * constructor or factory function.
471 * @return {T} Created instance or generated value.
472 */
473 _createInstanceFromEntry(entry, dependencies = []) {
474 if (dependencies.length === 0) {
475 dependencies = [];
476
477 for (let dependency of entry.dependencies) {
478 if (['function', 'string'].indexOf(typeof dependency) > -1) {
479 dependencies.push(this.get(dependency));
480 } else {
481 dependencies.push(dependency);
482 }
483 }
484 }
485 let constructor = entry.classConstructor;
486
487 return new constructor(...dependencies);
488 }
489
490 /**
491 * Retrieves the constant value denoted by the provided fully qualified
492 * composition name.
493 *
494 * The method returns the entry for the constant if the constant is registered
495 * with this object container, otherwise return {@code null}.
496 *
497 * Finally, if the constant composition name does not resolve to value,
498 * the method return {@code null}.
499 *
500 * @param {string} compositionName
501 * @return {?Entry<*>} An entry representing the value at the specified
502 * composition name in the constants. The method returns {@code null}
503 * if the specified composition name does not exist in the constants.
504 */
505 _getEntryFromConstant(compositionName) {
506 //TODO entries must be
507 if (typeof compositionName !== 'string') {
508 return null;
509 }
510
511 let objectProperties = compositionName.split('.');
512 let constantValue = this._entries.has(objectProperties[0]) ? this._entries.get(objectProperties[0]).sharedInstance : null;
513
514 let pathLength = objectProperties.length;
515 for (let i = 1; i < pathLength && constantValue; i++) {
516 constantValue = constantValue[objectProperties[i]];
517 }
518
519 if (constantValue !== undefined && constantValue !== null) {
520 let entry = this._createEntry(() => constantValue, [], {
521 writeable: false
522 });
523 entry.sharedInstance = constantValue;
524
525 return entry;
526 }
527
528 return null;
529 }
530
531 /**
532 * Retrieves the class denoted by the provided fully qualified name within
533 * the application namespace.
534 *
535 * The method then checks whether there are dependecies configured for the
536 * class, no matter whether the class is an implementation class or an
537 * "interface" class.
538 *
539 * The method returns the entry for the class if the class is registered
540 * with this object container, otherwise an unregistered entry is created
541 * and returned.
542 *
543 * Finally, if the namespace path does not resolve to a class, the method
544 * return an unregistered entry resolved to the value denoted by the
545 * namespace path.
546 *
547 * Alternatively, if a constructor function is passed in instead of a
548 * namespace path, the method returns {@code null}.
549 *
550 * @template T
551 * @param {(string|function(new: T, ...*))} path Namespace path pointing to
552 * a class or a value in the application namespace, or a constructor
553 * function.
554 * @return {?Entry<T>} An entry representing the value or class at the
555 * specified path in the namespace. The method returns {@code null}
556 * if the specified path does not exist in the namespace.
557 */
558 _getEntryFromNamespace(path) {
559 if (typeof path !== 'string' || !this._namespace.has(path)) {
560 return null;
561 }
562
563 let namespaceValue = this._namespace.get(path);
564
565 if (typeof namespaceValue === 'function') {
566 if (this._entries.has(namespaceValue)) {
567 return this._entries.get(namespaceValue);
568 }
569
570 return this._createEntry(namespaceValue);
571 }
572
573 let entry = this._createEntry(() => namespaceValue);
574 entry.sharedInstance = namespaceValue;
575 return entry;
576 }
577
578 /**
579 * Retrieves the class denoted by the provided class constructor.
580 *
581 * The method then checks whether there are defined {@code $dependecies}
582 * property for class. Then the class is registered to this object
583 * container.
584 *
585 * The method returns the entry for the class if the specified class
586 * does not have defined {@code $dependencies} property return
587 * {@code null}.
588 *
589 * @template T
590 * @param {function(new: T, ...*)} classConstructor
591 * @return {?Entry<T>} An entry representing the value at the specified
592 * classConstructor. The method returns {@code null}
593 * if the specified classConstructor does not have defined
594 * {@code $dependencies}.
595 */
596 _getEntryFromClassConstructor(classConstructor) {
597 if (typeof classConstructor === 'function' && Array.isArray(classConstructor.$dependencies)) {
598 let entry = this._createEntry(classConstructor, classConstructor.$dependencies);
599 this._entries.set(classConstructor, entry);
600
601 return entry;
602 }
603
604 return null;
605 }
606}
607
608exports.default = ObjectContainer;
609_namespace2.default.ima.ObjectContainer = ObjectContainer;
610
611/**
612 * Object container entry, representing either a class, interface, constant or
613 * an alias.
614 *
615 * @template T
616 */
617class Entry {
618 /**
619 * Initializes the entry.
620 *
621 * @param {(function(new: T, ...*)|function(...*): T)} classConstructor The
622 * class constructor or constant value getter.
623 * @param {*[]} [dependencies=[]] The dependencies to pass into the
624 * constructor function.
625 * @param {?{ writeable: boolean }} [options] The Entry options.
626 */
627 constructor(classConstructor, dependencies, options) {
628 /**
629 * The constructor of the class represented by this entry, or the
630 * getter of the value of the constant represented by this entry.
631 *
632 * @type {(function(new: T, ...*)|function(...*): T)}
633 */
634 this.classConstructor = classConstructor;
635
636 /**
637 * The shared instance of the class represented by this entry.
638 *
639 * @type {T}
640 */
641 this.sharedInstance = null;
642
643 /**
644 * The Entry options.
645 *
646 * @type {{ writeable: boolean }}
647 */
648 this._options = options || {
649 writeable: true
650 };
651
652 /**
653 * Dependencies of the class constructor of the class represented by
654 * this entry.
655 *
656 * @type {*[]}
657 */
658 this._dependencies = dependencies || [];
659
660 /**
661 * The override counter
662 *
663 * @type {number}
664 */
665 this._overrideCounter = 0;
666 }
667
668 set dependencies(dependencies) {
669 if ($Debug) {
670 if (!this.writeable) {
671 throw new Error(`The entry is constant and you ` + `can't redefined their dependencies ${dependencies}.`);
672 }
673
674 if (this._overrideCounter >= 1) {
675 throw new Error(`The dependencies entry can't be overrided more than once.` + `Fix your bind.js file for classConstructor ${this.classConstructor.name}.`);
676 }
677 }
678
679 this._dependencies = dependencies;
680 this._overrideCounter++;
681 }
682
683 get dependencies() {
684 return this._dependencies;
685 }
686
687 get writeable() {
688 return this._options.writeable;
689 }
690}
691
692typeof $IMA !== 'undefined' && $IMA !== null && $IMA.Loader && $IMA.Loader.register('ima/ObjectContainer', [], function (_export, _context) {
693 'use strict';
694 return {
695 setters: [],
696 execute: function () {
697 _export('default', exports.default);
698 }
699 };
700});