const leadingCapsExpr = /^\p{Lu}+/u;
/**
* The registry of injectable services.
* @extends Map
*/
export class Registry extends Map
{
/**
* Register dependencies
* @param {Function} target - The injectable class to be registered.
* @param {boolean} [target.isInjectable] - True if the target has been registered.
* @param {function[]} services - The injectable classes the target depends on.
*/
register(target, services) {
// We must allow for injecting into an inherited constructor - target.length will be 0.
if (target.length > 0 && services.length !== target.length) {
throw new Error(`Dependency count for ${target.name} should be ${target.length}`);
} else if (target.length === 0) {
const superName = Object.getPrototypeOf(target.prototype).constructor.name;
const inherited = (this.get(superName) && this.get(superName).services) || [];
// We must inject at least the dependencies that our superclass is expecting.
if (services.length < inherited.length) {
throw new Error(`Dependency count for ${target.name} should be at least ${inherited.length}`);
} else {
services.forEach((service, index) => {
if (inherited[index] && service !== inherited[index] && !(service.prototype instanceof inherited[index])) {
throw new Error(`Dependency mis-match for ${target.name} at ${service.name}`)
}
});
services = services.concat(inherited.slice(services.length));
}
}
/*
* We must ensure that we are not re-using a name that a superclass of ours is using.
* For example if B extends A, and D extends C,
* and we declare D to be @autoinjectable(A)
* then it is illegal to declare C to be @autoinjectable(B, A),
* because this.a will have conflicting meanings -
* methods of D will expect this.a to be the instance of B (which is legal),
* but methods of C will expect this.a to be an independent plain A instance, not B.
*/
const names = services.map(service => this.makeName(service));
let parent = Object.getPrototypeOf(target.prototype).constructor;
let entry = this.get(parent.name);
while (entry && entry.names.length) {
if (names.some(
(name, index) => entry.names.some((comp, compIndex) => (comp === name && compIndex !== index))
)) {
throw new Error(`injection conflict! Re-use of "${parent.name}" injection in "${target.name}"`);
}
parent = Object.getPrototypeOf(parent.prototype);
entry = this.get(parent.name);
}
this.set(target.name, {
services: services,
names: names,
});
target.isInjectable = true;
}
autoInject(target, services) {
this.register(target, services);
const names = this.get(target.name).names;
const autoInjected = class extends target {
constructor(...args) {
super(...args);
names.forEach((name, index) => this[name] = args[index]);
}
};
Object.defineProperty(autoInjected, 'name', {value: target.name});
return autoInjected;
}
/**
* Resolve dependencies
* @param {Function} target - The class of the service to be resolved.
* @param {boolean} [target.isInjectable] - True if the target has been registered.
* @returns {Object} - An instance of the target class.
*/
resolve(target) {
const data = this.get(target.name);
if (!target.isInjectable) {
throw new Error(`Target is not injectable for ${target.name}`);
}
return data.instance || (data.instance = new target(...(data.services).map(service => this.resolve(service))));
}
/**
* Makes a camelCase property name for a service class.
* @protected
* @param {Function} service - The class of the service.
* @return {string} - The name to use for properties holding an instance of this service.
*/
makeName(service) {
const name = service.name;
const modCount = (leadingCapsExpr.exec(name)[0].length - 1) || 1;
return (
modCount === name.length - 1
? name.toLowerCase()
: name.substring(0, modCount).toLowerCase() + name.substring(modCount)
);
}
}