mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-28 12:10:30 +00:00
Make Graph
serializable (#69)
Summary: This commit adds `toJSON()` and `static fromJSON()` on `Graph`. The main benefit at this time is that this gets us free interoperability with Jest’s snapshot testing. The implementation of `fromJSON` is not performance-tuned, and could probably be significantly optimized. See #65 for discussion. Test Plan: New unit tests added: `yarn flow && yarn test`. wchargin-branch: make-graph-serializable
This commit is contained in:
parent
cee90fd10f
commit
5960eab6c1
138
src/backend/__snapshots__/graph.test.js.snap
Normal file
138
src/backend/__snapshots__/graph.test.js.snap
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`graph #Graph JSON functions should serialize a simple graph 1`] = `
|
||||||
|
Object {
|
||||||
|
"edges": Object {
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"crab-self-assessment\\"}": Object {
|
||||||
|
"dst": Object {
|
||||||
|
"id": "razorclaw_crab#2",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
"payload": Object {
|
||||||
|
"evaluation": "not effective at avoiding hero",
|
||||||
|
},
|
||||||
|
"src": Object {
|
||||||
|
"id": "razorclaw_crab#2",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"hero_of_time#0@again_cooks@seafood_fruit_mix#3\\"}": Object {
|
||||||
|
"dst": Object {
|
||||||
|
"id": "hero_of_time#0",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
"payload": Object {
|
||||||
|
"crit": true,
|
||||||
|
"saveScummed": true,
|
||||||
|
},
|
||||||
|
"src": Object {
|
||||||
|
"id": "seafood_fruit_mix#3",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"hero_of_time#0@cooks@seafood_fruit_mix#3\\"}": Object {
|
||||||
|
"dst": Object {
|
||||||
|
"id": "hero_of_time#0",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
"payload": Object {
|
||||||
|
"crit": false,
|
||||||
|
},
|
||||||
|
"src": Object {
|
||||||
|
"id": "seafood_fruit_mix#3",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"hero_of_time#0@eats@seafood_fruit_mix#3\\"}": Object {
|
||||||
|
"dst": Object {
|
||||||
|
"id": "seafood_fruit_mix#3",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
"payload": Object {},
|
||||||
|
"src": Object {
|
||||||
|
"id": "hero_of_time#0",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"hero_of_time#0@grabs@razorclaw_crab#2\\"}": Object {
|
||||||
|
"dst": Object {
|
||||||
|
"id": "hero_of_time#0",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
"payload": Object {},
|
||||||
|
"src": Object {
|
||||||
|
"id": "razorclaw_crab#2",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"hero_of_time#0@picks@mighty_bananas#1\\"}": Object {
|
||||||
|
"dst": Object {
|
||||||
|
"id": "hero_of_time#0",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
"payload": Object {},
|
||||||
|
"src": Object {
|
||||||
|
"id": "mighty_bananas#1",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"mighty_bananas#1@included_in@seafood_fruit_mix#3\\"}": Object {
|
||||||
|
"dst": Object {
|
||||||
|
"id": "mighty_bananas#1",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
"payload": Object {},
|
||||||
|
"src": Object {
|
||||||
|
"id": "seafood_fruit_mix#3",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"razorclaw_crab#2@included_in@seafood_fruit_mix#3\\"}": Object {
|
||||||
|
"dst": Object {
|
||||||
|
"id": "razorclaw_crab#2",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
"payload": Object {},
|
||||||
|
"src": Object {
|
||||||
|
"id": "seafood_fruit_mix#3",
|
||||||
|
"pluginName": "hill_cooking_pot",
|
||||||
|
"repositoryName": "sourcecred/eventide",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nodes": Object {
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"hero_of_time#0\\"}": Object {
|
||||||
|
"payload": Object {},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"mighty_bananas#1\\"}": Object {
|
||||||
|
"payload": Object {},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"razorclaw_crab#2\\"}": Object {
|
||||||
|
"payload": Object {},
|
||||||
|
},
|
||||||
|
"{\\"repositoryName\\":\\"sourcecred/eventide\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"id\\":\\"seafood_fruit_mix#3\\"}": Object {
|
||||||
|
"payload": Object {
|
||||||
|
"effect": Array [
|
||||||
|
"attack_power",
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
@ -1,7 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import deepEqual from "lodash.isequal";
|
import deepEqual from "lodash.isequal";
|
||||||
import type {Address, Addressable} from "./address";
|
import type {Address, Addressable, AddressMapJSON} from "./address";
|
||||||
import {AddressMap} from "./address";
|
import {AddressMap} from "./address";
|
||||||
|
|
||||||
export type Node<T> = {|
|
export type Node<T> = {|
|
||||||
@ -16,6 +16,11 @@ export type Edge<T> = {|
|
|||||||
+payload: T,
|
+payload: T,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
export type GraphJSON = {|
|
||||||
|
+nodes: AddressMapJSON<Node<mixed>>,
|
||||||
|
+edges: AddressMapJSON<Edge<mixed>>,
|
||||||
|
|};
|
||||||
|
|
||||||
export class Graph {
|
export class Graph {
|
||||||
_nodes: AddressMap<Node<mixed>>;
|
_nodes: AddressMap<Node<mixed>>;
|
||||||
_edges: AddressMap<Edge<mixed>>;
|
_edges: AddressMap<Edge<mixed>>;
|
||||||
@ -39,6 +44,28 @@ export class Graph {
|
|||||||
return this._nodes.equals(that._nodes) && this._edges.equals(that._edges);
|
return this._nodes.equals(that._nodes) && this._edges.equals(that._edges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON(): GraphJSON {
|
||||||
|
return {
|
||||||
|
nodes: this._nodes.toJSON(),
|
||||||
|
edges: this._edges.toJSON(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJSON(json: GraphJSON): Graph {
|
||||||
|
const result = new Graph();
|
||||||
|
AddressMap.fromJSON(json.nodes)
|
||||||
|
.getAll()
|
||||||
|
.forEach((node) => {
|
||||||
|
result.addNode(node);
|
||||||
|
});
|
||||||
|
AddressMap.fromJSON(json.edges)
|
||||||
|
.getAll()
|
||||||
|
.forEach((edge) => {
|
||||||
|
result.addEdge(edge);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
addNode(node: Node<mixed>) {
|
addNode(node: Node<mixed>) {
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
throw new Error(`node is ${String(node)}`);
|
throw new Error(`node is ${String(node)}`);
|
||||||
|
@ -555,5 +555,44 @@ describe("graph", () => {
|
|||||||
expect(merged.equals(new Graph())).toBe(true);
|
expect(merged.equals(new Graph())).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("JSON functions", () => {
|
||||||
|
it("should serialize a simple graph", () => {
|
||||||
|
expect(advancedMealGraph().toJSON()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
it("should work transparently with JSON.stringify", () => {
|
||||||
|
// (This is guaranteed by the `JSON.stringify` API, and is more
|
||||||
|
// as documentation than actual test.)
|
||||||
|
expect(JSON.stringify(advancedMealGraph())).toEqual(
|
||||||
|
JSON.stringify(advancedMealGraph().toJSON())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("should canonicalize away node insertion order", () => {
|
||||||
|
const g1 = new Graph().addNode(heroNode()).addNode(mealNode());
|
||||||
|
const g2 = new Graph().addNode(mealNode()).addNode(heroNode());
|
||||||
|
expect(g1.toJSON()).toEqual(g2.toJSON());
|
||||||
|
});
|
||||||
|
it("should canonicalize away edge insertion order", () => {
|
||||||
|
const g1 = new Graph()
|
||||||
|
.addNode(heroNode())
|
||||||
|
.addNode(mealNode())
|
||||||
|
.addEdge(cookEdge())
|
||||||
|
.addEdge(duplicateCookEdge());
|
||||||
|
const g2 = new Graph()
|
||||||
|
.addNode(heroNode())
|
||||||
|
.addNode(mealNode())
|
||||||
|
.addEdge(duplicateCookEdge())
|
||||||
|
.addEdge(cookEdge());
|
||||||
|
expect(g1.toJSON()).toEqual(g2.toJSON());
|
||||||
|
});
|
||||||
|
it("should no-op on a serialization--deserialization roundtrip", () => {
|
||||||
|
const g = () => advancedMealGraph();
|
||||||
|
expect(Graph.fromJSON(g().toJSON()).equals(g())).toBe(true);
|
||||||
|
});
|
||||||
|
it("should no-op on a deserialization--serialization roundtrip", () => {
|
||||||
|
const json = () => advancedMealGraph().toJSON();
|
||||||
|
expect(Graph.fromJSON(json()).toJSON()).toEqual(json());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user