diff --git a/README.md b/README.md index cd8b243..ba67901 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,133 @@ Note that a factory function or constructor function is only called once. Each c Remember that singletons are only singletons within a single binder, though. Different binders -- for instance, created for separate test methods -- will each have their own singleton instance. +## Inspect Dependency Graph + +Pluto.js tracks how components are injected to help diagnose issues and aid in application discovery. The full injection graph is available for injection under the key, `plutoGraph`. + +Taking out Greeter example: + +```js +function greetFactory(greeting) { + return function greet() { + return `${greeting}, World!` + } +} + +class Greeter { + constructor(greet) { + this.greet = greet + } +} + +const bind = pluto() +bind('greeting').toInstance('Hello') +bind('greet').toFactory(greetFactory) +bind('greeter').toConstructor(Greeter) + +// Bootstrap application +const app = yield bind.bootstrap() + +// Retrieve the graph. Note that this can also be injected +// into a component directly! +const graph = app.get('plutoGraph') +``` + +### `Graph` Object + +The `Graph` class has the following relevant methods: + +**.nodes** + +An `Array` of all `GraphNode`s. + +**.getNode(name)** + +Returns the `GraphNode` with the given name. + +### `GraphNode` Object + +The `GraphNode` class has the following relevant methods: + +**.name** + +The string name used to bind the component. + +**.bindingStrategy** + +The strategy used to bind the component for injection. One of `instance`, `factory`, or `constructor`. + +**.parents** + +A `Map` of parent nodes, with names used for keys and `GraphNode` objects for values. + +**.children** + +A `Map` of child nodes, with names used for keys and `GraphNode` objects for values. + +**.isBuiltIn** + +Returns true if the node is built in to Pluto.js, like the `plutoBinder`, `plutoApp`, or `plutoGraph` itself. + +### JSON Representation + +The graph, when converted to JSON, will be represented as a flattened `Array` of `GraphNodes`, like: + +```json +[ + { + "name": "plutoGraph", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isBuiltIn": true + }, + { + "name": "plutoBinder", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isBuiltIn": true + }, + { + "name": "greeting", + "parents": [ + "greet" + ], + "children": [], + "bindingStrategy": "instance", + "isBuiltIn": false + }, + { + "name": "greet", + "parents": [ + "greeter" + ], + "children": [ + "greeting" + ], + "bindingStrategy": "factory", + "isBuiltIn": false + }, + { + "name": "greeter", + "parents": [], + "children": [ + "greet" + ], + "bindingStrategy": "constructor", + "isBuiltIn": false + }, + { + "name": "plutoApp", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isBuiltIn": true + } +] +``` + ## Self injection There are times when you might not know exactly what you'll need until later in runtime, and when you might want to manage injection dynamically. Pluto can inject itself to give you extra control. diff --git a/lib/Graph.js b/lib/Graph.js new file mode 100644 index 0000000..71ed7c0 --- /dev/null +++ b/lib/Graph.js @@ -0,0 +1,41 @@ +'use strict' + +const GraphNode = require('./GraphNode') + +module.exports = class Graph { + constructor() { + this._internal = {} + this._internal.nodes = new Map() + } + + addNode(name) { + const node = new GraphNode({ + name + }) + this._internal.nodes.set(name, node) + } + + getNode(name) { + return this._internal.nodes.get(name) + } + + wireChildren(name, childNames) { + const currentGraphNode = this.getNode(name) + for (let childName of childNames) { + const childNode = this.getNode(childName) + currentGraphNode.addChild(childName, childNode) + } + } + + get nodes() { + const nodes = [] + for (let node of this._internal.nodes.values()) { + nodes.push(node.toJSON()) + } + return nodes + } + + toJSON() { + return this.nodes + } +} diff --git a/lib/GraphNode.js b/lib/GraphNode.js new file mode 100644 index 0000000..b3ee6dc --- /dev/null +++ b/lib/GraphNode.js @@ -0,0 +1,60 @@ +'use strict' + +const builtInObjectNames = ['plutoBinder', 'plutoApp', 'plutoGraph'] + +module.exports = class GraphNode { + constructor(opts) { + this._internal = {} + this._internal.name = opts.name + this._internal.parents = new Map() + this._internal.children = new Map() + } + + addChild(name, node) { + // wire up relationship bi-directionally + this._internal.children.set(name, node) + node._internal.parents.set(this.name, this) + } + + get name() { + return this._internal.name + } + + get parents() { + return this._internal.parents + } + get children() { + return this._internal.children + } + + // return true if this is a component built in to pluto, like the plutoBinder + get isBuiltIn() { + return builtInObjectNames.indexOf(this._internal.name) >= 0 + } + + set bindingStrategy(bindingStrategy) { + this._internal.bindingStrategy = bindingStrategy + } + + get bindingStrategy() { + return this._internal.bindingStrategy + } + + toJSON() { + const o = { + name: this._internal.name, + parents: [], + children: [], + bindingStrategy: this.bindingStrategy, + isBuiltIn: this.isBuiltIn + } + + for (let name of this._internal.children.keys()) { + o.children.push(name) + } + for (let name of this._internal.parents.keys()) { + o.parents.push(name) + } + return o + } +} diff --git a/lib/fixtures/greeterGraph.json b/lib/fixtures/greeterGraph.json new file mode 100644 index 0000000..626be74 --- /dev/null +++ b/lib/fixtures/greeterGraph.json @@ -0,0 +1,52 @@ +[ + { + "name": "plutoGraph", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isBuiltIn": true + }, + { + "name": "plutoBinder", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isBuiltIn": true + }, + { + "name": "greeting", + "parents": [ + "greet" + ], + "children": [], + "bindingStrategy": "instance", + "isBuiltIn": false + }, + { + "name": "greet", + "parents": [ + "greeter" + ], + "children": [ + "greeting" + ], + "bindingStrategy": "factory", + "isBuiltIn": false + }, + { + "name": "greeter", + "parents": [], + "children": [ + "greet" + ], + "bindingStrategy": "constructor", + "isBuiltIn": false + }, + { + "name": "plutoApp", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isBuiltIn": true + } +] diff --git a/lib/pluto.js b/lib/pluto.js index 5694a9f..7a2f536 100644 --- a/lib/pluto.js +++ b/lib/pluto.js @@ -3,15 +3,21 @@ const co = require('co') const memoize = require('lodash.memoize') +const Graph = require('./Graph') + function isPromise(obj) { return obj && obj.then && typeof obj.then === 'function' } function pluto() { const namesToResolvers = new Map() + const graph = new Graph() + + bind('plutoGraph').toInstance(graph) - function createInstanceResolver(instance) { + function createInstanceResolver(name, instance) { return function () { + graph.getNode(name).bindingStrategy = 'instance' return Promise.resolve(instance) } } @@ -24,7 +30,7 @@ function pluto() { return argumentNames || [] } - function createFactoryResolver(factory) { + function createFactoryResolver(name, factory) { return co.wrap(function* () { if (isPromise(factory)) { factory = yield factory @@ -32,11 +38,16 @@ function pluto() { const argumentNames = getArgumentNames(factory) const args = yield getAll(argumentNames) + + // build injection graph + graph.wireChildren(name, argumentNames) + graph.getNode(name).bindingStrategy = 'factory' + return factory.apply(factory, args) }) } - function createConstructorResolver(Constructor) { + function createConstructorResolver(name, Constructor) { return co.wrap(function* () { if (isPromise(Constructor)) { Constructor = yield Constructor @@ -44,6 +55,10 @@ function pluto() { const argumentNames = getArgumentNames(Constructor) const args = yield getAll(argumentNames) + // build injection graph + graph.wireChildren(name, argumentNames) + graph.getNode(name).bindingStrategy = 'constructor' + // For future reference, // this can be done with the spread operator in Node versions >= v5. e.g., // @@ -58,6 +73,9 @@ function pluto() { const get = memoize((name) => { return new Promise((resolve, reject) => { + // Add nodes to graph pre-emptively. We'll wire them together later. + graph.addNode(name) + const resolver = namesToResolvers.get(name) if (!resolver) { reject(new Error(`nothing is mapped for name '${name}'`)) @@ -109,17 +127,17 @@ function pluto() { return { toInstance: function (instance) { validateBinding(instance) - namesToResolvers.set(name, createInstanceResolver(instance)) + namesToResolvers.set(name, createInstanceResolver(name, instance)) }, toFactory: function (factory) { validateBinding(factory) validateTargetIsAFunctionOrPromise(factory) - namesToResolvers.set(name, createFactoryResolver(factory)) + namesToResolvers.set(name, createFactoryResolver(name, factory)) }, toConstructor: function (constructor) { validateBinding(constructor) validateTargetIsAFunctionOrPromise(constructor) - namesToResolvers.set(name, createConstructorResolver(constructor)) + namesToResolvers.set(name, createConstructorResolver(name, constructor)) } } } diff --git a/lib/plutoSpec.js b/lib/plutoSpec.js index d3973d7..9cd4977 100644 --- a/lib/plutoSpec.js +++ b/lib/plutoSpec.js @@ -359,3 +359,43 @@ test('when bootstrapped, injects the bootstrapped app itself under the name `plu const actual = greeter.greet() t.is(actual, 'Bonjour, World!') }) + +test('builds the application graph', function* (t) { + function greetFactory(greeting) { + return function greet() { + return `${greeting}, World!` + } + } + + class Greeter { + constructor(greet) { + this.greet = greet + } + } + + const bind = pluto() + bind('greeting').toInstance('Hello') + bind('greet').toFactory(greetFactory) + bind('greeter').toConstructor(Greeter) + + // bootstrap and sanity check system + const app = yield bind.bootstrap() + t.is(app.get('greeter').greet(), 'Hello, World!') + + const graph = app.get('plutoGraph') + const greeting = graph.getNode('greeting') + const greet = graph.getNode('greet') + const greeter = graph.getNode('greeter') + + // Spot-check one node in the graph in memory + t.is(greet.bindingStrategy, 'factory') + t.is(greet.name, 'greet') + t.is(greet.parents.get('greeter'), greeter) + t.is(greet.children.get('greeting'), greeting) + t.is(greet.isBuiltIn, false) + + // Check all values of all nodes by checking full JSON serialization + const expectedJson = require('./fixtures/greeterGraph') + const actualJson = graph.toJSON() + t.deepEqual(actualJson, expectedJson) +}) diff --git a/package.json b/package.json index 5dcef46..c712a84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pluto", - "version": "1.1.0", + "version": "1.2.0", "description": "Dependency injection that's so small, it almost doesn't count.", "homepage": "https://github.com/ecowden/pluto.js", "keywords": [