Skip to content

Instantly share code, notes, and snippets.

@axefrog
Last active December 20, 2015 03:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save axefrog/6061616 to your computer and use it in GitHub Desktop.
Save axefrog/6061616 to your computer and use it in GitHub Desktop.
MyIOC - A simple, lightweight IOC container for TypeScript (and therefore JavaScript) that does everything I need and nothing I don't.
/*
MyIOC - IOC Container for TypeScript/JavaScript
designed and developed by Nathan Ridley
------------------------------------------------------------------
Copyright (c) 2013 Nathan Ridley (axefrog@gmail.com).
All rights reserved.
Redistribution and use in source and binary forms are permitted
provided that the above copyright notice and this paragraph are
duplicated in all such forms and that any documentation,
advertising materials, and other materials related to such
distribution and use acknowledge that the software was developed
by Nathan Ridley. Nathan Ridley's name may not be used to endorse
or promote products derived from this software without specific
prior written permission. THIS SOFTWARE IS PROVIDED "AS IS" AND
WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT
LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE.
------------------------------------------------------------------
This container provides a mechanism for registering and resolving
components in a JavaScript application via dependency injection.
As JavaScript is loosely typed and does not provide a simple
mechanism for reflection, component registration is a slightly
more manual process than with some other platforms. Registering a
component is simple though, and requires that the component
specification provide, where applicable:
1. A unique name for the component (e.g. "Square")
2. A list of additional component names (types) that this
component is valid for (e.g. the component "Square" may also
satify any request to resolve a component of type "Shape")
3. A value, a creator function or a constructor with which to
instantiate (or return) the component. A value implies a
pre-instantiated singleton component.
4. A list of arguments that are to be resolved and passed to the
creator or constructor function (if applicable). The arguments
are strings that identify specific components, or components of
a given type, and each will be resolved to a value and passed
as arguments when instantiating the component.
5. A lifecycle value indicating whether every request to resolve
the component should return the same instance (singleton) or
create a new instance every time (ephemeral).
Usage:
import IOC = require('IOC');
var container = new IOC.Container();
container.register({ ..ComponentSpecification.. });
container.register({ ..ComponentSpecification.. });
...
var obj = container.resolve('Component Name');
API Reference:
### ComponentSpecification
name: The name of the component. Use this name in calls to container.resolve(). This should
be unique within the container. required.
args: An array of strings representing components that will be resolved by the container
and passed as arguments to the component constructor. Each argument can be the name
of a component or any name specified in a component's `satisfies` property. If an
argument name includes a trailing array specified (e.g. 'MyComponent[]'), then all
components satisfying the argument name will be resolved, and passed in as an array.
If an array specifier is not used and multiple components match the argument name,
the first matching component will be resolved.
satisfies: An array of argument names (specified by other components' `args` property) that this
component can satisfy. Particularly useful when the component is a general settings
value or is one of several implementations of a common interface.
lifecycle: Specifies how the component should be resolved. "ephemeral" means a new instance of
the component will be created for each call to `resolve`. "Singleton" means that the
component will only be resolved once, and that value will then be used for subsequent
calls to `resolve`, both explicitly and for argument resolution.
construct: A reference to a constructor function which will be used to create a new instance of
the component. Any resolved `args` will be passed to the constructor function.
create: A function that will be called and expected to return an instance of the component.
Any resolved `args` will be passed to the function.
value: A predefined (preconstructed) value for the component. If `value` is provided, `args`
is not used. If this property is specified, the `lifecycle` is always "ephemeral".
register: An array of child component specifications which will be used in the context of this
component and any of its dependencies. Components included in the array will override
components specified by ancestor components. This property is mainly used when
injecting values for common components where the injected value is dependent upon the
context in which it is used (e.g. the name of a logger, queue, endpoint or otherwise)
Note: The properties `construct`, `create` and `value` are mutually exclusive, and exactly one of
them must be provided in order for the component to be able to resolve.
*/
export interface ComponentSpecification {
name: string;
satisfies?: string[];
construct?: Function;
args?: string[];
create?: (...args: any[]) => any;
value?: any;
lifecycle?: Lifecycle;
register?: ComponentSpecification[];
}
interface Spec extends ComponentSpecification {
_instance: any;
_registry?: SpecRegistry;
}
interface SpecRegistry {
[index: string]: Spec[];
}
export enum Lifecycle {
ephemeral,
singleton
}
export class Container {
private _registry: SpecRegistry;
constructor() {
this._registry = {};
this._registry['_container'] = [];
this._registry['_container'].push({
name: 'Container',
value: this,
_instance: this
});
}
private _resolveArgs(args: string[], componentName: string, registry: SpecRegistry[], warnings?: string[]): any[] {
var self = this;
var values = [];
var failed = false;
args.forEach((s: string, i: number) => {
var value: any;
if (s && s.length > 2 && s.substr(s.length - 2) === '[]')
value = self._resolveAll(s.substr(0, s.length - 2), registry, warnings);
else
value = self._resolveSingle(s, registry, warnings);
values.push(value);
if (!value) {
warnings.push('Unable to resolve parameter #' + i + ' -> "' + s + '" for component "' + componentName + '"');
failed = true;
}
});
return failed ? null : values;
}
private _resolveSpec(spec: Spec, registry: SpecRegistry[], warnings: string[]): any {
if (spec._instance && spec.lifecycle === Lifecycle.singleton)
return spec._instance;
var reg = spec._registry ? registry.concat(spec._registry) : registry;
var component: any;
var args = spec.args ? this._resolveArgs(spec.args, spec.name, reg, warnings) : [];
if (warnings.length > 0)
return null;
if(spec.construct) {
try {
component = Object.create(spec.construct.prototype);
spec.construct.apply(component, args);
}
catch(e) {
console.log('Error constructing component from specification:', spec);
throw e;
}
}
else if (spec.create)
component = spec.create.apply(null, args);
else if (spec.value)
component = spec.value;
if (spec.lifecycle === Lifecycle.singleton)
spec._instance = component;
return component;
}
private _getSpecGroup(type: string, registry: SpecRegistry[]): Spec[] {
for (var i = registry.length - 1; i >= 0; i--) {
var specs = registry[i][type];
if (specs && specs.length > 0)
return specs;
}
return null;
}
private _resolveSingle(type: string, registry: SpecRegistry[], warnings: string[]): any {
var specs = this._getSpecGroup(type, registry);
if (!specs || specs.length === 0) {
warnings.push('Cannot find any component which satisfies "' + type + '"');
return null;
}
var spec = specs[0];
return this._resolveSpec(spec, registry, warnings);
}
private _resolveAll(type: string, registry: SpecRegistry[], warnings: string[]): any[] {
var specs = this._getSpecGroup(type, registry);
var values = [];
if (specs && specs.length > 0)
for (var i = 0; i < specs.length; i++) {
var value = this._resolveSpec(specs[i], registry, warnings);
values.push(value);
}
return values;
}
private _register(spec: ComponentSpecification, registry: SpecRegistry): void {
if(!spec.construct && !spec.create && !spec.value)
throw new Error('Invalid component specification "' + spec.name + '" => must supply one of the following properties: construct, create, value');
var satisfies: string[] = (spec.satisfies || []).concat(spec.name);
if (satisfies)
satisfies.forEach((s: string) => {
var specs: Spec[] = registry[s] || (registry[s] = []);
specs.push(<Spec>spec);
});
var self = this;
if (spec.register && spec.register.length > 0) {
var childRegistry: SpecRegistry = {};
(<Spec>spec)._registry = childRegistry;
spec.register.forEach(function(childSpec: ComponentSpecification) {
self._register(childSpec, childRegistry);
});
}
}
private _validateSpec(spec: ComponentSpecification, namePrefix?: string) {
if(!spec.name)
throw new Error('Unable to register component specification: missing name');
var name = (namePrefix || '') + name;
if((spec.construct && (spec.create || spec.value)) || (spec.create && (spec.construct || spec.value)) || (spec.value && (spec.construct || spec.create)))
throw new Error('Unable to register component specification "' + name + '": only one of create, construct or value may be supplied');
if(spec.construct && typeof spec.construct !== 'function')
throw new Error('Unable to register component specification "' + name + '": construct is not a function');
if(spec.create && typeof spec.create !== 'function')
throw new Error('Unable to register component specification "' + name + '": create is not a function');
if(typeof spec.lifecycle !== 'undefined' && typeof spec.lifecycle !== 'number')
throw new Error('Unable to register component specification "' + name + '": lifecycle is not numeric');
if(spec.args && !(spec.args instanceof Array) && spec.args.filter(function(a) { return typeof a !== 'string'; }).length > 0)
throw new Error('Unable to register component specification "' + name + '": args must be an array of strings');
if(spec.register) {
if(!(spec.register instanceof Array) && spec.register.filter(function(a) { return typeof a !== 'object'; }).length > 0)
throw new Error('Unable to register component specification "' + name + '": register must be an array of objects');
var self = this;
spec.register.forEach(function(subspec) {
self._validateSpec(subspec, namePrefix + spec.name + ':');
});
}
}
register(spec: ComponentSpecification): void {
this._validateSpec(spec);
this._register(spec, this._registry);
}
resolveArgs(args: string[]): any[];
resolveArgs(type: string): any[];
resolveArgs(args: string[], spec: ComponentSpecification): any[];
resolveArgs(type: string, spec: ComponentSpecification): any[];
resolveArgs(args: string[], spec: ComponentSpecification[]): any[];
resolveArgs(type: string, spec: ComponentSpecification[]): any[];
resolveArgs(type: any, spec?: any): any[] {
var args: string[];
var warnings: string[] = [];
var registryStack = this._resolveAdHocSpecRegistry(spec);
if (typeof type === 'string') {
var spec = this._resolveSpec(spec, registryStack, warnings);
if (spec)
args = this._resolveArgs(spec.args, spec.name, registryStack, warnings);
}
else
args = this._resolveArgs(type, 'Anonymous Args', registryStack, warnings);
if (warnings.length > 0)
throw new Error('Unable to resolve arguments ["' + type.join('", "') + '"]:\n- ' + warnings.join('\n- '));
return args;
}
private _resolveAdHocSpecRegistry(spec: ComponentSpecification): SpecRegistry[];
private _resolveAdHocSpecRegistry(spec: ComponentSpecification[]): SpecRegistry[];
private _resolveAdHocSpecRegistry(specs: any): SpecRegistry[] {
var registryStack = [this._registry];
if(specs) {
if(!(specs instanceof Array)) specs = [specs];
var reg: SpecRegistry = {};
var self = this;
specs.forEach(function(spec) {
self._register(spec, reg);
});
registryStack.push(reg);
}
return registryStack;
}
resolve<T>(type: string): T;
resolve<T>(type: string, spec: ComponentSpecification): T;
resolve<T>(type: string, spec: ComponentSpecification[]): T;
resolve<T>(type: string, spec?: any): T {
var warnings: string[] = [];
var registryStack = this._resolveAdHocSpecRegistry(spec);
var component = this._resolveSingle(type, registryStack, warnings);
if (warnings.length > 0)
throw new Error('Unable to resolve component "' + type + '":\n- ' + warnings.join('\n- '));
return <T>component;
}
resolveAll<T>(type: string): T[];
resolveAll<T>(type: string, spec: ComponentSpecification): T[];
resolveAll<T>(type: string, spec: ComponentSpecification[]): T[];
resolveAll<T>(type: string, spec?: any): T[] {
var warnings: string[] = [];
var registryStack = this._resolveAdHocSpecRegistry(spec);
var components = this._resolveAll(type, registryStack, warnings);
if (warnings.length > 0)
throw new Error('Unable to resolve component "' + type + '":\n- ' + warnings.join('\n- '));
return <T[]>components;
}
isDefined(type: string): boolean {
return !!this._registry[type];
}
}
declare module nodeunit {
export class Test {
done(err?: any): void;
expect(num: number): void;
//assersions from node assert module
fail(actual: any, expected: any, message: string, operator: string): void;
assert(value: any, message: string): void;
ok(value: any, message?: string): void;
equal(actual: any, expected: any, message?: string): void;
notEqual(actual: any, expected: any, message?: string): void;
deepEqual(actual: any, expected: any, message?: string): void;
notDeepEqual(actual: any, expected: any, message?: string): void;
strictEqual(actual: any, expected: any, message?: string): void;
notStrictEqual(actual: any, expected: any, message?: string): void;
throws(block: any, error?: any, messsage?: string): void;
doesNotThrow(block: any, error?: any, messsage?: string): void;
ifError(value: any): void;
//assertion wrappers
equals(actual: any, expected: any, message?: string): void;
same(actual: any, expected: any, message?: string): void;
}
// Test Group Usage:
// var testGroup: nodeunit.ITestGroup = {
// setUp: function (callback: nodeunit.ICallbackFunction): void {
// callback();
// },
// tearDown: function (callback: nodeunit.ICallbackFunction): void {
// callback();
// },
// test1: function (test: nodeunit.Test): void {
// test.done();
// }
// }
// exports.testgroup = testGroup;
export interface ITestGroup {
setUp? (callback: ICallbackFunction);
tearDown? (callback: ICallbackFunction);
}
export interface ICallbackFunction { (err?: any): void; }
}
// Test component registration and constructor argument resolution
var __extends = this.__extends || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
__.prototype = b.prototype;
d.prototype = new __();
};
var IOC = require('./IOC');
function test_that_the_container_resolves_arguments_correctly(test) {
var container = new IOC.Container();
container.register({
name: 'Shape',
args: ['Shape.sides', 'Shape.name'],
construct: Shape
});
container.register({
name: 'Shape Sides',
satisfies: ['Shape.sides'],
value: 5
});
container.register({
name: 'Shape Name',
satisfies: ['Shape.name'],
value: 'Pentagon'
});
var obj = container.resolve('Shape');
test.ok(obj);
test.ok(obj instanceof Shape, 'obj not an instance of Shape');
test.equal(obj.sides, 5);
test.done();
}
exports.testSomething = testSomething;
var Shape = (function () {
function Shape(sides, name) {
this._sides = sides;
this._name = name;
}
Object.defineProperty(Shape.prototype, "sides", {
get: function () {
return this._sides;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Shape.prototype, "name", {
get: function () {
return this._name;
},
enumerable: true,
configurable: true
});
Shape.prototype.describe = function () {
var num;
switch (this._sides) {
case 0:
num = "no sides";
break;
case 1:
num = "one side";
break;
default:
num = this._sides + " sides";
break;
}
return this._name + ' has ' + num;
};
return Shape;
})();
var Square = (function (_super) {
__extends(Square, _super);
function Square() {
_super.call(this, 4, 'Square');
}
return Square;
})(Shape);
var Circle = (function (_super) {
__extends(Circle, _super);
function Circle() {
_super.call(this, 1, 'Circle');
}
return Circle;
})(Shape);
/// <reference path="definitions/node.d.ts" />
/// <reference path="definitions/nodeunit.d.ts" />
import IOC = require('./IOC');
export function testSomething(test:nodeunit.Test): any {
var container = new IOC.Container();
container.register({
name: 'Shape',
args: ['Shape.sides', 'Shape.name'],
construct: Shape
});
container.register({
name: 'Shape Sides',
satisfies: ['Shape.sides'],
value: 5
});
container.register({
name: 'Shape Name',
satisfies: ['Shape.name'],
value: 'Pentagon'
});
var obj = container.resolve<Shape>('Shape');
test.ok(obj);
test.ok(obj instanceof Shape, 'obj not an instance of Shape');
test.equal(obj.sides, 5);
test.done();
}
class Shape {
private _sides:number;
private _name:string;
constructor(sides:number, name:string) {
this._sides = sides;
this._name = name;
}
public get sides() { return this._sides; }
public get name() { return this._name; }
describe(): string {
var num:any;
switch(this._sides) {
case 0: num = "no sides"; break;
case 1: num = "one side"; break;
default: num = this._sides + " sides"; break;
}
return this._name + ' has ' + num;
}
}
class Square extends Shape {
constructor() {
super(4, 'Square');
}
}
class Circle extends Shape {
constructor() {
super(1, 'Circle');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment