Last active
December 20, 2015 03:09
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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