mirror of
https://github.com/status-im/sourcecred.git
synced 2025-01-11 21:24:35 +00:00
Create an AddressMap
abstraction (#67)
Summary: This commit reifies the concept of an `Addressable`, which is any object that has a covariant `address: Address` attribute, and implements a simple data structure for storing addressable items keyed against their addresses. Instances of `AddressMap` can replace the four fields of `Graph`: ```js _nodes: AddressMap<Node<mixed>>; _edges: AddressMap<Edge<mixed>>; _outEdges: AddressMap<{|+address: Address, +edges: Address[]|}>; _inEdges: AddressMap<{|+address: Address, +edges: Address[]|}>; ``` Test Plan: New unit tests included, with 100% coverage: `yarn flow && yarn test`. wchargin-branch: address-map
This commit is contained in:
parent
a798f9bac2
commit
a8da44c94b
14
src/backend/__snapshots__/address.test.js.snap
Normal file
14
src/backend/__snapshots__/address.test.js.snap
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`address AddressMap stringifies to JSON 1`] = `
|
||||||
|
Object {
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/suburbia\\",\\"pluginName\\":\\"houseville\\",\\"id\\":\\"mansion\\"}": Object {
|
||||||
|
"baths": 5,
|
||||||
|
"beds": 10,
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/suburbia\\",\\"pluginName\\":\\"houseville\\",\\"id\\":\\"mattressStore\\"}": Object {
|
||||||
|
"baths": 1,
|
||||||
|
"beds": 99,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
112
src/backend/address.js
Normal file
112
src/backend/address.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import deepEqual from "lodash.isequal";
|
||||||
|
|
||||||
|
export type Address = {|
|
||||||
|
+repositoryName: string,
|
||||||
|
+pluginName: string,
|
||||||
|
+id: string,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export interface Addressable {
|
||||||
|
+address: Address;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SansAddress<T: Addressable> = $Exact<$Diff<T, {+address: Address}>>;
|
||||||
|
|
||||||
|
export type AddressMapJSON<T: Addressable> = {
|
||||||
|
[serializedAddress: string]: SansAddress<T>,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data structure for storing addressable objects, keyed by their
|
||||||
|
* addresses.
|
||||||
|
*/
|
||||||
|
export class AddressMap<T: Addressable> {
|
||||||
|
// TODO(@wchargin): Evaluate performance gains from using a triple-map
|
||||||
|
// here. Cf. https://jsperf.com/address-string-302039074.
|
||||||
|
_data: {[serializedAddress: string]: T};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an empty `AddressMap`.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this._data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether this map logically equals another map. Two maps are
|
||||||
|
* logically equal if they contain the same keys and the values at
|
||||||
|
* each key are deep-equal.
|
||||||
|
*/
|
||||||
|
equals(that: AddressMap<T>): boolean {
|
||||||
|
return deepEqual(this._data, that._data);
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): AddressMapJSON<T> {
|
||||||
|
const result = {};
|
||||||
|
Object.keys(this._data).forEach((key) => {
|
||||||
|
const node = {...this._data[key]};
|
||||||
|
delete node.address;
|
||||||
|
result[key] = node;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON(json: AddressMapJSON<T>): AddressMap<T> {
|
||||||
|
const result: AddressMap<T> = new AddressMap();
|
||||||
|
Object.keys(json).forEach((key) => {
|
||||||
|
result._data[key] = {...json[key], address: JSON.parse(key)};
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given object to the map, replacing any existing value for
|
||||||
|
* the same address.
|
||||||
|
*
|
||||||
|
* Returns `this` for easy chaining.
|
||||||
|
*/
|
||||||
|
add(t: T): this {
|
||||||
|
if (t.address == null) {
|
||||||
|
throw new Error(`address is ${String(t.address)}`);
|
||||||
|
}
|
||||||
|
const key = JSON.stringify(t.address);
|
||||||
|
this._data[key] = t;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the object at the given address, if it exists, or `undefined`
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
get(address: Address): T {
|
||||||
|
if (address == null) {
|
||||||
|
throw new Error(`address is ${String(address)}`);
|
||||||
|
}
|
||||||
|
const key = JSON.stringify(address);
|
||||||
|
return this._data[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all objects stored in the map, in some unspecified order.
|
||||||
|
*/
|
||||||
|
getAll(): T[] {
|
||||||
|
return Object.keys(this._data).map((k) => this._data[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a copy of the given array and sort its elements by their
|
||||||
|
* addresses. The original array and its elements are not modified.
|
||||||
|
*/
|
||||||
|
export function sortedByAddress<T: Addressable>(xs: T[]) {
|
||||||
|
function cmp(x1: T, x2: T): -1 | 0 | 1 {
|
||||||
|
// TODO(@wchargin): This can be replaced by three string-comparisons
|
||||||
|
// to avoid stringifying.
|
||||||
|
const a1 = JSON.stringify(x1.address);
|
||||||
|
const a2 = JSON.stringify(x2.address);
|
||||||
|
return a1 > a2 ? 1 : a1 < a2 ? -1 : 0;
|
||||||
|
}
|
||||||
|
return xs.slice().sort(cmp);
|
||||||
|
}
|
154
src/backend/address.test.js
Normal file
154
src/backend/address.test.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import type {Address} from "./address";
|
||||||
|
import {AddressMap, sortedByAddress} from "./address";
|
||||||
|
|
||||||
|
describe("address", () => {
|
||||||
|
// Some test data using objects that have addresses, like houses.
|
||||||
|
type House = {|
|
||||||
|
+address: Address,
|
||||||
|
+beds: number,
|
||||||
|
+baths: number,
|
||||||
|
|};
|
||||||
|
function makeAddress(id: string): Address {
|
||||||
|
return {
|
||||||
|
repositoryName: "sourcecred/suburbia",
|
||||||
|
pluginName: "houseville",
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const mansion = (): House => ({
|
||||||
|
address: makeAddress("mansion"),
|
||||||
|
beds: 10,
|
||||||
|
baths: 5,
|
||||||
|
});
|
||||||
|
const fakeMansion = (): House => ({
|
||||||
|
// Same address, different content.
|
||||||
|
address: makeAddress("mansion"),
|
||||||
|
beds: 33,
|
||||||
|
baths: 88,
|
||||||
|
});
|
||||||
|
const mattressStore = (): House => ({
|
||||||
|
address: makeAddress("mattressStore"),
|
||||||
|
beds: 99,
|
||||||
|
baths: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("AddressMap", () => {
|
||||||
|
const makeMap = (): AddressMap<House> =>
|
||||||
|
new AddressMap().add(mansion()).add(mattressStore());
|
||||||
|
|
||||||
|
it("creates a simple map", () => {
|
||||||
|
makeMap();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets objects by key", () => {
|
||||||
|
expect(makeMap().get(mansion().address)).toEqual(mansion());
|
||||||
|
expect(makeMap().get(mattressStore().address)).toEqual(mattressStore());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets all objects, in some order", () => {
|
||||||
|
const actual = makeMap().getAll();
|
||||||
|
const expected = [mansion(), mattressStore()];
|
||||||
|
expect(sortedByAddress(actual)).toEqual(sortedByAddress(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stringifies to JSON", () => {
|
||||||
|
expect(makeMap().toJSON()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stringifies elements sans addresses", () => {
|
||||||
|
const json = makeMap().toJSON();
|
||||||
|
Object.keys(json).forEach((k) => {
|
||||||
|
const value = json[k];
|
||||||
|
expect(Object.keys(value).sort()).toEqual(["baths", "beds"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rehydrates elements with addresses", () => {
|
||||||
|
const newMap: AddressMap<House> = AddressMap.fromJSON(makeMap().toJSON());
|
||||||
|
newMap.getAll().forEach((house) => {
|
||||||
|
expect(Object.keys(house).sort()).toEqual(["address", "baths", "beds"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves equality over a JSON roundtrip", () => {
|
||||||
|
const result = AddressMap.fromJSON(makeMap().toJSON());
|
||||||
|
expect(result.equals(makeMap())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recognizes reference equality", () => {
|
||||||
|
const x = makeMap();
|
||||||
|
expect(x.equals(x)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recognizes deep equality", () => {
|
||||||
|
expect(makeMap().equals(makeMap())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recognizes equality invariant of construction order", () => {
|
||||||
|
const m1 = new AddressMap().add(mansion()).add(mattressStore());
|
||||||
|
const m2 = new AddressMap().add(mattressStore()).add(mansion());
|
||||||
|
expect(m1.equals(m2)).toBe(true);
|
||||||
|
expect(m2.equals(m1)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recognizes disequality when element lists differ", () => {
|
||||||
|
expect(makeMap().equals(new AddressMap())).toBe(false);
|
||||||
|
expect(new AddressMap().equals(makeMap())).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recognizes disequality when contents differ", () => {
|
||||||
|
const m1 = new AddressMap().add(mattressStore()).add(mansion());
|
||||||
|
const m2 = new AddressMap().add(mattressStore()).add(fakeMansion());
|
||||||
|
expect(m1.equals(m2)).toBe(false);
|
||||||
|
expect(m2.equals(m1)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("has nice error messages", () => {
|
||||||
|
[null, undefined].forEach((bad) => {
|
||||||
|
it(`when getting ${String(bad)} elements`, () => {
|
||||||
|
const message = `address is ${String(bad)}`;
|
||||||
|
expect(() => makeMap().get((bad: any))).toThrow(message);
|
||||||
|
});
|
||||||
|
it(`when adding elements with ${String(bad)} address`, () => {
|
||||||
|
const message = `address is ${String(bad)}`;
|
||||||
|
const element = {
|
||||||
|
address: (bad: any),
|
||||||
|
beds: 23,
|
||||||
|
baths: 45,
|
||||||
|
};
|
||||||
|
expect(() => makeMap().add(element)).toThrow(message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sortedByAddress", () => {
|
||||||
|
it("sorts the empty array", () => {
|
||||||
|
expect(sortedByAddress([])).toEqual([]);
|
||||||
|
});
|
||||||
|
it("sorts a sorted array", () => {
|
||||||
|
const input = () => [mansion(), mattressStore()];
|
||||||
|
const output = () => [mansion(), mattressStore()];
|
||||||
|
expect(sortedByAddress(input())).toEqual(output());
|
||||||
|
});
|
||||||
|
it("sorts a reverse-sorted array", () => {
|
||||||
|
const input = () => [mattressStore(), mansion()];
|
||||||
|
const output = () => [mansion(), mattressStore()];
|
||||||
|
expect(sortedByAddress(input())).toEqual(output());
|
||||||
|
});
|
||||||
|
it("sorts an array with duplicates", () => {
|
||||||
|
const input = () => [mattressStore(), mansion(), mattressStore()];
|
||||||
|
const output = () => [mansion(), mattressStore(), mattressStore()];
|
||||||
|
expect(sortedByAddress(input())).toEqual(output());
|
||||||
|
});
|
||||||
|
it("doesn't mutate its input", () => {
|
||||||
|
const input = () => [mattressStore(), mansion()];
|
||||||
|
const x = input();
|
||||||
|
expect(x).toEqual(input());
|
||||||
|
expect(sortedByAddress(x)).not.toEqual(input());
|
||||||
|
expect(x).toEqual(input());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user