UNPKG

hilary

Version:

a simple Dependency Injection (DI) library that provides Inversion of Control (IoC) capabilities in Node.js and the browser

688 lines (588 loc) 26.6 kB
(function (register) { 'use strict'; var ASYNC = 'polyn::async', CONTEXT = 'hilary::context', IMMUTABLE = 'polyn::Immutable', IS = 'polyn::is', DEFAULT = 'default'; register({ name: 'HilaryApi', factory: HilaryApi }); function HilaryApi (async, is, id, Immutable, locale, Logger, Exception, Context, HilaryModule, ResolveTasks) { var Api, scopes = {}, defaultScope; /* // The Hilary constructor // @param options (string or Object): a named scope, or multiple options // @param options.name (string): a named scope // // @returns new Hilary scope with parent set to this (the current Hilary scope) */ Api = function (options) { var self, logger = new Logger(options), config = new Config(options), context, resolveTasks; context = new Context(config); resolveTasks = new ResolveTasks(context, logger, resolve); if (context.isException) { return context; } self = { __isHilaryScope: true, context: context, HilaryModule: HilaryModule, register: register, makeRegistrationTask: makeRegistrationTask, resolve: resolve, exists: exists, dispose: dispose, bootstrap: bootstrap, scope: scope, setParentScope: setParentScope }; Object.defineProperty(self, 'name', { enumerable: false, configurable: false, get: function () { return self.context.scope; }, set: function () { logger.warn(name + ' is read only'); } }); scopes[config.scope] = self; // TODO: this is probably better done with a wrapper // if (config.hilaryCompatible) { // // add hilaryjs compatible APIs // self.autoRegister = register; // self.Bootstrapper = bootstrap; // } /* // register a module by name, an array of modules // @param definition (object): the module defintion: at least the name and factory properties are required // @param next (function): optional async API // @returns this (the Hilary scope) */ function register (moduleOrArray, callback) { var err = new Error(locale.api.REGISTER_ERR); if (is.object(moduleOrArray)) { logger.trace({ message: 'Registering a single module', module: moduleOrArray }); return registerOne(moduleOrArray, err, callback); } else if (is.array(moduleOrArray)) { logger.trace({ message: 'Registering an array of modules', modules: moduleOrArray }); return optionalAsync(function () { moduleOrArray.forEach(registerOne); return self; }, err, callback); } else { var exc = new Exception({ type: locale.errorTypes.INVALID_ARG, error: new Error(locale.api.REGISTER_ARG + JSON.stringify(moduleOrArray)), data: moduleOrArray }); logger.error({ message: 'Registration failed', exception: exc }); if (is.function(callback)) { callback(exc); } return exc; } } /* // register a module by name // @param definition (object): the module defintion: at least the name and factory properties are required // @param next (function): optional async API // @returns this (the Hilary scope) */ function registerOne (input, err, callback) { var tasks = []; if (self.name === DEFAULT && input && input.scope && input.scope !== DEFAULT) { // the module declares the scope that is meant to be // registered to, and it is not 'default' return self.scope(input.scope).register(input, callback); } tasks.push(function bind (next) { var hilaryModule = new HilaryModule(input); if (hilaryModule.isException) { logger.error({ message: 'Invalid registration model', module: hilaryModule }); return next(new Exception({ type: locale.errorTypes.INVALID_REGISTRATION, error: new Error(hilaryModule.error.message), messages: hilaryModule.messages, data: input })); } else { logger.trace('Successfully bound to HilaryModule: ' + hilaryModule.name); return next(null, hilaryModule); } }); tasks.push(function addToContainer (hilaryModule, next) { context.container.register(hilaryModule); logger.trace('Module registered on container: ' + hilaryModule.name); next(null, hilaryModule); }); // TODO: addTagReferences (i.e. tags: ['controller']) so modules can be resolved by tag if (is.function(callback)) { return async.waterfall(tasks, function (err, hilaryModule) { if (err) { logger.error({ message: 'Registration failed', input: input, err: err }); return callback(err); } logger.debug('Registration success: ' + hilaryModule.name); callback(err, hilaryModule); }); } else { var output; async.waterfall(tasks, { blocking: true }, function (err, hilaryModule) { if (err) { logger.error({ message: 'Registration failed', input: input, err: err }); output = err; return; } logger.debug('Registration success: ' + hilaryModule.name); output = hilaryModule; }); return output; } } /* // make a registration task that can be added to a bootstrapper startup array */ function makeRegistrationTask (moduleOrArray) { logger.trace({ message: 'Making a registration task', module: moduleOrArray }); return function (scope, done) { var err; if (!scope || !scope.__isHilaryScope || is.not.function(done)) { err = new Error(locale.bootstrap.INVALID_TASK_ARGUMENT); logger.error({ message: locale.bootstrap.INVALID_TASK_ARGUMENT, err: err }); return arguments[arguments.length-1](new Exception({ type: locale.errorTypes.INVALID_REGISTRATION, error: err, messages: [locale.bootstrap.INVALID_TASK_ARGUMENT], data: moduleOrArray })); } register(moduleOrArray, function (err) { if (err) { return done(err); } done(null, scope); }); }; } /* // attempt to resolve a dependency by name (supports parental hierarchy) // @param moduleName (string): the qualified name that the module can be located by in the container // @returns the module that is being resolved */ function resolve (moduleName, callback) { logger.trace('resolving: ' + moduleName); if (is.regexp(moduleName)) { var modules = resolveTasks.filterMatchingRegistrations(moduleName, context) .map(function (moduleNameText) { return resolveOne(moduleNameText, moduleNameText); }) if (is.function(callback)) { callback(null, modules); } else { return modules; } } return resolveOne(moduleName, moduleName, callback); } function resolveOne (moduleName, relyingModuleName, callback) { var ctx = { name: moduleName, relyingName: relyingModuleName, theModule: undefined, resolved: undefined, isResolved: false, registerSingleton: true, members: [] }, tasks = []; tasks.push(function (next) { next(null, ctx); }); tasks.push(resolveTasks.validateModuleName); tasks.push(resolveTasks.findModule); tasks.push(resolveTasks.resolveDependencies); tasks.push(resolveTasks.reduceMembers); tasks.push(resolveTasks.optionallyRegisterSingleton); tasks.push(resolveTasks.bindToOutput); // RUN the waterfall if (is.function(callback)) { async.waterfall(tasks, function (err, results) { if ( err && err.type === locale.errorTypes.MODULE_NOT_FOUND && context.parent && scopes[context.parent] ) { logger.trace('Attempting to resolve the module, ' + ctx.name + ', on the parent scope: ' + context.parent); return scopes[context.parent].resolve(moduleName, callback); } else if (err && err.type === locale.errorTypes.MODULE_NOT_FOUND) { logger.trace('Attempting to gracefully degrade, ' + ctx.name); return resolveDegraded(ctx, callback); } if (err) { logger.error({ message: 'Resolve failed for: ' + ctx.name, err: err }); return callback(err); } logger.debug('Module resolved: ' + ctx.name); callback(null, results); }); } else { var output; async.waterfall(tasks, { blocking: true }, function (err, results) { if ( err && err.type === locale.errorTypes.MODULE_NOT_FOUND && context.parent && scopes[context.parent] ) { logger.trace('Attempting to resolve the module, ' + ctx.name + ', on the parent scope: ' + context.parent); output = scopes[context.parent].resolve(moduleName); return; } else if (err && err.type === locale.errorTypes.MODULE_NOT_FOUND) { logger.trace('Attempting to gracefully degrade, ' + ctx.name); output = resolveDegraded(ctx); return; } if (err) { logger.error({ message: 'Resolve failed for: ' + ctx.name, err: err }); output = err; return; } logger.debug('Module resolved: ' + ctx.name); output = results; }); return output; } } function resolveDegraded (ctx, callback) { var tasks = []; tasks.push(function (next) { next(null, ctx); }); tasks.push(resolveTasks.findDegraded); tasks.push(resolveTasks.reduceMembers); tasks.push(resolveTasks.optionallyRegisterSingleton); tasks.push(resolveTasks.bindToOutput); if (is.function(callback)) { async.waterfall(tasks, function (err, results) { if (err) { logger.error({ message: 'Resolve failed for: ' + ctx.name, err: err }); return callback(err); } logger.debug('Module resolved: ' + ctx.name); callback(null, results); }); } else { var output; async.waterfall(tasks, { blocking: true }, function (err, results) { if (err) { logger.error({ message: 'Resolve failed for: ' + ctx.name, err: err }); output = err; return; } logger.debug('Module resolved: ' + ctx.name); output = results; }); return output; } } /* // Checks to see if a module exists and returns a boolean result // @param moduleName (string): the qualified name that the module can be located by in the container // @returns true if the module exists, otherwise false */ function exists (moduleName) { logger.debug('Checking if module exists: ' + moduleName); return context.container.exists(moduleName); } /* // Disposes a module, or all modules. When a moduleName is not passed // as an argument, the entire container is disposed. // @param moduleNames (string): The name of the module to dispose // @returns boolean: true if the object(s) were disposed, otherwise false */ function dispose (moduleNames, callback) { var nameOrArr, cb; if (typeof moduleNames === 'function') { logger.debug('Disposing all modules on scope, ' + self.name); nameOrArr = null; cb = moduleNames; } else { logger.debug({ message: 'Disposing module(s) on scope, ' + self.name, moduleNames: moduleNames }); nameOrArr = moduleNames; cb = callback; } return optionalAsync(function () { var results; if (is.array(nameOrArr)) { results = context.container.dispose(nameOrArr).disposed.concat( context.singletonContainer.dispose(nameOrArr).disposed ); return { result: results.length === nameOrArr.length, disposed: results }; } return context.container.dispose(nameOrArr) || context.singletonContainer.dispose(nameOrArr); }, new Error(), cb); } /* // exposes the constructor for hilary so you can create // new scopes, and child scopes // @param options.name (string): a named scope // // @returns new Hilary scope with parent set to this (the current Hilary scope) */ function scope (name, options, callback) { name = name || id.createUid(8); options = options || {}; options.parent = self.context.scope === DEFAULT ? getScopeName(options.parent) : getScopeName(options.parent || self); if (scopes[name]) { logger.debug('Returning existing scope: ' + name); } else { logger.debug({ message: 'Creating new scope: ' + name, options: options }); } return optionalAsync(function () { return Api.scope(name, options); }, new Error(), callback); } /* // allows you to set a scopes parent container explicitly // @param options.utils (object): utilities to use for validation (i.e. isFunction) // @param options.exceptions (object): exception handling // // @returns new Hilary scope with parent set to this (the current Hilary scope) */ function setParentScope (scope) { var name = getScopeName(scope); if (!name) { logger.error('Unable to set the parent scope of, ' + self.context.scope + ', to: ' + name); return new Exception({ type: locale.errorTypes.INVALID_ARG, error: new Error(locale.api.PARENT_CONTAINER_ARG) }); } logger.debug('Setting the parent scope of, ' + self.context.scope + ', to: ' + name); context.parent = name; return context; } function getScopeName (scope) { if (!scope) { return null; } else if (is.string(scope)) { return scope; } else if (scope.__isHilaryScope) { return scope.context.scope; } else { return null; } } function bootstrap (startupTasks, callback) { var tasks = [], callbackExists = is.function(callback), done; if (is.defined(startupTasks) && is.not.array(startupTasks)) { tasks.push(function (next) { next(new Exception({ type: locale.errorTypes.BOOTSTRAP_FAILED, error: new Error(locale.bootstrap.TASKS_ARRAY) })); }); } if (callbackExists) { logger.trace('Using the callback argument for the bootstrapper for: ' + self.context.scope); done = callback; } else { logger.trace('A callback was not defined for the bootstrapper for: ' + self.context.scope); done = function (err) { if (err) { logger.fatal(new Exception({ type: locale.errorTypes.BOOTSTRAP_FAILED, error: err })); } else { logger.trace('finished bootstrapping: ' + self.context.scope); } }; } tasks.push(function start (next) { logger.trace('bootstrapping hilary for: ' + self.context.scope); next(null, self); }); if (Array.isArray(startupTasks)) { startupTasks.forEach(function (task) { if (is.function(task)) { tasks.push(task); } }); } if (is.defined(startupTasks) && startupTasks.length > 0 && tasks.length === 1) { tasks.push(function (scope, next) { next(new Exception({ type: locale.errorTypes.BOOTSTRAP_FAILED, error: new Error(locale.bootstrap.TASKS_ARRAY) })); }); } if (tasks.length === 1) { logger.trace('no task functions were found in the bootstrapper for: ' + self.context.scope); } return async.waterfall(tasks, done); } function optionalAsync(func, err, callback) { if (is.function(callback)) { async.runAsync(function () { var result = tryWith(func, err); if (result.isException) { callback(err); } else { callback(null, result); } }); } else { return tryWith(func, err); } } function tryWith(func, err) { try { return func(); } catch (e) { err.message += '(' + e.message + ')'; err.cause = e; logger.error({ message: e.message, err: err }); return { err: err, isException: true }; } } function Config (options) { var self = {}; options = options || {}; if (is.string(options)) { self.scope = options; } else if (is.nullOrUndefined(options.scope)) { if (is.string(options.name)) { self.scope = options.name; } else { self.scope = id.createUid(8); } } self.logging = options.logging || { level: 30 }; if (is.string(options.parent)) { self.parent = options.parent; } else if (options.parent && options.parent.__isHilaryScope) { self.parent = options.parent.context.scope; } return self; } // /Config return self; }; // /Api Api.scope = function (name, options, seal) { seal = typeof seal !== 'boolean' ? true : seal; if (scopes[name]) { return scopes[name]; } else { options = options || {}; options.name = name; scopes[name] = new Api(options); if (seal) { freeze(scopes[name]); } return scopes[name]; } }; defaultScope = Api.scope(DEFAULT, null, false); defaultScope.Context = Context; // REGISTER Default Modules // TODO: should these be on the container, as well, since that is normal behavior? defaultScope.context.singletonContainer.register({ name: ASYNC, factory: async }); defaultScope.context.singletonContainer.register({ name: CONTEXT, singleton: false, factory: function () { return defaultScope.context; } }); defaultScope.context.singletonContainer.register({ name: IMMUTABLE, factory: Immutable }); defaultScope.context.singletonContainer.register({ name: IS, factory: is }); freeze(defaultScope); return defaultScope; } // /Api function freeze (scope) { Object.freeze(scope); Object.seal(scope.context); Object.seal(scope.context.container); Object.seal(scope.context.singletonContainer); } }(function (registration) { 'use strict'; try { if (typeof module !== 'undefined' && module.exports) { module.exports = registration.factory; } else if (typeof window !== 'undefined') { window.__hilary = window.__hilary || {}; window.__hilary[registration.name] = registration.factory; } else { throw new Error('[HILARY] Unkown runtime environment'); } } catch (e) { var name = registration && registration.name ? registration.name : 'MISSING NAME'; var err = new Error('[HILARY] Registration failure: ' + name); err.cause = e; throw err; } }));