Add the `NodeReference.neighbors` implementation (#319)
The implementation is adapted from our previous implementation, but has been refactored to be more appropriate for our generator-function approach. The unit tests comprehensively explore a simple example graph, testing the following cases: - Direction filtering (IN/OUT/ANY/unspecified) - Node types - Edge types - Self-loop edges - Dangling edges - Removed edges don't appear Test plan: Carefully inspect the added unit tests. Paired with @wchargin
This commit is contained in:
parent
8182bb340c
commit
c7854c1154
|
@ -4,6 +4,7 @@ import deepEqual from "lodash.isequal";
|
||||||
import type {Address} from "./address";
|
import type {Address} from "./address";
|
||||||
import {AddressMap} from "./address";
|
import {AddressMap} from "./address";
|
||||||
import type {Compatible} from "../util/compat";
|
import type {Compatible} from "../util/compat";
|
||||||
|
import stringify from "json-stable-stringify";
|
||||||
|
|
||||||
export type Node<NR: NodeReference, NP: NodePayload> = {|
|
export type Node<NR: NodeReference, NP: NodePayload> = {|
|
||||||
+ref: NR,
|
+ref: NR,
|
||||||
|
@ -160,6 +161,10 @@ export class Graph {
|
||||||
if (!indexedEdge) {
|
if (!indexedEdge) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
return this._upgradeIndexedEdge(indexedEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
_upgradeIndexedEdge(indexedEdge: IndexedEdge): Edge<any> {
|
||||||
return {
|
return {
|
||||||
address: indexedEdge.address,
|
address: indexedEdge.address,
|
||||||
src: this._nodes[indexedEdge.srcIndex].address,
|
src: this._nodes[indexedEdge.srcIndex].address,
|
||||||
|
@ -174,12 +179,9 @@ export class Graph {
|
||||||
* If filter is provided, it will return only edges with the requested type.
|
* If filter is provided, it will return only edges with the requested type.
|
||||||
*/
|
*/
|
||||||
*edges(options?: PluginFilter): Iterator<Edge<any>> {
|
*edges(options?: PluginFilter): Iterator<Edge<any>> {
|
||||||
let edges = this._edges.getAll().map((indexedEdge) => ({
|
let edges = this._edges
|
||||||
address: indexedEdge.address,
|
.getAll()
|
||||||
src: this._nodes[indexedEdge.srcIndex].address,
|
.map((indexedEdge) => this._upgradeIndexedEdge(indexedEdge));
|
||||||
dst: this._nodes[indexedEdge.dstIndex].address,
|
|
||||||
payload: indexedEdge.payload,
|
|
||||||
}));
|
|
||||||
const filter = addressFilterer(options);
|
const filter = addressFilterer(options);
|
||||||
for (const edge of edges) {
|
for (const edge of edges) {
|
||||||
if (filter(edge.address)) {
|
if (filter(edge.address)) {
|
||||||
|
@ -433,10 +435,51 @@ class InternalReference implements NodeReference {
|
||||||
return this._graph._nodes[indexDatum.index].node;
|
return this._graph._nodes[indexDatum.index].node;
|
||||||
}
|
}
|
||||||
|
|
||||||
neighbors(
|
*neighbors(
|
||||||
options?: NeighborsOptions
|
options?: NeighborsOptions
|
||||||
): Iterator<{|+ref: NodeReference, +edge: Edge<any>|}> {
|
): Iterator<{|+ref: NodeReference, +edge: Edge<any>|}> {
|
||||||
const _ = options;
|
const indexDatum = this._graph._nodeIndices.get(this._address);
|
||||||
throw new Error("Not implemented");
|
if (indexDatum == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nodeIndex = indexDatum.index;
|
||||||
|
|
||||||
|
const direction = (options && options.direction) || "ANY";
|
||||||
|
const edgeFilter =
|
||||||
|
options == null ? (_) => true : addressFilterer(options.edge);
|
||||||
|
const nodeFilter =
|
||||||
|
options == null ? (_) => true : addressFilterer(options.node);
|
||||||
|
|
||||||
|
const graph = this._graph;
|
||||||
|
const adjacencies = [];
|
||||||
|
if (direction === "ANY" || direction === "IN") {
|
||||||
|
adjacencies.push({list: graph._inEdges[nodeIndex], direction: "IN"});
|
||||||
|
}
|
||||||
|
if (direction === "ANY" || direction === "OUT") {
|
||||||
|
adjacencies.push({list: graph._outEdges[nodeIndex], direction: "OUT"});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const adjacency of adjacencies) {
|
||||||
|
for (const edgeAddress of adjacency.list) {
|
||||||
|
const indexedEdge = graph._edges.get(edgeAddress);
|
||||||
|
if (indexedEdge == null) {
|
||||||
|
throw new Error(
|
||||||
|
`Edge at address ${stringify(edgeAddress)} does not exist`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (direction === "ANY" && adjacency.direction === "IN") {
|
||||||
|
if (indexedEdge.srcIndex === indexedEdge.dstIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const edge = graph._upgradeIndexedEdge(indexedEdge);
|
||||||
|
const ref = graph.ref(
|
||||||
|
adjacency.direction === "IN" ? edge.src : edge.dst
|
||||||
|
);
|
||||||
|
if (edgeFilter(edge.address) && nodeFilter(ref.address())) {
|
||||||
|
yield {edge, ref};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import stringify from "json-stable-stringify";
|
import stringify from "json-stable-stringify";
|
||||||
import sortBy from "lodash.sortby";
|
import sortBy from "lodash.sortby";
|
||||||
|
|
||||||
import type {Node} from "./graph";
|
import type {Node, Edge, NodeReference} from "./graph";
|
||||||
import {DelegateNodeReference, Graph} from "./graph";
|
import {DelegateNodeReference, Graph} from "./graph";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -71,6 +71,138 @@ describe("graph", () => {
|
||||||
expect(() => graph.ref((null: any))).toThrow("null");
|
expect(() => graph.ref((null: any))).toThrow("null");
|
||||||
expect(() => graph.ref((undefined: any))).toThrow("undefined");
|
expect(() => graph.ref((undefined: any))).toThrow("undefined");
|
||||||
});
|
});
|
||||||
|
describe("neighbors", () => {
|
||||||
|
// Note: The tests share more state within this block than in the rest of the code.
|
||||||
|
// It should be fine so long as neighbors never mutates the graph.
|
||||||
|
const edge = (id, src, dst, type = "EDGE") => ({
|
||||||
|
address: {id, owner: {plugin: EXAMPLE_PLUGIN_NAME, type}},
|
||||||
|
src: src.address(),
|
||||||
|
dst: dst.address(),
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
// A cute little diagram:
|
||||||
|
// A>B = edge from A to B
|
||||||
|
// Nodes in the graph: bar, foo, isolated
|
||||||
|
// (Parens) = Repeated or absent node
|
||||||
|
//
|
||||||
|
// bar
|
||||||
|
// V
|
||||||
|
// foo > (foo)
|
||||||
|
// V
|
||||||
|
// (absent) isolated
|
||||||
|
const foo = new FooPayload();
|
||||||
|
const bar = new BarPayload(1, "hello");
|
||||||
|
const isolated = new BarPayload(666, "ghost");
|
||||||
|
const absent = new BarPayload(404, "hello");
|
||||||
|
|
||||||
|
const bar_foo = edge("bar_foo", bar, foo);
|
||||||
|
const foo_foo = edge("foo_foo", foo, foo, "SELF");
|
||||||
|
const foo_absent = edge("foo_absent", foo, absent);
|
||||||
|
const phantomEdge = edge("spooky", foo, isolated);
|
||||||
|
const graph = newGraph()
|
||||||
|
.addNode(bar)
|
||||||
|
.addNode(foo)
|
||||||
|
.addNode(isolated)
|
||||||
|
.addEdge(bar_foo)
|
||||||
|
.addEdge(foo_foo)
|
||||||
|
.addEdge(foo_absent)
|
||||||
|
.addEdge(phantomEdge)
|
||||||
|
.removeEdge(phantomEdge.address);
|
||||||
|
|
||||||
|
const refFor = (x) => graph.ref(x.address());
|
||||||
|
|
||||||
|
const fooNeighbor = {
|
||||||
|
bar: {edge: bar_foo, ref: refFor(bar)},
|
||||||
|
absent: {edge: foo_absent, ref: refFor(absent)},
|
||||||
|
foo: {edge: foo_foo, ref: refFor(foo)},
|
||||||
|
};
|
||||||
|
|
||||||
|
function expectNeighborsEqual(
|
||||||
|
actual: Iterable<{|+edge: Edge<any>, +ref: NodeReference|}>,
|
||||||
|
expected: {|+edge: Edge<any>, +ref: NodeReference|}[]
|
||||||
|
) {
|
||||||
|
const sort = (xs) => sortBy(xs, (x) => stringify(x.edge.address));
|
||||||
|
expect(sort(Array.from(actual))).toEqual(sort(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
it("ref in empty graph has no neighbors", () => {
|
||||||
|
const ref = newGraph().ref(foo.address());
|
||||||
|
expectNeighborsEqual(ref.neighbors(), []);
|
||||||
|
});
|
||||||
|
it("graph with no edges has no neighbors", () => {
|
||||||
|
const g = newGraph()
|
||||||
|
.addNode(foo)
|
||||||
|
.addNode(bar);
|
||||||
|
const ref = g.ref(foo.address());
|
||||||
|
expectNeighborsEqual(ref.neighbors(), []);
|
||||||
|
});
|
||||||
|
it("finds neighbors for an absent node", () => {
|
||||||
|
expectNeighborsEqual(refFor(absent).neighbors(), [
|
||||||
|
{edge: foo_absent, ref: refFor(foo)},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
describe("filters by direction:", () => {
|
||||||
|
[
|
||||||
|
["IN", [fooNeighbor.bar, fooNeighbor.foo]],
|
||||||
|
["OUT", [fooNeighbor.absent, fooNeighbor.foo]],
|
||||||
|
["ANY", [fooNeighbor.bar, fooNeighbor.absent, fooNeighbor.foo]],
|
||||||
|
[
|
||||||
|
"unspecified",
|
||||||
|
[fooNeighbor.bar, fooNeighbor.absent, fooNeighbor.foo],
|
||||||
|
],
|
||||||
|
].forEach(([direction, expectedNeighbors]) => {
|
||||||
|
it(direction, () => {
|
||||||
|
const options = direction === "unspecified" ? {} : {direction};
|
||||||
|
expectNeighborsEqual(
|
||||||
|
refFor(foo).neighbors(options),
|
||||||
|
expectedNeighbors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("filters edges by type:", () => {
|
||||||
|
[
|
||||||
|
["EDGE", [fooNeighbor.bar, fooNeighbor.absent]],
|
||||||
|
["SELF", [fooNeighbor.foo]],
|
||||||
|
[
|
||||||
|
"unspecified",
|
||||||
|
[fooNeighbor.bar, fooNeighbor.absent, fooNeighbor.foo],
|
||||||
|
],
|
||||||
|
].forEach(([type, expectedNeighbors]) => {
|
||||||
|
it(type, () => {
|
||||||
|
const options =
|
||||||
|
type === "unspecified"
|
||||||
|
? {}
|
||||||
|
: {edge: {plugin: EXAMPLE_PLUGIN_NAME, type}};
|
||||||
|
expectNeighborsEqual(
|
||||||
|
refFor(foo).neighbors(options),
|
||||||
|
expectedNeighbors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("filters nodes by type:", () => {
|
||||||
|
[
|
||||||
|
["FOO", [fooNeighbor.foo]],
|
||||||
|
["BAR", [fooNeighbor.bar, fooNeighbor.absent]],
|
||||||
|
[
|
||||||
|
"unspecified",
|
||||||
|
[fooNeighbor.bar, fooNeighbor.absent, fooNeighbor.foo],
|
||||||
|
],
|
||||||
|
].forEach(([type, expectedNeighbors]) => {
|
||||||
|
it(type, () => {
|
||||||
|
const options =
|
||||||
|
type === "unspecified"
|
||||||
|
? {}
|
||||||
|
: {node: {plugin: EXAMPLE_PLUGIN_NAME, type}};
|
||||||
|
expectNeighborsEqual(
|
||||||
|
refFor(foo).neighbors(options),
|
||||||
|
expectedNeighbors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("node", () => {
|
describe("node", () => {
|
||||||
|
|
Loading…
Reference in New Issue