Use versioning for `Graph` and `AddressMap` (#293)
This commit adds explicit versioning to the `Graph` and `AddressMap` JSON representations, using the new `compat` module. This will make it safer to change the serialization format for these classes. (Note: I don't expect we'll add backcompat handlers for these classes soon, but having versioning means that we can change the serialization format in a way that breaks old data cleanly and explicitly, rather than introducing undefined behavior.) Test plan: The changes are slight, and well-captured by the snapshot tests. Note that after this commit, the SourceCred commands will fail on old data, so old data will need to be regenerated.
This commit is contained in:
parent
1fc860bd56
commit
5a40bb0a30
|
@ -1,16 +1,22 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`address AddressMap stringifies to JSON 1`] = `
|
||||
Object {
|
||||
"{\\"id\\":\\"mansion\\",\\"pluginName\\":\\"houseville\\",\\"type\\":\\"HOME\\"}": Object {
|
||||
"baths": 5,
|
||||
"beds": 10,
|
||||
Array [
|
||||
Object {
|
||||
"type": "sourcecred/sourcecred/AddressMap",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
"{\\"id\\":\\"mattressStore\\",\\"pluginName\\":\\"houseville\\",\\"type\\":\\"BUSINESS\\"}": Object {
|
||||
"baths": 1,
|
||||
"beds": 99,
|
||||
Object {
|
||||
"{\\"id\\":\\"mansion\\",\\"pluginName\\":\\"houseville\\",\\"type\\":\\"HOME\\"}": Object {
|
||||
"baths": 5,
|
||||
"beds": 10,
|
||||
},
|
||||
"{\\"id\\":\\"mattressStore\\",\\"pluginName\\":\\"houseville\\",\\"type\\":\\"BUSINESS\\"}": Object {
|
||||
"baths": 1,
|
||||
"beds": 99,
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`address toString and fromString serialization looks good in snapshot review 1`] = `
|
||||
|
|
|
@ -1,138 +1,156 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`graph #Graph JSON functions should serialize a simple graph 1`] = `
|
||||
Object {
|
||||
"edges": Object {
|
||||
"{\\"id\\":\\"crab-self-assessment\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"SILLY\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "razorclaw_crab#2",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
"payload": Object {
|
||||
"evaluation": "not effective at avoiding hero",
|
||||
},
|
||||
"src": Object {
|
||||
"id": "razorclaw_crab#2",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@again_cooks@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
"payload": Object {
|
||||
"crit": true,
|
||||
"saveScummed": true,
|
||||
},
|
||||
"src": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@cooks@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
"payload": Object {
|
||||
"crit": false,
|
||||
},
|
||||
"src": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@eats@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@grabs@razorclaw_crab#2\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "razorclaw_crab#2",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@picks@mighty_bananas#1\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "mighty_bananas#1",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"mighty_bananas#1@included_in@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"INGREDIENT\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "mighty_bananas#1",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"razorclaw_crab#2@included_in@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"INGREDIENT\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "razorclaw_crab#2",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
Array [
|
||||
Object {
|
||||
"type": "sourcecred/sourcecred/Graph",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
"nodes": Object {
|
||||
"{\\"id\\":\\"hero_of_time#0\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"PC\\"}": Object {
|
||||
"payload": Object {},
|
||||
},
|
||||
"{\\"id\\":\\"mighty_bananas#1\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"FOOD\\"}": Object {
|
||||
"payload": Object {},
|
||||
},
|
||||
"{\\"id\\":\\"razorclaw_crab#2\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"FOOD\\"}": Object {
|
||||
"payload": Object {},
|
||||
},
|
||||
"{\\"id\\":\\"seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"FOOD\\"}": Object {
|
||||
"payload": Object {
|
||||
"effect": Array [
|
||||
"attack_power",
|
||||
1,
|
||||
],
|
||||
Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"type": "sourcecred/sourcecred/AddressMap",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"{\\"id\\":\\"crab-self-assessment\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"SILLY\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "razorclaw_crab#2",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
"payload": Object {
|
||||
"evaluation": "not effective at avoiding hero",
|
||||
},
|
||||
"src": Object {
|
||||
"id": "razorclaw_crab#2",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@again_cooks@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
"payload": Object {
|
||||
"crit": true,
|
||||
"saveScummed": true,
|
||||
},
|
||||
"src": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@cooks@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
"payload": Object {
|
||||
"crit": false,
|
||||
},
|
||||
"src": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@eats@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@grabs@razorclaw_crab#2\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "razorclaw_crab#2",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"hero_of_time#0@picks@mighty_bananas#1\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"ACTION\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "hero_of_time#0",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "PC",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "mighty_bananas#1",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"mighty_bananas#1@included_in@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"INGREDIENT\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "mighty_bananas#1",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
"{\\"id\\":\\"razorclaw_crab#2@included_in@seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"INGREDIENT\\"}": Object {
|
||||
"dst": Object {
|
||||
"id": "razorclaw_crab#2",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
"payload": Object {},
|
||||
"src": Object {
|
||||
"id": "seafood_fruit_mix#3",
|
||||
"pluginName": "hill_cooking_pot",
|
||||
"type": "FOOD",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"nodes": Array [
|
||||
Object {
|
||||
"type": "sourcecred/sourcecred/AddressMap",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
Object {
|
||||
"{\\"id\\":\\"hero_of_time#0\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"PC\\"}": Object {
|
||||
"payload": Object {},
|
||||
},
|
||||
"{\\"id\\":\\"mighty_bananas#1\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"FOOD\\"}": Object {
|
||||
"payload": Object {},
|
||||
},
|
||||
"{\\"id\\":\\"razorclaw_crab#2\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"FOOD\\"}": Object {
|
||||
"payload": Object {},
|
||||
},
|
||||
"{\\"id\\":\\"seafood_fruit_mix#3\\",\\"pluginName\\":\\"hill_cooking_pot\\",\\"type\\":\\"FOOD\\"}": Object {
|
||||
"payload": Object {
|
||||
"effect": Array [
|
||||
"attack_power",
|
||||
1,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
`;
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
import deepEqual from "lodash.isequal";
|
||||
import stringify from "json-stable-stringify";
|
||||
|
||||
import {toCompat, fromCompat} from "../util/compat";
|
||||
import type {Compatible} from "../util/compat";
|
||||
|
||||
export type Address = {|
|
||||
+pluginName: string,
|
||||
+id: string,
|
||||
|
@ -15,9 +18,12 @@ export interface Addressable {
|
|||
|
||||
export type SansAddress<T: Addressable> = $Exact<$Diff<T, {+address: Address}>>;
|
||||
|
||||
export type AddressMapJSON<T: Addressable> = {
|
||||
export type AddressMapJSON<T: Addressable> = Compatible<{
|
||||
[serializedAddress: string]: SansAddress<T>,
|
||||
};
|
||||
}>;
|
||||
|
||||
export const COMPAT_TYPE = "sourcecred/sourcecred/AddressMap";
|
||||
export const COMPAT_VERSION = "0.1.0";
|
||||
|
||||
/**
|
||||
* A data structure for storing addressable objects, keyed by their
|
||||
|
@ -70,13 +76,26 @@ export class AddressMap<T: Addressable> {
|
|||
});
|
||||
});
|
||||
});
|
||||
return result;
|
||||
return toCompat(
|
||||
{
|
||||
type: COMPAT_TYPE,
|
||||
version: COMPAT_VERSION,
|
||||
},
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
static fromJSON(json: AddressMapJSON<T>): AddressMap<T> {
|
||||
const decompat = fromCompat(
|
||||
{
|
||||
type: COMPAT_TYPE,
|
||||
version: COMPAT_VERSION,
|
||||
},
|
||||
json
|
||||
);
|
||||
const result: AddressMap<T> = new AddressMap();
|
||||
Object.keys(json).forEach((key) => {
|
||||
result.add({...json[key], address: JSON.parse(key)});
|
||||
Object.keys(decompat).forEach((key) => {
|
||||
result.add({...decompat[key], address: JSON.parse(key)});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,15 @@
|
|||
import sortBy from "lodash.sortby";
|
||||
import stringify from "json-stable-stringify";
|
||||
|
||||
import {fromCompat} from "../util/compat";
|
||||
import type {Address} from "./address";
|
||||
import {AddressMap, fromString, toString} from "./address";
|
||||
import {
|
||||
AddressMap,
|
||||
fromString,
|
||||
toString,
|
||||
COMPAT_TYPE,
|
||||
COMPAT_VERSION,
|
||||
} from "./address";
|
||||
|
||||
describe("address", () => {
|
||||
// Some test data using objects that have addresses, like houses.
|
||||
|
@ -70,7 +77,11 @@ describe("address", () => {
|
|||
});
|
||||
|
||||
it("stringifies elements sans addresses", () => {
|
||||
const json = makeMap().toJSON();
|
||||
const compatJson = makeMap().toJSON();
|
||||
const json = fromCompat(
|
||||
{type: COMPAT_TYPE, version: COMPAT_VERSION},
|
||||
compatJson
|
||||
);
|
||||
Object.keys(json).forEach((k) => {
|
||||
const value = json[k];
|
||||
expect(Object.keys(value).sort()).toEqual(["baths", "beds"]);
|
||||
|
|
|
@ -4,6 +4,8 @@ import deepEqual from "lodash.isequal";
|
|||
import stringify from "json-stable-stringify";
|
||||
import type {Address, Addressable, AddressMapJSON} from "./address";
|
||||
import {AddressMap} from "./address";
|
||||
import {toCompat, fromCompat} from "../util/compat";
|
||||
import type {Compatible} from "../util/compat";
|
||||
|
||||
export type Node<+T> = {|
|
||||
+address: Address,
|
||||
|
@ -17,10 +19,13 @@ export type Edge<+T> = {|
|
|||
+payload: T,
|
||||
|};
|
||||
|
||||
export type GraphJSON<NP, EP> = {|
|
||||
const COMPAT_TYPE = "sourcecred/sourcecred/Graph";
|
||||
const COMPAT_VERSION = "0.1.0";
|
||||
|
||||
export type GraphJSON<NP, EP> = Compatible<{|
|
||||
+nodes: AddressMapJSON<Node<NP>>,
|
||||
+edges: AddressMapJSON<Edge<EP>>,
|
||||
|};
|
||||
|}>;
|
||||
|
||||
export class Graph<NP, EP> {
|
||||
_nodes: AddressMap<Node<NP>>;
|
||||
|
@ -50,20 +55,30 @@ export class Graph<NP, EP> {
|
|||
}
|
||||
|
||||
toJSON(): GraphJSON<NP, EP> {
|
||||
return {
|
||||
nodes: this._nodes.toJSON(),
|
||||
edges: this._edges.toJSON(),
|
||||
};
|
||||
return toCompat(
|
||||
{type: COMPAT_TYPE, version: COMPAT_VERSION},
|
||||
{
|
||||
nodes: this._nodes.toJSON(),
|
||||
edges: this._edges.toJSON(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static fromJSON<NP, EP>(json: GraphJSON<NP, EP>): Graph<NP, EP> {
|
||||
const compatJson = fromCompat(
|
||||
{
|
||||
type: COMPAT_TYPE,
|
||||
version: COMPAT_VERSION,
|
||||
},
|
||||
json
|
||||
);
|
||||
const result = new Graph();
|
||||
AddressMap.fromJSON(json.nodes)
|
||||
AddressMap.fromJSON(compatJson.nodes)
|
||||
.getAll()
|
||||
.forEach((node) => {
|
||||
result.addNode(node);
|
||||
});
|
||||
AddressMap.fromJSON(json.edges)
|
||||
AddressMap.fromJSON(compatJson.edges)
|
||||
.getAll()
|
||||
.forEach((edge) => {
|
||||
result.addEdge(edge);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue