Add `NodeReference` and `NodePorcelain` to core (#286)

`NodeReference` and `NodePorcelain` act as abstractions over the two
states a Node can be in.

- We might have the address of a node (because some edge pointed to it),
but don't actually have the Node in the graph. In that case, we can do
some queries on the node (e.g. find its neighbors), but can't access
the payload. This corresponds to having a `NodeReference`.

- We might have the node in the graph. In that case, we can access a
`NodePorcelain`.

The main benefit this abstraction brings is type-safety over accessing
data from a `NodePayload`. Previously, the coding conventions encouraged
clients to ignore the distinction, and the type signatures incorrectly
reported that many payload-level properties were non-nullable. Now, the
`get` method that mapp a `Reference` to a `Payload` is explicilty
nullable.

Given a `NodePorcelain`, it's always possible to retrieve the reference
via `ref()`. Given the `NodeReference`, you might be able to retrieve
the `NodePorcelain` via `get()`.

Clients that subtype `NodePorcelain` and `NodeReference` should, in
general, override the `ref()` and `get()` methods to return their
subtype. We also recommend having subclasses overwrite the constructors
to take a base `NodePorcelain` and `NodeReference` respectively
(although the base classes take a `Graph` and `address` as constructor
arguments).

Test Plan: Inspect the unit tests, they are pretty thorough.

Paired with @wchargin
This commit is contained in:
Dandelion Mané 2018-05-15 17:25:20 -07:00 committed by GitHub
parent f31d2c517d
commit 7ccef98c87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 155 additions and 0 deletions

68
src/core/porcelain.js Normal file
View File

@ -0,0 +1,68 @@
// @flow
import type {Address} from "./address";
import type {Edge, Graph, Node} from "./graph";
export class NodeReference<+T> {
_graph: Graph<any, any>;
_address: Address;
constructor(g: Graph<any, any>, a: Address) {
this._graph = g;
this._address = a;
}
neighbors(options?: {|
+nodeType?: string,
+edgeType?: string,
+direction?: "IN" | "OUT" | "ANY",
|}): {|+ref: NodeReference<any>, edge: Edge<any>|}[] {
return this._graph
.neighborhood(this._address, options)
.map(({neighbor, edge}) => ({
ref: new NodeReference(this._graph, neighbor),
edge,
}));
}
graph(): Graph<any, any> {
return this._graph;
}
address(): Address {
return this._address;
}
type(): string {
return this._address.type;
}
get(): ?NodePorcelain<T> {
const node = this._graph.node(this._address);
if (node != null) {
return new NodePorcelain(this, node);
}
}
}
export class NodePorcelain<+T> {
+_ref: NodeReference<T>;
+_node: Node<T>;
constructor(ref: NodeReference<T>, n: Node<T>) {
this._ref = ref;
this._node = n;
}
node(): Node<T> {
return this._node;
}
payload(): T {
return this._node.payload;
}
ref(): NodeReference<T> {
return this._ref;
}
}

View File

@ -0,0 +1,87 @@
// @flow
import {NodePorcelain, NodeReference} from "./porcelain";
import * as demoData from "./graphDemoData";
function exampleStuff() {
const graph = demoData.advancedMealGraph();
const heroNode = demoData.heroNode();
const heroReference = new NodeReference(graph, heroNode.address);
const heroPorcelain = new NodePorcelain(heroReference, heroNode);
const fakeAddress = demoData.makeAddress(
"I do not exist",
"minion of Magnificent Foo Plugin"
);
const fakeReference = new NodeReference(graph, fakeAddress);
return {
graph,
heroNode,
heroReference,
heroPorcelain,
fakeAddress,
fakeReference,
};
}
describe("NodeReference", () => {
it("can retrieve graph", () => {
const {graph, heroReference, fakeReference} = exampleStuff();
expect(heroReference.graph()).toBe(graph);
expect(fakeReference.graph()).toBe(graph);
});
it("can retrieve address", () => {
const {
heroReference,
fakeReference,
fakeAddress,
heroNode,
} = exampleStuff();
expect(heroReference.address()).toEqual(heroNode.address);
expect(fakeReference.address()).toEqual(fakeAddress);
});
it("can retrieve type", () => {
const {
heroReference,
fakeReference,
fakeAddress,
heroNode,
} = exampleStuff();
expect(heroReference.type()).toBe(heroNode.address.type);
expect(fakeReference.type()).toBe(fakeAddress.type);
});
it("can retrieve porcelain", () => {
const {heroReference, heroPorcelain, fakeReference} = exampleStuff();
expect(heroReference.get()).toEqual(heroPorcelain);
expect(fakeReference.get()).toEqual(undefined);
});
it("can retrieve neighbors", () => {
const {heroReference, fakeReference, graph} = exampleStuff();
expect(
heroReference
.neighbors()
.map(({edge, ref}) => ({edge, neighbor: ref.address()}))
).toEqual(graph.neighborhood(heroReference.address()));
expect(fakeReference.neighbors()).toEqual([]);
});
});
describe("NodePorcelain", () => {
it("can get node", () => {
const {heroNode, heroPorcelain} = exampleStuff();
expect(heroPorcelain.node()).toEqual(heroNode);
});
it("can get payload", () => {
const {heroNode, heroPorcelain} = exampleStuff();
expect(heroPorcelain.payload()).toEqual(heroNode.payload);
});
it("can get ref", () => {
const {heroPorcelain, heroReference} = exampleStuff();
expect(heroPorcelain.ref()).toEqual(heroReference);
});
});