index.ts
/**
* A Papaya container
*/
export class Papaya<T extends { [name: string]: any } = any> {
private _services: { [name: string]: any } = {}
private _functions: { [name: string]: true } = {}
private _factories: { [name: string]: true } = {}
/**
* Gets a service by name.
*
* If the service is not set, will return undefined.
*
* @param name The service name
* @return The service or undefined if it is not set
*/
public get<K extends keyof T>(name: K): T[K] {
type Service = (this: this, container: this) => T[K]
if (this._factories[name]) {
return (this._services[name] as Service).call(this, this)
}
if (this._functions[name]) {
this._services[name] = (this._services[name] as Service).call(this, this)
delete this._functions[name]
}
return this._services[name]
}
/**
* Creates a constant by name and value.
*
* @param name The service name
* @param constant The value to be set
*/
public constant<K extends keyof T>(name: K, constant: T[K]): this {
this._setService(name, constant)
return this
}
/**
* Sets a service by name and a service function
*
* The return value of the `service` function will be treated as a singleton
* service meaning it will be initialized the first time it is requested and
* its value will be cached for subsequent requests.
*
* @param name The service name
* @param service The service singleton function or static service
*/
public service<K extends keyof T>(
name: K,
service: (this: this, container: this) => T[K]
): this {
this._setService(name, service, this._functions)
return this
}
/**
* Sets a factory service by name and value.
*
* The `factory` function will be called every time the service is requested.
* So if it returns an object, it will create a new object for every request.
*
* @param name The service name
* @param factory The service factory function or static service
*/
public factory<K extends keyof T>(
name: K,
factory: (this: this, container: this) => T[K]
): this {
this._setService(name, factory, this._factories)
return this
}
/**
* Extends an existing service and overrides it.
*
* The `extender` function will be called with 2 argument which will be the
* previous value of the service and the Papaya container. If there is no
* existing `name` service, it will throw an error immediately. The
* `extender` function should return the new value for the service that will
* override the existing one.
*
* If `this.extend` is called for a service that was created with
* `this.service`, the resulting service will be a singleton.
*
* If `this.extend` is called for a service that was created with
* `this.factory` the resulting service will be a factory.
*
* If `this.extend` is called for a service that was created with
* `this.constant`, the resulting service will be a singleton.
*
* @param name The service name
* @param extender The service extender function or static service.
*/
public extend<K extends keyof T>(
name: string,
extender: (this: this, extended: T[K], container: this) => T[K]
): this {
if (!this.has(name)) {
throw new Error(`Cannot extend missing service: ${name}`)
}
const extended = this._services[name]
let protect = false
const service = () => {
return extender.call(
this,
protect ? extended : extended.call(this, this),
this
)
}
if (this._factories[name]) {
return this.factory(name, service)
} else {
protect = !this._functions[name]
return this.service(name, service)
}
}
/**
* Register a service provider function.
*
* `provider` will be called with the container as the context. Service
* providers are a good place to register related services using
* `this.service` etc.
*
* @param provider The service provider function
*/
public register(provider: (this: this, container: Papaya<any>) => void) {
provider.call(this, this)
return this
}
/**
* Get an array of the regitered service names
*
* @return An array of service names
*/
public keys(): Array<keyof T> {
return Object.keys(this._services)
}
/**
* Check whether a service has been registered for the given name'
*
* @return True if a service has been registered for `name`, false otherwise.
*/
public has<K extends keyof T>(name: K): boolean {
return this._services.hasOwnProperty(name)
}
private _setService(
name: string,
service: any,
registry?: { [name: string]: true }
) {
delete this._services[name]
delete this._functions[name]
delete this._factories[name]
if (registry && typeof service === 'function') {
registry[name] = true
}
this._services[name] = service
}
}