Implement `Graph.toJSON` and `Graph.fromJSON` (#374)
The serialization scheme uses `IndexedEdge`s: ```js type Integer = number; type IndexedEdge = {| Address: EdgeAddressT, srcIndex: Integer, dstIndex: Integer, |} ``` The nodes are first sorted. Then, we generate indexed edges from the regular edges by replacing each node address with its index in the sorted order. This encoding reduces the number of addresses serialized from `n + 3e` to `n + e` (where `n` is the number of nodes and `e` is the number of edges). This is based on work in #295, but in contrast to that PR, we do not index the in-memory representations of graphs. Only the JSON representation is indexed. Test plan: Unit tests added. A snapshot test is also included, both to make it easy to inspect an example of a JSON-serialized graph, and to ensure backwards-compatibility. (The snapshot likely should not change independent of the VERSION string.)
This commit is contained in:
parent
6177f6c740
commit
5fc0d42c1f
|
@ -0,0 +1,37 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`core/graph toJSON / fromJSON toJSON matches snapshot 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"type": "sourcecred/graph",
|
||||
"version": "0.4.0",
|
||||
},
|
||||
Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"address": Array [
|
||||
"edge",
|
||||
],
|
||||
"dstIndex": 0,
|
||||
"srcIndex": 1,
|
||||
},
|
||||
Object {
|
||||
"address": Array [
|
||||
"edge",
|
||||
"2",
|
||||
],
|
||||
"dstIndex": 1,
|
||||
"srcIndex": 0,
|
||||
},
|
||||
],
|
||||
"nodes": Array [
|
||||
Array [
|
||||
"dst",
|
||||
],
|
||||
Array [
|
||||
"src",
|
||||
],
|
||||
],
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -1,9 +1,11 @@
|
|||
// @flow
|
||||
|
||||
import deepEqual from "lodash.isequal";
|
||||
import sortBy from "lodash.sortby";
|
||||
|
||||
import {makeAddressModule, type AddressModule} from "./address";
|
||||
|
||||
import {toCompat, fromCompat, type Compatible} from "../util/compat";
|
||||
export opaque type NodeAddressT: string = string;
|
||||
export opaque type EdgeAddressT: string = string;
|
||||
export const NodeAddress: AddressModule<NodeAddressT> = (makeAddressModule({
|
||||
|
@ -23,6 +25,8 @@ export type Edge = {|
|
|||
+dst: NodeAddressT,
|
||||
|};
|
||||
|
||||
const COMPAT_INFO = {type: "sourcecred/graph", version: "0.4.0"};
|
||||
|
||||
export type Neighbor = {|+node: NodeAddressT, +edge: Edge|};
|
||||
|
||||
export opaque type DirectionT = Symbol;
|
||||
|
@ -42,7 +46,18 @@ export type NeighborsOptions = {|
|
|||
+edgePrefix: EdgeAddressT,
|
||||
|};
|
||||
|
||||
export opaque type GraphJSON = any; // TODO
|
||||
type AddressJSON = string[]; // Result of calling {Node,Edge}Address.toParts
|
||||
type Integer = number;
|
||||
type IndexedEdgeJSON = {|
|
||||
+address: AddressJSON,
|
||||
+srcIndex: Integer,
|
||||
+dstIndex: Integer,
|
||||
|};
|
||||
|
||||
export opaque type GraphJSON = Compatible<{|
|
||||
+nodes: AddressJSON[],
|
||||
+edges: IndexedEdgeJSON[],
|
||||
|}>;
|
||||
|
||||
type ModificationCount = number;
|
||||
|
||||
|
@ -316,12 +331,38 @@ export class Graph {
|
|||
}
|
||||
|
||||
toJSON(): GraphJSON {
|
||||
throw new Error("toJSON");
|
||||
const sortedNodes = Array.from(this.nodes()).sort();
|
||||
const nodeToSortedIndex = new Map();
|
||||
sortedNodes.forEach((node, i) => {
|
||||
nodeToSortedIndex.set(node, i);
|
||||
});
|
||||
const sortedEdges = sortBy(Array.from(this.edges()), (x) => x.address);
|
||||
const indexedEdges = sortedEdges.map(({src, dst, address}) => {
|
||||
const srcIndex = nodeToSortedIndex.get(src);
|
||||
const dstIndex = nodeToSortedIndex.get(dst);
|
||||
if (srcIndex == null || dstIndex == null) {
|
||||
throw new Error(`Invariant violation`);
|
||||
}
|
||||
return {srcIndex, dstIndex, address: EdgeAddress.toParts(address)};
|
||||
});
|
||||
const rawJSON = {
|
||||
nodes: sortedNodes.map((x) => NodeAddress.toParts(x)),
|
||||
edges: indexedEdges,
|
||||
};
|
||||
return toCompat(COMPAT_INFO, rawJSON);
|
||||
}
|
||||
|
||||
static fromJSON(json: GraphJSON): Graph {
|
||||
const _ = json;
|
||||
throw new Error("fromJSON");
|
||||
const {nodes: nodesJSON, edges} = fromCompat(COMPAT_INFO, json);
|
||||
const result = new Graph();
|
||||
const nodes = nodesJSON.map((x) => NodeAddress.fromParts(x));
|
||||
nodes.forEach((n) => result.addNode(n));
|
||||
edges.forEach(({address, srcIndex, dstIndex}) => {
|
||||
const src = nodes[srcIndex];
|
||||
const dst = nodes[dstIndex];
|
||||
result.addEdge({address: EdgeAddress.fromParts(address), src, dst});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
static merge(graphs: Iterable<Graph>): Graph {
|
||||
|
|
|
@ -947,6 +947,96 @@ describe("core/graph", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("toJSON / fromJSON", () => {
|
||||
const src = NodeAddress.fromParts(["src"]);
|
||||
const dst = NodeAddress.fromParts(["dst"]);
|
||||
const edge1 = () => ({
|
||||
src,
|
||||
dst,
|
||||
address: EdgeAddress.fromParts(["edge"]),
|
||||
});
|
||||
const edge2 = () => ({
|
||||
src: dst,
|
||||
dst: src,
|
||||
address: EdgeAddress.fromParts(["edge", "2"]),
|
||||
});
|
||||
it("toJSON matches snapshot", () => {
|
||||
const graph = new Graph()
|
||||
.addNode(src)
|
||||
.addNode(dst)
|
||||
.addEdge(edge1())
|
||||
.addEdge(edge2());
|
||||
expect(graph.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("compose to identity", () => {
|
||||
function expectCompose(g) {
|
||||
const json = g.toJSON();
|
||||
const newGraph = Graph.fromJSON(json);
|
||||
const newJSON = newGraph.toJSON();
|
||||
expect(newGraph.equals(g)).toBe(true);
|
||||
expect(newJSON).toEqual(json);
|
||||
}
|
||||
it("for an empty graph", () => {
|
||||
expectCompose(new Graph());
|
||||
});
|
||||
it("for a graph with some nodes", () => {
|
||||
const g = new Graph().addNode(src).addNode(dst);
|
||||
expectCompose(g);
|
||||
});
|
||||
it("for a graph with nodes added and removed", () => {
|
||||
const g = new Graph()
|
||||
.addNode(src)
|
||||
.addNode(dst)
|
||||
.removeNode(src);
|
||||
expectCompose(g);
|
||||
});
|
||||
it("for a graph with nodes and edges", () => {
|
||||
const g = new Graph()
|
||||
.addNode(src)
|
||||
.addNode(dst)
|
||||
.addEdge(edge1())
|
||||
.addEdge(edge2());
|
||||
expectCompose(g);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toJSON representation is canonical", () => {
|
||||
function expectCanonicity(g1, g2) {
|
||||
expect(g1.toJSON()).toEqual(g2.toJSON());
|
||||
}
|
||||
it("for an empty graph", () => {
|
||||
expectCanonicity(new Graph(), new Graph());
|
||||
});
|
||||
it("for graph with nodes added in different order", () => {
|
||||
const g1 = new Graph().addNode(src).addNode(dst);
|
||||
const g2 = new Graph().addNode(dst).addNode(src);
|
||||
expectCanonicity(g1, g2);
|
||||
});
|
||||
it("for a graph with nodes added and removed", () => {
|
||||
const g1 = new Graph()
|
||||
.addNode(src)
|
||||
.addNode(dst)
|
||||
.removeNode(src);
|
||||
const g2 = new Graph().addNode(dst);
|
||||
expectCanonicity(g1, g2);
|
||||
});
|
||||
it("for a graph with edges added and removed", () => {
|
||||
const g1 = new Graph()
|
||||
.addNode(src)
|
||||
.addNode(dst)
|
||||
.addEdge(edge1())
|
||||
.removeEdge(edge1().address)
|
||||
.addEdge(edge2());
|
||||
const g2 = new Graph()
|
||||
.addNode(src)
|
||||
.addNode(dst)
|
||||
.addEdge(edge2());
|
||||
expectCanonicity(g1, g2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("edgeToString", () => {
|
||||
it("works", () => {
|
||||
const edge = {
|
||||
|
|
Loading…
Reference in New Issue