Skip to content

Commit

Permalink
First commit - extract dependency-graph from asset-smasher
Browse files Browse the repository at this point in the history
  • Loading branch information
jriecken committed May 18, 2013
0 parents commit 1fdaf69
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ignore node_modules
node_modules
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Dependency Graph Changelog

## 0.1.0 (May 18, 2013)

- Initial Release - extracted out of asset-smasher
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (C) 2013 by Jim Riecken

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Dependency Graph

Simple dependency graph

## Overview

This is a simple dependency graph useful for determining the order to do a list of things that depend on certain items being done before they are.

To use, `npm install dependency-graph` and then `require('dependency-graph').DepGraph`

## API

### DepGraph

Nodes in the graph are just simple strings.

- `addNode(name)` - add a node in the graph
- `removeNode(name)` - remove a node from the graph
- `hasNode(name)` - check if a node exists in the graph
- `addDependency(from, to)` - add a dependency between two nodes (will throw an Error if one of the nodes does not exist)
- `removeDependency(from, to)` - remove a dependency between two nodes
- `dependenciesOf(name, leavesOnly)` - get an array containing the nodes that the specified node depends on (transitively). If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned in the array.
- `dependantsOf(name, leavesOnly)` - get an array containing the nodes that depend on the specified node (transitively). If `leavesOnly` is true, only nodes that do not have any dependants will be returned in the array.
- `overallOrder(leavesOnly)` - construct the overall processing order for the dependency graph. If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned.

## Examples

var DepGraph = require('dependency-graph').DepGraph;

var graph = new DepGraph();
graph.addNode('a');
graph.addNode('b');
graph.addNode('c');

graph.addDependency('a', 'b');
graph.addDependency('b', 'c');

graph.dependenciesOf('a'); // ['c', 'b']
graph.dependenciesOf('b'); // ['c']
graph.dependantsOf('c'); // ['a', 'b']

graph.overallOrder(); // ['c', 'b', 'a']
graph.overallOrder(true); // ['c']
131 changes: 131 additions & 0 deletions lib/dep_graph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* A simple dependency graph
*/
var _ = require('underscore');

/**
* Helper for creating a Depth-First-Search on
* a set of edges.
*
* Detects cycles and throws an Error if one is detected
*
* @param edges The set of edges to DFS through
* @param leavesOnly Whether to only return "leaf" nodes (ones who have no edges)
* @param result An array in which the results will be populated
*/
function createDFS(edges, leavesOnly, result) {
var chain = {};
var visited = {};
return function DFS(name) {
visited[name] = true;
chain[name] = true;
edges[name].forEach(function (edgeName) {
if (!visited[edgeName]) {
DFS(edgeName);
} else if (chain[edgeName]) {
throw new Error('Dependency Cycle Found: ' + edgeName);
}
});
chain[name] = false;
if ((!leavesOnly || edges[name].length === 0) && result.indexOf(name) === -1) {
result.push(name);
}
};
}

/**
* Simple Dependency Graph
*/
var DepGraph = exports.DepGraph = function DepGraph() {
this.nodes = {};
this.outgoingEdges = {}; // Node name -> [Dependency Node name]
this.incomingEdges = {}; // Node name -> [Dependant Node name]
};
DepGraph.prototype = {
addNode:function (name) {
this.nodes[name] = name;
this.outgoingEdges[name] = [];
this.incomingEdges[name] = [];
},
removeNode:function (name) {
delete this.nodes[name];
delete this.outgoingEdges[name];
delete this.incomingEdges[name];
_.each(this.incomingEdges, function (edges) {
var idx = edges.indexOf(name);
if (idx >= 0) {
edges.splice(idx, 1);
}
});
},
hasNode:function (name) {
return !!this.nodes[name];
},
addDependency:function (from, to) {
if (this.hasNode(from) && this.hasNode(to)) {
if (this.outgoingEdges[from].indexOf(to) === -1) {
this.outgoingEdges[from].push(to);
}
if (this.incomingEdges[to].indexOf(from) === -1) {
this.incomingEdges[to].push(from);
}
return true;
} else {
throw new Error('One of the nodes does not exist: ' + from + ', ' + to);
}
},
removeDependency:function (from, to) {
var idx = this.outgoingEdges[from].indexOf(to);
if (idx >= 0) {
this.outgoingEdges[from].splice(idx, 1);
}
idx = this.incomingEdges[to].indexOf(from);
if (idx >= 0) {
this.incomingEdges[to].splice(idx, 1);
}
},
dependenciesOf:function (name, leavesOnly) {
if (this.nodes[name]) {
var result = [];
var DFS = createDFS(this.outgoingEdges, leavesOnly, result);
DFS(name);
var idx = result.indexOf(name);
if (idx >= 0) {
result.splice(idx, 1);
}
return result;
}
else {
throw new Error('Node does not exist: ' + name);
}
},
dependantsOf:function (name, leavesOnly) {
if (this.nodes[name]) {
var result = [];
var DFS = createDFS(this.incomingEdges, leavesOnly, result);
DFS(name);
var idx = result.indexOf(name);
if (idx >= 0) {
result.splice(idx, 1);
}
return result;
} else {
throw new Error('Node does not exist: ' + name);
}
},
overallOrder:function (leavesOnly) {
var self = this;
var result = [];
var DFS = createDFS(this.outgoingEdges, leavesOnly, result);
_.each(_.filter(_.keys(this.nodes), function (node) {
return self.incomingEdges[node].length === 0;
}), function (n) {
DFS(n);
});
if (_.size(this.nodes) > 0 && result.length === 0) {
// Special case when there are no nodes with no dependants
throw new Error('Dependency Cycle Found');
}
return result;
}
};
35 changes: 35 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "dependency-graph",
"description": "Simple dependency graph.",
"version": "0.1.0",
"author": "Jim Riecken <[email protected]>",
"keywords": [
"dependency",
"graph"
],
"license": {
"type": "MIT",
"url": "http://github.com/jriecken/dependency-graph/raw/master/LICENSE"
},
"repository": {
"type": "git",
"url": "git://github.com/jriecken/dependency-graph.git"
},
"bugs": {
"url": "http://github.com/jriecken/dependency-graph/issues"
},
"main": "./lib/dep_graph.js",
"scripts": {
"test": "jasmine-node specs"
},
"dependencies": {
"underscore": "1.4.4"
},
"optionalDependencies": {},
"devDependencies": {
"jasmine-node": "1.7.1"
},
"engines": {
"node": ">= 0.6.0"
}
}
117 changes: 117 additions & 0 deletions specs/dep_graph_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
var DepGraph = require('../lib/dep_graph').DepGraph;

describe('DepGraph', function () {

it('should be able to add/remove nodes', function () {
var graph = new DepGraph();

graph.addNode('Foo');
graph.addNode('Bar');

expect(graph.hasNode('Foo')).toBe(true);
expect(graph.hasNode('Bar')).toBe(true);
expect(graph.hasNode('NotThere')).toBe(false);

graph.removeNode('Bar');

expect(graph.hasNode('Bar')).toBe(false);
});

it('should be able to add dependencies between nodes', function () {
var graph = new DepGraph();

graph.addNode('a');
graph.addNode('b');
graph.addNode('c');

graph.addDependency('a','b');
graph.addDependency('a','c');

expect(graph.dependenciesOf('a')).toEqual(['b', 'c']);
});

it('should throw an error if a node does not exist and a dependency is added', function () {
var graph = new DepGraph();

graph.addNode('a');

expect(function () {
graph.addDependency('a','b');
}).toThrow();
});

it('should detect cycles', function () {
var graph = new DepGraph();

graph.addNode('a');
graph.addNode('b');
graph.addNode('c');

graph.addDependency('a', 'b');
graph.addDependency('b', 'c');
graph.addDependency('c', 'a');

expect(function () {
graph.dependenciesOf('a');
}).toThrow();
});

it('should retrieve dependencies and dependants in the correct order', function () {
var graph = new DepGraph();

graph.addNode('a');
graph.addNode('b');
graph.addNode('c');
graph.addNode('d');

graph.addDependency('a', 'd');
graph.addDependency('a', 'b');
graph.addDependency('b', 'c');
graph.addDependency('d', 'b');

expect(graph.dependenciesOf('a')).toEqual(['c', 'b', 'd']);
expect(graph.dependenciesOf('b')).toEqual(['c']);
expect(graph.dependenciesOf('c')).toEqual([]);
expect(graph.dependenciesOf('d')).toEqual(['c', 'b']);

expect(graph.dependantsOf('a')).toEqual([]);
expect(graph.dependantsOf('b')).toEqual(['a','d']);
expect(graph.dependantsOf('c')).toEqual(['a','d','b']);
expect(graph.dependantsOf('d')).toEqual(['a']);
});

it('should be able to resolve the overall order of things', function () {
var graph = new DepGraph();

graph.addNode('a');
graph.addNode('b');
graph.addNode('c');
graph.addNode('d');
graph.addNode('e');

graph.addDependency('a', 'b');
graph.addDependency('a', 'c');
graph.addDependency('b', 'c');
graph.addDependency('c', 'd');

expect(graph.overallOrder()).toEqual(['d', 'c', 'b', 'a', 'e']);
});

it('should be able to only retrieve the "leaves" in the overall order', function () {
var graph = new DepGraph();

graph.addNode('a');
graph.addNode('b');
graph.addNode('c');
graph.addNode('d');
graph.addNode('e');

graph.addDependency('a', 'b');
graph.addDependency('a', 'c');
graph.addDependency('b', 'c');
graph.addDependency('c', 'd');

expect(graph.overallOrder(true)).toEqual(['d', 'e']);
});

});

0 comments on commit 1fdaf69

Please sign in to comment.