Allow redundant adds to the Graph (#79)

Graph.addNode and Graph.addEdge now allow adding the same node or edge
multiple times, provided that the duplicate adds are trying to insert
identical content.

This came up while prototyping the GitHub plugin; rather than create
myriad subgraphs and merge them, I found it convenient to construct a
single graph and iteratively add nodes. Since the same node may be
discovered multiple times (most notably user identities), there was a
need for a "conservative add" abstraction that adds a node if it doesn't
exist yet, but errors only if multiple adds conflict.

Since this behavior is generic and highly conservative, it seemed
appropriate to include in the graph class itself.

Test Plan:
The unit tests have been updated to include the new behavior.
This commit is contained in:
Dandelion Mané 2018-03-17 00:28:17 -07:00 committed by GitHub
parent d2501947a6
commit 1e791782d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 39 additions and 13 deletions

View File

@ -70,11 +70,18 @@ export class Graph {
if (node == null) { if (node == null) {
throw new Error(`node is ${String(node)}`); throw new Error(`node is ${String(node)}`);
} }
if (this.getNode(node.address) !== undefined) { const existingNode = this.getNode(node.address);
if (existingNode !== undefined) {
if (deepEqual(existingNode, node)) {
return this;
} else {
throw new Error( throw new Error(
`node at address ${JSON.stringify(node.address)} already exists` `node at address ${JSON.stringify(
node.address
)} exists with distinct contents`
); );
} }
}
this._nodes.add(node); this._nodes.add(node);
this._outEdges.add({address: node.address, edges: []}); this._outEdges.add({address: node.address, edges: []});
this._inEdges.add({address: node.address, edges: []}); this._inEdges.add({address: node.address, edges: []});
@ -85,11 +92,18 @@ export class Graph {
if (edge == null) { if (edge == null) {
throw new Error(`edge is ${String(edge)}`); throw new Error(`edge is ${String(edge)}`);
} }
if (this.getEdge(edge.address) !== undefined) { const existingEdge = this.getEdge(edge.address);
if (existingEdge !== undefined) {
if (deepEqual(existingEdge, edge)) {
return this;
} else {
throw new Error( throw new Error(
`edge at address ${JSON.stringify(edge.address)} already exists` `edge at address ${JSON.stringify(
edge.address
)} exists with distinct contents`
); );
} }
}
if (this.getNode(edge.src) === undefined) { if (this.getNode(edge.src) === undefined) {
throw new Error(`source ${JSON.stringify(edge.src)} does not exist`); throw new Error(`source ${JSON.stringify(edge.src)} does not exist`);
} }

View File

@ -192,24 +192,36 @@ describe("graph", () => {
}); });
describe("creating nodes and edges", () => { describe("creating nodes and edges", () => {
it("forbids adding a node with existing address", () => { it("forbids adding a node with existing address and different contents", () => {
expect(() => expect(() =>
demoData.simpleMealGraph().addNode({ demoData.simpleMealGraph().addNode({
address: demoData.crabNode().address, address: demoData.crabNode().address,
payload: {anotherCrab: true}, payload: {anotherCrab: true},
}) })
).toThrow(/already exists/); ).toThrow(/exists with distinct contents/);
}); });
it("forbids adding an edge with existing address", () => { it("adding a node redundantly is a no-op", () => {
const simple1 = demoData.simpleMealGraph();
const simple2 = demoData.simpleMealGraph().addNode(demoData.heroNode());
expect(simple1.equals(simple2)).toBe(true);
});
it("forbids adding an edge with existing address and different contents", () => {
expect(() => expect(() =>
demoData.simpleMealGraph().addEdge({ demoData.simpleMealGraph().addEdge({
address: demoData.cookEdge().address, address: demoData.cookEdge().address,
src: demoData.crabNode().address, src: demoData.crabNode().address,
dst: demoData.crabNode().address, dst: demoData.crabNode().address,
payload: {}, payload: {isDifferent: true},
}) })
).toThrow(/already exists/); ).toThrow(/exists with distinct contents/);
});
it("adding an edge redundantly is a no-op", () => {
const simple1 = demoData.simpleMealGraph();
const simple2 = demoData.simpleMealGraph().addEdge(demoData.cookEdge());
expect(simple1.equals(simple2)).toBe(true);
}); });
it("allows creating self-loops", () => { it("allows creating self-loops", () => {