From 4ef97e0491fb1d987ce8333bc34bbecede4e4bf5 Mon Sep 17 00:00:00 2001 From: Evan Cowden Date: Thu, 25 May 2017 18:19:14 -0500 Subject: [PATCH 1/3] adds injection graph tracking --- README.md | 147 +++++++++++++++++++++++++++++++++ lib/Graph.js | 41 +++++++++ lib/GraphNode.js | 72 ++++++++++++++++ lib/fixtures/greeterGraph.json | 64 ++++++++++++++ lib/pluto.js | 30 +++++-- lib/plutoSpec.js | 42 ++++++++++ package.json | 2 +- 7 files changed, 391 insertions(+), 7 deletions(-) create mode 100644 lib/Graph.js create mode 100644 lib/GraphNode.js create mode 100644 lib/fixtures/greeterGraph.json diff --git a/README.md b/README.md index cd8b243..ca194fb 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,153 @@ 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** + +Returns 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. + +**.isRoot** + +Returns `true` if the node has zero parents. + +**.isLeaf** + +Returns `true` if the node has zero children. + +**.isBuiltIn** + +Returns true if the node is built in to pluto, 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", + "isRoot": true, + "isLeaf": true, + "isBuiltIn": true + }, + { + "name": "plutoBinder", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isRoot": true, + "isLeaf": true, + "isBuiltIn": true + }, + { + "name": "greeting", + "parents": [ + "greet" + ], + "children": [], + "bindingStrategy": "instance", + "isRoot": false, + "isLeaf": true, + "isBuiltIn": false + }, + { + "name": "greet", + "parents": [ + "greeter" + ], + "children": [ + "greeting" + ], + "bindingStrategy": "factory", + "isRoot": false, + "isLeaf": false, + "isBuiltIn": false + }, + { + "name": "greeter", + "parents": [], + "children": [ + "greet" + ], + "bindingStrategy": "constructor", + "isRoot": true, + "isLeaf": false, + "isBuiltIn": false + }, + { + "name": "plutoApp", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isRoot": true, + "isLeaf": true, + "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..d057eb6 --- /dev/null +++ b/lib/GraphNode.js @@ -0,0 +1,72 @@ +'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 // TODO should this return a copy? + } + get children() { + return this._internal.children // TODO should this return a copy? + } + + // a "Root" node has no parents + get isRoot() { + return this._internal.parents.size === 0 + } + + // a "Leaf" node is a node with no children + get isLeaf() { + return this._internal.children.size === 0 + } + + // return true if this is a component built in to pluto, like the plutoBinder + get isBuiltIn() { + return builtInObjectNames.includes(this._internal.name) + } + + 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, + isRoot: this.isRoot, + isLeaf: this.isLeaf, + 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..859c0de --- /dev/null +++ b/lib/fixtures/greeterGraph.json @@ -0,0 +1,64 @@ +[ + { + "name": "plutoGraph", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isRoot": true, + "isLeaf": true, + "isBuiltIn": true + }, + { + "name": "plutoBinder", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isRoot": true, + "isLeaf": true, + "isBuiltIn": true + }, + { + "name": "greeting", + "parents": [ + "greet" + ], + "children": [], + "bindingStrategy": "instance", + "isRoot": false, + "isLeaf": true, + "isBuiltIn": false + }, + { + "name": "greet", + "parents": [ + "greeter" + ], + "children": [ + "greeting" + ], + "bindingStrategy": "factory", + "isRoot": false, + "isLeaf": false, + "isBuiltIn": false + }, + { + "name": "greeter", + "parents": [], + "children": [ + "greet" + ], + "bindingStrategy": "constructor", + "isRoot": true, + "isLeaf": false, + "isBuiltIn": false + }, + { + "name": "plutoApp", + "parents": [], + "children": [], + "bindingStrategy": "instance", + "isRoot": true, + "isLeaf": true, + "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..b6b78b3 100644 --- a/lib/plutoSpec.js +++ b/lib/plutoSpec.js @@ -359,3 +359,45 @@ 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.isRoot, false) + t.is(greet.isLeaf, false) + 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": [ From 9abe08f7fd7dcffd2521b669fc670f93de418412 Mon Sep 17 00:00:00 2001 From: Evan Cowden Date: Thu, 25 May 2017 18:26:47 -0500 Subject: [PATCH 2/3] remove isLeaf and isRoot flags --- README.md | 24 ++---------------------- lib/GraphNode.js | 12 ------------ lib/fixtures/greeterGraph.json | 12 ------------ lib/plutoSpec.js | 2 -- 4 files changed, 2 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index ca194fb..ba67901 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ The `Graph` class has the following relevant methods: **.nodes** -Returns an `Array` of all `GraphNode`s. +An `Array` of all `GraphNode`s. **.getNode(name)** @@ -196,17 +196,9 @@ A `Map` of parent nodes, with names used for keys and `GraphNode` objects for va A `Map` of child nodes, with names used for keys and `GraphNode` objects for values. -**.isRoot** - -Returns `true` if the node has zero parents. - -**.isLeaf** - -Returns `true` if the node has zero children. - **.isBuiltIn** -Returns true if the node is built in to pluto, like the `plutoBinder`, `plutoApp`, or `plutoGraph` itself. +Returns true if the node is built in to Pluto.js, like the `plutoBinder`, `plutoApp`, or `plutoGraph` itself. ### JSON Representation @@ -219,8 +211,6 @@ The graph, when converted to JSON, will be represented as a flattened `Array` of "parents": [], "children": [], "bindingStrategy": "instance", - "isRoot": true, - "isLeaf": true, "isBuiltIn": true }, { @@ -228,8 +218,6 @@ The graph, when converted to JSON, will be represented as a flattened `Array` of "parents": [], "children": [], "bindingStrategy": "instance", - "isRoot": true, - "isLeaf": true, "isBuiltIn": true }, { @@ -239,8 +227,6 @@ The graph, when converted to JSON, will be represented as a flattened `Array` of ], "children": [], "bindingStrategy": "instance", - "isRoot": false, - "isLeaf": true, "isBuiltIn": false }, { @@ -252,8 +238,6 @@ The graph, when converted to JSON, will be represented as a flattened `Array` of "greeting" ], "bindingStrategy": "factory", - "isRoot": false, - "isLeaf": false, "isBuiltIn": false }, { @@ -263,8 +247,6 @@ The graph, when converted to JSON, will be represented as a flattened `Array` of "greet" ], "bindingStrategy": "constructor", - "isRoot": true, - "isLeaf": false, "isBuiltIn": false }, { @@ -272,8 +254,6 @@ The graph, when converted to JSON, will be represented as a flattened `Array` of "parents": [], "children": [], "bindingStrategy": "instance", - "isRoot": true, - "isLeaf": true, "isBuiltIn": true } ] diff --git a/lib/GraphNode.js b/lib/GraphNode.js index d057eb6..cdedd64 100644 --- a/lib/GraphNode.js +++ b/lib/GraphNode.js @@ -27,16 +27,6 @@ module.exports = class GraphNode { return this._internal.children // TODO should this return a copy? } - // a "Root" node has no parents - get isRoot() { - return this._internal.parents.size === 0 - } - - // a "Leaf" node is a node with no children - get isLeaf() { - return this._internal.children.size === 0 - } - // return true if this is a component built in to pluto, like the plutoBinder get isBuiltIn() { return builtInObjectNames.includes(this._internal.name) @@ -56,8 +46,6 @@ module.exports = class GraphNode { parents: [], children: [], bindingStrategy: this.bindingStrategy, - isRoot: this.isRoot, - isLeaf: this.isLeaf, isBuiltIn: this.isBuiltIn } diff --git a/lib/fixtures/greeterGraph.json b/lib/fixtures/greeterGraph.json index 859c0de..626be74 100644 --- a/lib/fixtures/greeterGraph.json +++ b/lib/fixtures/greeterGraph.json @@ -4,8 +4,6 @@ "parents": [], "children": [], "bindingStrategy": "instance", - "isRoot": true, - "isLeaf": true, "isBuiltIn": true }, { @@ -13,8 +11,6 @@ "parents": [], "children": [], "bindingStrategy": "instance", - "isRoot": true, - "isLeaf": true, "isBuiltIn": true }, { @@ -24,8 +20,6 @@ ], "children": [], "bindingStrategy": "instance", - "isRoot": false, - "isLeaf": true, "isBuiltIn": false }, { @@ -37,8 +31,6 @@ "greeting" ], "bindingStrategy": "factory", - "isRoot": false, - "isLeaf": false, "isBuiltIn": false }, { @@ -48,8 +40,6 @@ "greet" ], "bindingStrategy": "constructor", - "isRoot": true, - "isLeaf": false, "isBuiltIn": false }, { @@ -57,8 +47,6 @@ "parents": [], "children": [], "bindingStrategy": "instance", - "isRoot": true, - "isLeaf": true, "isBuiltIn": true } ] diff --git a/lib/plutoSpec.js b/lib/plutoSpec.js index b6b78b3..9cd4977 100644 --- a/lib/plutoSpec.js +++ b/lib/plutoSpec.js @@ -392,8 +392,6 @@ test('builds the application graph', function* (t) { t.is(greet.name, 'greet') t.is(greet.parents.get('greeter'), greeter) t.is(greet.children.get('greeting'), greeting) - t.is(greet.isRoot, false) - t.is(greet.isLeaf, false) t.is(greet.isBuiltIn, false) // Check all values of all nodes by checking full JSON serialization From af7d81da6e8c397656c7b66a1115f2e15a2ebeb0 Mon Sep 17 00:00:00 2001 From: Evan Cowden Date: Thu, 25 May 2017 18:45:28 -0500 Subject: [PATCH 3/3] removes use of Array.includes(...) to support Node v4 --- lib/GraphNode.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/GraphNode.js b/lib/GraphNode.js index cdedd64..b3ee6dc 100644 --- a/lib/GraphNode.js +++ b/lib/GraphNode.js @@ -21,15 +21,15 @@ module.exports = class GraphNode { } get parents() { - return this._internal.parents // TODO should this return a copy? + return this._internal.parents } get children() { - return this._internal.children // TODO should this return a copy? + return this._internal.children } // return true if this is a component built in to pluto, like the plutoBinder get isBuiltIn() { - return builtInObjectNames.includes(this._internal.name) + return builtInObjectNames.indexOf(this._internal.name) >= 0 } set bindingStrategy(bindingStrategy) {