ima
Version:
IMA.js framework for isomorphic javascript application
772 lines (648 loc) • 26.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _namespace = require("./namespace");
var _namespace2 = _interopRequireDefault(_namespace);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
_namespace2.default.namespace('ima');
/**
* The Object Container is an enhanced dependency injector with support for
* aliases and constants, and allowing to reference classes in the application
* namespace by specifying their fully qualified names.
*/
class ObjectContainer {
/**
* Returns constant for plugin binding state.
*
* When the object container is in plugin binding state, it is impossible
* to register new aliases using the {@linkcode bind()} method and register
* new constant using the {@linkcode constant()} method, or override the
* default class dependencies of any already-configured class using the
* {@linkcode inject()} method (classes that were not configured yet may be
* configured using the {@linkcode inject()} method or {@linkcode provide()}
* method).
*
* This prevents the unpriviledged code (e.g. 3rd party plugins) from
* overriding the default dependency configuration provided by ima, or
* overriding the configuration of a 3rd party plugin by another 3rd party
* plugin.
*
* The application itself has always access to the unlocked object
* container.
*
* @return {string} The plugin binding state.
*/
static get PLUGIN_BINDING_STATE() {
return 'plugin';
}
/**
* Returns constant for IMA binding state.
*
* When the object container is in ima binding state, it is possible
* to register new aliases using the {@linkcode bind()} method and register
* new constant using the {@linkcode constant()} method, or override the
* default class dependencies of any already-configured class using the
* {@linkcode inject()} method (classes that were not configured yet may be
* configured using the {@linkcode inject()} method or {@linkcode provide()}
* method).
*
* @return {string} The IMA binding state.
*/
static get IMA_BINDING_STATE() {
return 'ima';
}
/**
* Returns constant for app binding state.
*
* When the object container is in app binding state, it is possible
* to register new aliases using the {@linkcode bind()} method and register
* new constant using the {@linkcode constant()} method, or override the
* default class dependencies of any already-configured class using the
* {@linkcode inject()} method (classes that were not configured yet may be
* configured using the {@linkcode inject()} method or {@linkcode provide()}
* method).
*
* @return {string} The app binding state.
*/
static get APP_BINDING_STATE() {
return 'app';
}
/**
* Initializes the object container.
*
* @param {ima.Namespace} namespace The namespace container, used to
* access classes and values using their fully qualified names.
*/
constructor(namespace) {
/**
* The namespace container, used to access classes and values using
* their fully qualified names.
*
* @type {ima.Namespace}
*/
this._namespace = namespace;
/**
*
* @type {Map<(string|function(new: *, ...*)|function(...*): *), Entry<*>>}
*/
this._entries = new Map();
/**
* The current binding state.
*
* The {@linkcode setBindingState()} method may be called for changing
* object container binding state only by the bootstrap script.
*
* @type {?string}
*/
this._bindingState = null;
/**
* The current plugin binding to OC.
*
* The {@linkcode setBindingState()} method may be called for changing
* object container binding state only by the bootstrap script.
*
* @type {?string}
*/
this._bindingPlugin = null;
}
/**
* Binds the specified class or factory function and dependencies to the
* specified alias. Binding a class or factory function to an alias allows
* the class or function to be specied as a dependency by specifying the
* alias and creating new instances by referring to the class or function
* by the alias.
*
* Also note that the same class or function may be bound to several
* aliases and each may use different dependencies.
*
* The alias will use the default dependencies bound for the class if no
* dependencies are provided.
*
* @template T
* @param {string} name Alias name.
* @param {(function(new: T, ...*)|function(...*): T)} classConstructor The
* class constructor or a factory function.
* @param {?*[]} [dependencies] The dependencies to pass into the
* constructor or factory function.
* @return {ObjectContainer} This object container.
*/
bind(name, classConstructor, dependencies) {
if ($Debug) {
if (this._bindingState === ObjectContainer.PLUGIN_BINDING_STATE) {
throw new Error(`ima.ObjectContainer:bind Object container ` + `is locked. You do not have the permission to ` + `create a new alias named ${name}.`);
}
if (typeof classConstructor !== 'function') {
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.`);
}
}
let classConstructorEntry = this._entries.get(classConstructor);
let nameEntry = this._entries.get(name);
let entry = classConstructorEntry || nameEntry;
if (classConstructorEntry && !nameEntry && dependencies) {
let entry = this._createEntry(classConstructor, dependencies);
this._entries.set(name, entry);
return this;
}
if (entry) {
this._entries.set(name, entry);
if (dependencies) {
this._updateEntryValues(entry, classConstructor, dependencies);
}
} else {
let entry = this._createEntry(classConstructor, dependencies);
this._entries.set(classConstructor, entry);
this._entries.set(name, entry);
}
return this;
}
/**
* Defines a new constant registered with this object container. Note that
* this is the only way of passing {@code string} values to constructors
* because the object container treats strings as class, interface, alias
* or constant names.
*
* @param {string} name The constant name.
* @param {*} value The constant value.
* @return {ObjectContainer} This object container.
*/
constant(name, value) {
if ($Debug) {
if (this._entries.has(name) || !!this._getEntryFromConstant(name)) {
throw new Error(`ima.ObjectContainer:constant The ${name} ` + `constant has already been declared and cannot be ` + `redefined.`);
}
if (this._bindingState === ObjectContainer.PLUGIN_BINDING_STATE) {
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.`);
}
}
let constantEntry = this._createEntry(() => value, [], {
writeable: false
});
constantEntry.sharedInstance = value;
this._entries.set(name, constantEntry);
return this;
}
/**
* Configures the object loader with the specified default dependencies for
* the specified class.
*
* New instances of the class created by this object container will receive
* the provided dependencies into constructor unless custom dependencies
* are provided.
*
* @template T
* @param {function(new: T, ...*)} classConstructor The class constructor.
* @param {?*[]} dependencies The dependencies to pass into the
* constructor function.
* @return {ObjectContainer} This object container.
*/
inject(classConstructor, dependencies) {
if ($Debug) {
if (typeof classConstructor !== 'function') {
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.`);
}
if (this._entries.has(classConstructor) && this._bindingState === ObjectContainer.PLUGIN_BINDING_STATE) {
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.`);
}
}
let classConstructorEntry = this._entries.get(classConstructor);
if (classConstructorEntry) {
if (dependencies) {
this._updateEntryValues(classConstructorEntry, classConstructor, dependencies);
}
} else {
classConstructorEntry = this._createEntry(classConstructor, dependencies);
this._entries.set(classConstructor, classConstructorEntry);
}
return this;
}
/**
* Configures the default implementation of the specified interface to use
* when an implementation provider of the specified interface is requested
* from this object container.
*
* The implementation constructor will obtain the provided default
* dependencies or the dependencies provided to the {@codelink create()}
* method.
*
* @template {Interface}
* @template {Implementation} extends Interface
* @param {function(new: Interface)} interfaceConstructor The constructor
* of the interface representing the service.
* @param {function(new: Implementation, ...*)} implementationConstructor
* The constructor of the class implementing the service interface.
* @param {?*[]} dependencies The dependencies to pass into the
* constructor function.
* @return {ObjectContainer} This object container.
*/
provide(interfaceConstructor, implementationConstructor, dependencies) {
if ($Debug) {
if (this._entries.has(interfaceConstructor) && this._bindingState === ObjectContainer.PLUGIN_BINDING_STATE) {
throw new Error('ima.ObjectContainer:provide The ' + 'implementation of the provided interface ' + `(${interfaceConstructor.name}) has already been ` + `configured and cannot be overridden.`);
} // check that implementation really extends interface
let prototype = implementationConstructor.prototype;
if (!(prototype instanceof interfaceConstructor)) {
throw new Error('ima.ObjectContainer:provide The specified ' + `class (${implementationConstructor.name}) does not ` + `implement the ${interfaceConstructor.name} ` + `interface.`);
}
}
let classConstructorEntry = this._entries.get(implementationConstructor);
if (classConstructorEntry) {
this._entries.set(interfaceConstructor, classConstructorEntry);
if (dependencies) {
this._updateEntryValues(classConstructorEntry, implementationConstructor, dependencies);
}
} else {
classConstructorEntry = this._createEntry(implementationConstructor, dependencies);
this._entries.set(implementationConstructor, classConstructorEntry);
this._entries.set(interfaceConstructor, classConstructorEntry);
}
return this;
}
/**
* Retrieves the shared instance or value of the specified constant, alias,
* class or factory function, interface, or fully qualified namespace path
* (the method checks these in this order in case of a name clash).
*
* The instance or value is created lazily the first time it is requested.
*
* @template T
* @param {(string|function(new: T, ...*)|function(...*): T)} name The name
* of the alias, class, interface, or the class, interface or a
* factory function.
* @return {T} The shared instance or value.
*/
get(name) {
let entry = this._getEntry(name);
if (entry.sharedInstance === null) {
entry.sharedInstance = this._createInstanceFromEntry(entry);
}
return entry.sharedInstance;
}
/**
* Returns the class constructor function of the specified class.
*
* @template T
* @param {string|function(new: T, ...*)} name The name by which the class
* is registered with this object container.
* @return {function(new: T, ...*)} The constructor function.
*/
getConstructorOf(name) {
let entry = this._getEntry(name);
return entry.classConstructor;
}
/**
* Returns {@code true} if the specified object, class or resource is
* registered with this object container.
*
* @template T
* @param {string|function(new: T, ...*)} name The resource name.
* @return {boolean} {@code true} if the specified object, class or
* resource is registered with this object container.
*/
has(name) {
return this._entries.has(name) || !!this._getEntryFromConstant(name) || !!this._getEntryFromNamespace(name) || !!this._getEntryFromClassConstructor(name);
}
/**
* Creates a new instance of the class or retrieves the value generated by
* the factory function identified by the provided name, class, interface,
* or factory function, passing in the provided dependencies.
*
* The method uses the dependencies specified when the class, interface or
* factory function has been registered with the object container if no
* custom dependencies are provided.
*
* @template T
* @param {(string|function(new: T, ...*)|function(...*): T)} name The name
* of the alias, class, interface, or the class, interface or a
* factory function to use.
* @param {?*[]} dependencies The dependencies to pass into the
* constructor or factory function.
* @return {T} Created instance or generated value.
*/
create(name, dependencies) {
let entry = this._getEntry(name);
return this._createInstanceFromEntry(entry, dependencies);
}
/**
* Clears all entries from this object container and resets the locking
* mechanism of this object container.
*
* @return {ObjectContainer} This object container.
*/
clear() {
this._entries.clear();
this._bindingState = null;
this._bindingPlugin = null;
return this;
}
/**
*
* @param {?string} bindingState
* @param {?string} bindingPluginName
*/
setBindingState(bindingState, bindingPluginName = null) {
if (this._bindingState === ObjectContainer.APP_BINDING_STATE) {
throw new Error(`ima.ObjectContainer:setBindingState The setBindingState() ` + `method has to be called only by the bootstrap script. Other ` + `calls are not allowed.`);
}
this._bindingState = bindingState;
this._bindingPlugin = bindingState === ObjectContainer.PLUGIN_BINDING_STATE ? bindingPluginName : null;
}
/**
* Retrieves the entry for the specified constant, alias, class or factory
* function, interface, or fully qualified namespace path (the method
* checks these in this order in case of a name clash).
*
* The method retrieves an existing entry even if a qualified namespace
* path is provided (if the target class or interface has been configured
* in this object container).
*
* The method throws an {@codelink Error} if no such constant, alias,
* registry, interface implementation is known to this object container and
* the provided identifier is not a valid namespace path specifying an
* existing class, interface or value.
*
* @template T
* @param {string|function(new: T, ...*)} name Name of a constant or alias,
* factory function, class or interface constructor, or a fully
* qualified namespace path.
* @return {?Entry<T>} The retrieved entry.
* @throws {Error} If no such constant, alias, registry, interface
* implementation is known to this object container.
*/
_getEntry(name) {
let entry = this._entries.get(name) || this._getEntryFromConstant(name) || this._getEntryFromNamespace(name) || this._getEntryFromClassConstructor(name);
if ($Debug) {
if (!entry) {
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.`);
}
}
return entry;
}
/**
* The method update classConstructor and dependencies for defined entry.
* The entry throw Error for constants and if you try override dependencies
* more than once.
*
* @template T
* @param {(function(new: T, ...*)|function(...*): T)} classConstructor The
* class constructor or factory function.
* @param {Entry} entry The entry representing the class that should
* have its instance created or factory faction to use to create a
* value.
* @param {*[]} dependencies The dependencies to pass into the
* constructor or factory function.
*/
_updateEntryValues(entry, classConstructor, dependencies) {
entry.classConstructor = classConstructor;
entry.dependencies = dependencies;
}
/**
* Creates a new entry for the provided class or factory function, the
* provided dependencies and entry options.
*
* @template T
* @param {(function(new: T, ...*)|function(...*): T)} classConstructor The
* class constructor or factory function.
* @param {?*[]} [dependencies] The dependencies to pass into the
* constructor or factory function.
* @param {{ writeable: boolean }} options
* @return {T} Created instance or generated value.
*/
_createEntry(classConstructor, dependencies, options) {
if ((!dependencies || dependencies.length === 0) && Array.isArray(classConstructor.$dependencies)) {
dependencies = classConstructor.$dependencies;
}
let referrer = this._bindingState;
if (this._bindingState === ObjectContainer.PLUGIN_BINDING_STATE) {
referrer = this._bindingPlugin;
}
return new Entry(classConstructor, dependencies, referrer, options);
}
/**
* Creates a new instance of the class or retrieves the value generated by
* the factory function represented by the provided entry, passing in the
* provided dependencies.
*
* The method uses the dependencies specified by the entry if no custom
* dependencies are provided.
*
* @template T
* @param {Entry<T>} entry The entry representing the class that should
* have its instance created or factory faction to use to create a
* value.
* @param {*[]} [dependencies=[]] The dependencies to pass into the
* constructor or factory function.
* @return {T} Created instance or generated value.
*/
_createInstanceFromEntry(entry, dependencies = []) {
if (dependencies.length === 0) {
dependencies = [];
for (let dependency of entry.dependencies) {
if (['function', 'string'].indexOf(typeof dependency) > -1) {
dependencies.push(this.get(dependency));
} else {
dependencies.push(dependency);
}
}
}
let constructor = entry.classConstructor;
return new constructor(...dependencies);
}
/**
* Retrieves the constant value denoted by the provided fully qualified
* composition name.
*
* The method returns the entry for the constant if the constant is registered
* with this object container, otherwise return {@code null}.
*
* Finally, if the constant composition name does not resolve to value,
* the method return {@code null}.
*
* @param {string} compositionName
* @return {?Entry<*>} An entry representing the value at the specified
* composition name in the constants. The method returns {@code null}
* if the specified composition name does not exist in the constants.
*/
_getEntryFromConstant(compositionName) {
//TODO entries must be
if (typeof compositionName !== 'string') {
return null;
}
let objectProperties = compositionName.split('.');
let constantValue = this._entries.has(objectProperties[0]) ? this._entries.get(objectProperties[0]).sharedInstance : null;
let pathLength = objectProperties.length;
for (let i = 1; i < pathLength && constantValue; i++) {
constantValue = constantValue[objectProperties[i]];
}
if (constantValue !== undefined && constantValue !== null) {
let entry = this._createEntry(() => constantValue, [], {
writeable: false
});
entry.sharedInstance = constantValue;
return entry;
}
return null;
}
/**
* Retrieves the class denoted by the provided fully qualified name within
* the application namespace.
*
* The method then checks whether there are dependecies configured for the
* class, no matter whether the class is an implementation class or an
* "interface" class.
*
* The method returns the entry for the class if the class is registered
* with this object container, otherwise an unregistered entry is created
* and returned.
*
* Finally, if the namespace path does not resolve to a class, the method
* return an unregistered entry resolved to the value denoted by the
* namespace path.
*
* Alternatively, if a constructor function is passed in instead of a
* namespace path, the method returns {@code null}.
*
* @template T
* @param {(string|function(new: T, ...*))} path Namespace path pointing to
* a class or a value in the application namespace, or a constructor
* function.
* @return {?Entry<T>} An entry representing the value or class at the
* specified path in the namespace. The method returns {@code null}
* if the specified path does not exist in the namespace.
*/
_getEntryFromNamespace(path) {
if (typeof path !== 'string' || !this._namespace.has(path)) {
return null;
}
let namespaceValue = this._namespace.get(path);
if (typeof namespaceValue === 'function') {
if (this._entries.has(namespaceValue)) {
return this._entries.get(namespaceValue);
}
return this._createEntry(namespaceValue);
}
let entry = this._createEntry(() => namespaceValue);
entry.sharedInstance = namespaceValue;
return entry;
}
/**
* Retrieves the class denoted by the provided class constructor.
*
* The method then checks whether there are defined {@code $dependecies}
* property for class. Then the class is registered to this object
* container.
*
* The method returns the entry for the class if the specified class
* does not have defined {@code $dependencies} property return
* {@code null}.
*
* @template T
* @param {function(new: T, ...*)} classConstructor
* @return {?Entry<T>} An entry representing the value at the specified
* classConstructor. The method returns {@code null}
* if the specified classConstructor does not have defined
* {@code $dependencies}.
*/
_getEntryFromClassConstructor(classConstructor) {
if (typeof classConstructor === 'function' && Array.isArray(classConstructor.$dependencies)) {
let entry = this._createEntry(classConstructor, classConstructor.$dependencies);
this._entries.set(classConstructor, entry);
return entry;
}
return null;
}
}
exports.default = ObjectContainer;
_namespace2.default.ima.ObjectContainer = ObjectContainer;
/**
* Object container entry, representing either a class, interface, constant or
* an alias.
*
* @template T
*/
class Entry {
/**
* Initializes the entry.
*
* @param {(function(new: T, ...*)|function(...*): T)} classConstructor The
* class constructor or constant value getter.
* @param {*[]} [dependencies=[]] The dependencies to pass into the
* constructor function.
* @param {?string} referrer Reference to part of application that created
* this entry.
* @param {?{ writeable: boolean }} [options] The Entry options.
*/
constructor(classConstructor, dependencies, referrer, options) {
/**
* The constructor of the class represented by this entry, or the
* getter of the value of the constant represented by this entry.
*
* @type {(function(new: T, ...*)|function(...*): T)}
*/
this.classConstructor = classConstructor;
/**
* The shared instance of the class represented by this entry.
*
* @type {T}
*/
this.sharedInstance = null;
/**
* The Entry options.
*
* @type {{ writeable: boolean }}
*/
this._options = options || {
writeable: true
};
/**
* Reference to part of application that created
* this entry.
*
* @type {string}
*/
this._referrer = referrer;
/**
* Dependencies of the class constructor of the class represented by
* this entry.
*
* @type {*[]}
*/
this._dependencies = dependencies || [];
/**
* The override counter
*
* @type {number}
*/
this._overrideCounter = 0;
}
set dependencies(dependencies) {
if ($Debug) {
if (!this.writeable) {
throw new Error(`The entry is constant and you ` + `can't redefined their dependencies ${dependencies}.`);
}
if (this._overrideCounter >= 1) {
throw new Error(`The dependencies entry can't be overrided more than once.` + `Fix your bind.js file for classConstructor ${this.classConstructor.name}.`);
}
}
this._dependencies = dependencies;
this._overrideCounter++;
}
get dependencies() {
return this._dependencies;
}
get referrer() {
return this._referrer;
}
get writeable() {
return this._options.writeable;
}
}
typeof $IMA !== 'undefined' && $IMA !== null && $IMA.Loader && $IMA.Loader.register('ima/ObjectContainer', [], function (_export, _context) {
'use strict';
return {
setters: [],
execute: function () {
_export('default', exports.default);
}
};
});