Add `addressAppend` (#332)

Consider a case where a user wants to construct many addresses sharing a
common prefix. For example, every GitHub entity may have an address
starting with the component sequence `["sourcecred", "github"]`.

Currently, users can implement this with `toParts`:

```js
const GITHUB_ADDRESS = Address.nodeAddress(["sourcecred", "github"]);
function createGithubAddress(ids: $ReadOnlyArray<string>): NodeAddress {
  return nodeAddress([...Address.toParts(GITHUB_ADDRESS), ...ids]);
}
```

This is unfortunate from a performance standpoint, as we needlessly
perform string and array operations, when under the hood this is
basically a string concatenation.

This commit fixes this by adding functions `nodeAppend` and
`edgeAppend`, which take a node (resp. edge) and some components to
append, returning a new address. Consider how straightforward our
example case becomes:

```js
const GITHUB_NODE_PREFIX = Address.nodeAddress(["sourcecred", "github"]);
const ISSUE_NODE_PREFIX = Address.nodeAppend(GITHUB_NODE_PREFIX, "issue");

// There is no longer any need for an explicit creation function
const anIssueAddress = Address.nodeAppend(ISSUE_NODE_PREFIX, someIssueID);
```

Paired with @wchargin.

Test Plan:
Unit tests added; run `yarn travis`.
This commit is contained in:
Dandelion Mané 2018-06-04 15:19:24 -07:00 committed by GitHub
parent bd28030caa
commit e092b32bca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 90 additions and 3 deletions

View File

@ -86,14 +86,18 @@ export function assertAddressArray(arr: $ReadOnlyArray<string>) {
}); });
} }
function nullDelimited(components: $ReadOnlyArray<string>): string {
return [...components, ""].join(SEPARATOR);
}
export function nodeAddress(arr: $ReadOnlyArray<string>): NodeAddress { export function nodeAddress(arr: $ReadOnlyArray<string>): NodeAddress {
assertAddressArray(arr); assertAddressArray(arr);
return [NODE_PREFIX, ...arr, ""].join(SEPARATOR); return NODE_PREFIX + SEPARATOR + nullDelimited(arr);
} }
export function edgeAddress(arr: $ReadOnlyArray<string>): EdgeAddress { export function edgeAddress(arr: $ReadOnlyArray<string>): EdgeAddress {
assertAddressArray(arr); assertAddressArray(arr);
return [EDGE_PREFIX, ...arr, ""].join(SEPARATOR); return EDGE_PREFIX + SEPARATOR + nullDelimited(arr);
} }
export function toParts(a: GenericAddress): string[] { export function toParts(a: GenericAddress): string[] {
@ -101,3 +105,21 @@ export function toParts(a: GenericAddress): string[] {
const parts = a.split(SEPARATOR); const parts = a.split(SEPARATOR);
return parts.slice(1, parts.length - 1); return parts.slice(1, parts.length - 1);
} }
export function nodeAppend(
base: NodeAddress,
...components: string[]
): NodeAddress {
assertNodeAddress(base);
assertAddressArray(components);
return base + nullDelimited(components);
}
export function edgeAppend(
base: EdgeAddress,
...components: string[]
): EdgeAddress {
assertEdgeAddress(base);
assertAddressArray(components);
return base + nullDelimited(components);
}

View File

@ -1,10 +1,13 @@
// @flow // @flow
import type {NodeAddress, EdgeAddress} from "./_address";
import { import {
assertNodeAddress, assertNodeAddress,
assertEdgeAddress, assertEdgeAddress,
nodeAddress,
edgeAddress, edgeAddress,
edgeAppend,
nodeAddress,
nodeAppend,
toParts, toParts,
} from "./_address"; } from "./_address";
@ -79,6 +82,68 @@ describe("core/address", () => {
}); });
}); });
function checkAppend<
Good: NodeAddress | EdgeAddress,
Bad: NodeAddress | EdgeAddress
>(
f: (Good, ...string[]) => Good,
goodConstructor: (string[]) => Good,
badConstructor: (string[]) => Bad
) {
describe(f.name, () => {
describe("errors on", () => {
[null, undefined].forEach((bad) => {
it(`${String(bad)} base input`, () => {
// $ExpectFlowError
expect(() => f(bad, "foo")).toThrow(String(bad));
});
it(`${String(bad)} component`, () => {
// $ExpectFlowError
expect(() => f(goodConstructor(["foo"]), bad)).toThrow(String(bad));
});
});
it("malformed base", () => {
// $ExpectFlowError
expect(() => f("foo", "foo")).toThrow("Bad address");
});
it("base of wrong kind", () => {
// $ExpectFlowError
expect(() => f(badConstructor(["foo"]), "foo")).toThrow(
/Expected.*Address/
);
});
it("invalid component", () => {
expect(() => f(goodConstructor(["foo"]), "foo\0oo"));
});
});
describe("works on", () => {
function check(
description: string,
baseComponents: string[],
...components: string[]
) {
test(description, () => {
const base = goodConstructor(baseComponents);
const expectedParts = [...baseComponents, ...components];
expect(toParts(f(base, ...components))).toEqual(expectedParts);
});
}
check("the base address with no extra component", []);
check("the base address with empty component", [], "");
check("the base address with nonempty component", [], "a");
check("the base address with lots of components", [], "a", "b");
check("a longer address with no extra component", ["a", ""]);
check("a longer address with empty component", ["a", ""], "");
check("a longer address with nonempty component", ["a", ""], "b");
check("a longer address with lots of components", ["a", ""], "b", "c");
});
});
}
checkAppend(nodeAppend, nodeAddress, edgeAddress);
checkAppend(edgeAppend, edgeAddress, nodeAddress);
describe("type assertions", () => { describe("type assertions", () => {
function checkAssertion(f, good, bad, badMsg) { function checkAssertion(f, good, bad, badMsg) {
describe(f.name, () => { describe(f.name, () => {