Implement {to,from}{Node,Edge}Address (#329) (#333)

This commit implements Node/Edge addresses, and helper functions for
generating and manipulating them.

Test plan: Unit tests included.

Paired with @wchargin
This commit is contained in:
Dandelion Mané 2018-06-02 18:56:21 -07:00 committed by GitHub
parent aff4ddd4e3
commit 3acfefb904
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 172 additions and 9 deletions

View File

@ -1,5 +1,6 @@
// @flow
import stringify from "json-stable-stringify";
export opaque type NodeAddress = string;
export opaque type EdgeAddress = string;
export type Edge = {|
@ -20,22 +21,74 @@ export type NeighborsOptions = {|
export opaque type GraphJSON = any; // TODO
export function toNodeAddress(arr: $ReadOnlyArray<string>): NodeAddress {
const _ = arr;
throw new Error("toNodeAddress");
const NODE_PREFIX = "N";
const EDGE_PREFIX = "E";
const SEPARATOR = "\0";
function isNodeAddress(x: string): boolean {
return x.startsWith(NODE_PREFIX) && x.endsWith(SEPARATOR);
}
function isEdgeAddress(x: string): boolean {
return x.startsWith(EDGE_PREFIX) && x.endsWith(SEPARATOR);
}
function assertNodeAddress(x: NodeAddress) {
if (x == null) {
throw new Error(String(x));
}
if (!isNodeAddress(x)) {
if (isEdgeAddress(x)) {
throw new Error(`Expected NodeAddress, got EdgeAddress: ${x}`);
}
throw new Error(`Malformed address: ${x}`);
}
}
function assertEdgeAddress(x: EdgeAddress) {
if (x == null) {
throw new Error(String(x));
}
if (!isEdgeAddress(x)) {
if (isNodeAddress(x)) {
throw new Error(`Expected EdgeAddress, got NodeAddress: ${x}`);
}
throw new Error(`Malformed address: ${x}`);
}
}
function assertAddressArray(arr: $ReadOnlyArray<string>) {
if (arr == null) {
throw new Error(String(arr));
}
arr.forEach((s: string) => {
if (s == null) {
throw new Error(`${String(s)} in ${stringify(arr)}`);
}
if (s.indexOf(SEPARATOR) !== -1) {
throw new Error(`NUL char: ${stringify(arr)}`);
}
});
}
export function toNodeAddress(arr: $ReadOnlyArray<string>): NodeAddress {
assertAddressArray(arr);
return [NODE_PREFIX, ...arr, ""].join(SEPARATOR);
}
export function fromNodeAddress(n: NodeAddress): string[] {
const _ = n;
throw new Error("fromNodeAddress");
assertNodeAddress(n);
const parts = n.split(SEPARATOR);
return parts.slice(1, parts.length - 1);
}
export function toEdgeAddress(arr: $ReadOnlyArray<string>): EdgeAddress {
const _ = arr;
throw new Error("toEdgeAddress");
assertAddressArray(arr);
return [EDGE_PREFIX, ...arr, ""].join(SEPARATOR);
}
export function fromEdgeAddress(n: EdgeAddress): string[] {
const _ = n;
throw new Error("fromEdgeAddress");
assertEdgeAddress(n);
const parts = n.split(SEPARATOR);
return parts.slice(1, parts.length - 1);
}
export class Graph {

110
src/v3/core/graph.test.js Normal file
View File

@ -0,0 +1,110 @@
// @flow
import {
toNodeAddress,
fromNodeAddress,
toEdgeAddress,
fromEdgeAddress,
} from "./graph";
describe("core/graph", () => {
describe("address functions", () => {
function throwOnNullOrUndefined(f) {
[null, undefined].forEach((bad) => {
it(`${f.name} throws on ${String(bad)}`, () => {
// $ExpectFlowError
expect(() => f(bad)).toThrow(String(bad));
});
});
}
describe("toNodeAddress & fromNodeAddress", () => {
throwOnNullOrUndefined(toNodeAddress);
throwOnNullOrUndefined(fromNodeAddress);
it("toNodeAddress errors on path containing NUL char", () => {
expect(() => toNodeAddress(["foo", "bar\0", "zoink"])).toThrow(
"NUL char"
);
});
[null, undefined].forEach((bad) => {
it(`toNodeAddress errors on path containing ${String(bad)}`, () => {
// $ExpectFlowError
expect(() => toNodeAddress(["foo", bad, "zoink"])).toThrow(
String(bad)
);
});
});
describe("compose to identity", () => {
function checkIdentity(name, example) {
it(name, () => {
expect(fromNodeAddress(toNodeAddress(example))).toEqual(example);
});
}
checkIdentity("on a simple example", ["an", "example"]);
describe("with an empty component", () => {
checkIdentity("at the start", ["", "example"]);
checkIdentity("in the middle", ["example", "", "foo"]);
checkIdentity("at the end", ["example", "", "foo", ""]);
});
checkIdentity("with an empty array", []);
});
it("fromNodeAddress errors if passed an edge address", () => {
// $ExpectFlowError
expect(() => fromNodeAddress(toEdgeAddress(["ex"]))).toThrow(
"EdgeAddress"
);
});
it("fromNodeAddress errors if passed a malformed string", () => {
// $ExpectFlowError
expect(() => fromNodeAddress("N/foo")).toThrow("Malformed");
});
});
describe("toEdgeAddress & fromEdgeAddress", () => {
throwOnNullOrUndefined(toEdgeAddress);
throwOnNullOrUndefined(fromEdgeAddress);
it("toEdgeAddress errors on path containing NUL char", () => {
expect(() => toEdgeAddress(["foo", "bar\0", "zoink"])).toThrow(
"NUL char"
);
});
[null, undefined].forEach((bad) => {
it(`toEdgeAddress errors on path containing ${String(bad)}`, () => {
// $ExpectFlowError
expect(() => toEdgeAddress(["foo", bad, "zoink"])).toThrow(
String(bad)
);
});
});
describe("compose to identity", () => {
function checkIdentity(name, example) {
it(name, () => {
expect(fromEdgeAddress(toEdgeAddress(example))).toEqual(example);
});
}
checkIdentity("on a simple example", ["an", "example"]);
describe("with an empty component", () => {
checkIdentity("at the start", ["", "example"]);
checkIdentity("in the middle", ["example", "", "foo"]);
checkIdentity("at the end", ["example", "", "foo", ""]);
});
checkIdentity("with an empty array", []);
});
it("fromEdgeAddress errors if passed an node address", () => {
// $ExpectFlowError
expect(() => fromEdgeAddress(toNodeAddress(["ex"]))).toThrow(
"NodeAddress"
);
});
it("fromEdgeAddress errors if passed a malformed string", () => {
// $ExpectFlowError
expect(() => fromEdgeAddress("E/foo")).toThrow("Malformed");
});
});
it("edge and node addresses are distinct", () => {
expect(toEdgeAddress([""])).not.toEqual(toNodeAddress([""]));
expect(toEdgeAddress(["foo"])).not.toEqual(toNodeAddress(["foo"]));
});
});
});