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:
parent
aff4ddd4e3
commit
3acfefb904
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import stringify from "json-stable-stringify";
|
||||||
export opaque type NodeAddress = string;
|
export opaque type NodeAddress = string;
|
||||||
export opaque type EdgeAddress = string;
|
export opaque type EdgeAddress = string;
|
||||||
export type Edge = {|
|
export type Edge = {|
|
||||||
|
@ -20,22 +21,74 @@ export type NeighborsOptions = {|
|
||||||
|
|
||||||
export opaque type GraphJSON = any; // TODO
|
export opaque type GraphJSON = any; // TODO
|
||||||
|
|
||||||
export function toNodeAddress(arr: $ReadOnlyArray<string>): NodeAddress {
|
const NODE_PREFIX = "N";
|
||||||
const _ = arr;
|
const EDGE_PREFIX = "E";
|
||||||
throw new Error("toNodeAddress");
|
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[] {
|
export function fromNodeAddress(n: NodeAddress): string[] {
|
||||||
const _ = n;
|
assertNodeAddress(n);
|
||||||
throw new Error("fromNodeAddress");
|
const parts = n.split(SEPARATOR);
|
||||||
|
return parts.slice(1, parts.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toEdgeAddress(arr: $ReadOnlyArray<string>): EdgeAddress {
|
export function toEdgeAddress(arr: $ReadOnlyArray<string>): EdgeAddress {
|
||||||
const _ = arr;
|
assertAddressArray(arr);
|
||||||
throw new Error("toEdgeAddress");
|
return [EDGE_PREFIX, ...arr, ""].join(SEPARATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromEdgeAddress(n: EdgeAddress): string[] {
|
export function fromEdgeAddress(n: EdgeAddress): string[] {
|
||||||
const _ = n;
|
assertEdgeAddress(n);
|
||||||
throw new Error("fromEdgeAddress");
|
const parts = n.split(SEPARATOR);
|
||||||
|
return parts.slice(1, parts.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Graph {
|
export class Graph {
|
||||||
|
|
|
@ -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"]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue