Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Injection Graph #34

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
127 changes: 127 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down
41 changes: 41 additions & 0 deletions 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
}
}
60 changes: 60 additions & 0 deletions 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
}
}
52 changes: 52 additions & 0 deletions 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
}
]
30 changes: 24 additions & 6 deletions lib/pluto.js
Expand Up @@ -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)
}
}
Expand All @@ -24,26 +30,35 @@ function pluto() {
return argumentNames || []
}

function createFactoryResolver(factory) {
function createFactoryResolver(name, factory) {
return co.wrap(function* () {
if (isPromise(factory)) {
factory = yield factory
}

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
}
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.,
//
Expand All @@ -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}'`))
Expand Down Expand Up @@ -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))
}
}
}
Expand Down