From 3acfefb9040683a5edd26dec52efd1c2f2717aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Sat, 2 Jun 2018 18:56:21 -0700 Subject: [PATCH] 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 --- src/v3/core/graph.js | 71 ++++++++++++++++++++---- src/v3/core/graph.test.js | 110 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 src/v3/core/graph.test.js diff --git a/src/v3/core/graph.js b/src/v3/core/graph.js index 96b6902..a5a4015 100644 --- a/src/v3/core/graph.js +++ b/src/v3/core/graph.js @@ -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): 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) { + 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): 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): 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 { diff --git a/src/v3/core/graph.test.js b/src/v3/core/graph.test.js new file mode 100644 index 0000000..aba9d53 --- /dev/null +++ b/src/v3/core/graph.test.js @@ -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"])); + }); + }); +});