@midgar/midgar
Version:
node js framework
218 lines (184 loc) • 5.67 kB
JavaScript
const path = require('path')
const { isObject, asyncGlob } = require('./utils')
/**
* @typedef {Object} ServiceDef
* @property {string} name Service name
* @property {constructor} service Service class
* @property {Array<string>} dependencies Dependencies service names
*/
/**
* Container class
* Dependency injection container
*/
module.exports = class Container {
/**
* @param {App} app App instance
*/
constructor (app) {
/**
* App Instance
* @type {App}
*/
this.app = app
/**
* Service définitions dictionnary
*/
this._serviceDefs = {}
/**
* Service instances dictionnary
*/
this._instances = {}
}
/**
* Add a service définition
*
* @param {ServiceDef} serviceDef Service définition
*
* @returns {void}
*/
addService (serviceDef) {
try {
this._checkServiceDef(serviceDef)
} catch (error) {
const serviceDefString = JSON.stringify(serviceDef, null, ' ')
throw new Error(`${error.message}\n${serviceDefString}`)
}
this._serviceDefs[serviceDef.name] = serviceDef
}
/**
* Add many service définitions
*
* @param {Array<ServiceDef>} serviceDefs Service définitions
*
* @returns {void}
*/
addServices (serviceDefs) {
for (const serviceDef of serviceDefs) {
this.addService(serviceDef)
}
}
/**
* Check a service definition and throw an Error
* if somthing wrong
*
* @param {ServiceDef} serviceDef Service définition
*
* @private
*/
_checkServiceDef (serviceDef) {
if (typeof serviceDef !== 'object') throw new TypeError(`Invalid service définition type !`)
if (serviceDef.service === undefined) throw new Error(`No service propertie in service définition !`)
if (serviceDef.name === undefined) throw new Error(`No name propertie in service définition !`)
if (typeof serviceDef.name !== 'string') throw new TypeError(`Invalid service name type !`)
if (typeof serviceDef.service !== 'function')
throw new TypeError(`Invalid service type !`)
if (serviceDef.dependencies === undefined) return
if (!Array.isArray(serviceDef.dependencies))
throw new TypeError(`Invalid service dependencies type !`)
for (const dependency of serviceDef.dependencies) {
if (typeof dependency !== 'string')
throw new TypeError(`Invalid service dependency type !`)
}
}
/**
* Add a directory of service
*
* Load all service in the dirPath with the glob pattern
*
* @param {String} dirPath Directory path
* @param {String} pattern Glob pattern
*
* @returns {Promise<void>}
*/
async addServiceDir (dirPath, pattern) {
const serviceFiles = await asyncGlob(dirPath, pattern)
for (const fileName of serviceFiles) {
const serviceDef = require(path.join(dirPath, fileName))
this.addService(serviceDef)
}
}
/**
* Return an instance of a service
*
* @param {string} name Service name
*
* @returns {any}
*/
getService (name) {
return this._getService(name)
}
/**
* Return an instance of a service
*
* @param {string} name Service name
* @param {array} parents Array of parents services
*
* @private
* @returns {any}
*/
_getService (name, parents = []) {
if (this._instances[name] === undefined) {
this._instances[name] = this._createServiceInstance(name, parents)
}
return this._instances[name]
}
/**
* Create an instance of a service
*
* @param {string} name Service name
* @param {array} parents Array of parents services
*
* @private
* @returns {any}
*/
_createServiceInstance (name, parents = []) {
if (this._serviceDefs[name] === undefined)
throw new Error(`Container: unknow service ${name}.`)
parents.push(name)
const serviceDef = this._serviceDefs[name]
let instance
let dependencies = []
if (serviceDef.dependencies && serviceDef.dependencies.length) {
dependencies = this._getServiceDependencies(serviceDef, parents)
}
const args = [this.app]
args.push(...dependencies)
this.app.debug(`Container: create service instance ${name}.`)
// If the service is a class
if (/^class\s/.test(Function.prototype.toString.call(serviceDef.service))) {
const Class = serviceDef.service
instance = new Class(...args)
} else {
instance = serviceDef.service(...args)
}
return instance
}
/**
* Retrun an array of dependencies instance
*
* @param {string} serviceDef Parent service name
* @param {array} parents Array of parents services
*
* @private
* @returns {Array<any>}
*/
_getServiceDependencies (serviceDef, parents) {
const depependencies = []
for (const dependency of serviceDef.dependencies) {
let name
if (isObject(dependency)) {
type = dependency.type
name = dependency.name
} else if (typeof dependency === 'string') {
name = dependency
}
if (parents.indexOf(name) !== -1) {
throw new Error(
`Circular dependency in service ${serviceDef.name}, ${name} already depend on ${serviceDef.name} (${[...parents, name].join('->')})!`
)
}
depependencies.push(this._getService(name, JSON.parse(JSON.stringify(parents))))
}
return depependencies
}
}