Initiatives: add EdgeSpecJson support to InitativeFile v0.2.0 (#1763)
An additional change here is we're allowing more keys to be omitted in the JSON format. This is both intuitive for data entry, and safer in terms of Flow types (as JSON.parse returns any). The test examples now cover a v0.1.0 (initiative-A), v0.2.0 with just URLs (initiative-B) and one with just entries (initiative-C). To make this commit smaller and easier to review, we're not yet adding `EdgeSpec` to the `Initiative` type and will ignore the entries when converting from `InitiativeFile` to `Initiative`.
This commit is contained in:
parent
5ef69bef50
commit
0a671025d7
|
@ -7,15 +7,21 @@ Map {
|
|||
"http://foo.bar/A/champ",
|
||||
],
|
||||
"completed": true,
|
||||
"contributions": Array [
|
||||
"contributions": Object {
|
||||
"urls": Array [
|
||||
"http://foo.bar/A/contrib",
|
||||
],
|
||||
"dependencies": Array [
|
||||
},
|
||||
"dependencies": Object {
|
||||
"urls": Array [
|
||||
"http://foo.bar/A/dep",
|
||||
],
|
||||
"references": Array [
|
||||
},
|
||||
"references": Object {
|
||||
"urls": Array [
|
||||
"http://foo.bar/A/ref",
|
||||
],
|
||||
},
|
||||
"timestampIso": "2020-01-08T22:01:57.711Z",
|
||||
"title": "Initiative A",
|
||||
"weight": Object {
|
||||
|
@ -28,15 +34,21 @@ Map {
|
|||
"http://foo.bar/B/champ",
|
||||
],
|
||||
"completed": false,
|
||||
"contributions": Array [
|
||||
"contributions": Object {
|
||||
"urls": Array [
|
||||
"http://foo.bar/B/contrib",
|
||||
],
|
||||
"dependencies": Array [
|
||||
},
|
||||
"dependencies": Object {
|
||||
"urls": Array [
|
||||
"http://foo.bar/B/dep",
|
||||
],
|
||||
"references": Array [
|
||||
},
|
||||
"references": Object {
|
||||
"urls": Array [
|
||||
"http://foo.bar/B/ref",
|
||||
],
|
||||
},
|
||||
"timestampIso": "2020-01-08T22:01:57.722Z",
|
||||
"title": "Initiative B",
|
||||
"weight": Object {
|
||||
|
@ -44,6 +56,50 @@ Map {
|
|||
"incomplete": 42,
|
||||
},
|
||||
},
|
||||
"initiative-C.json" => Object {
|
||||
"champions": Array [
|
||||
"http://foo.bar/C/champ",
|
||||
],
|
||||
"completed": false,
|
||||
"contributions": Object {
|
||||
"entries": Array [
|
||||
Object {
|
||||
"contributors": Array [
|
||||
"https://foo.bar/C/contrib-user",
|
||||
],
|
||||
"title": "Add test contrib",
|
||||
"weight": 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
"dependencies": Object {
|
||||
"entries": Array [
|
||||
Object {
|
||||
"contributors": Array [
|
||||
"https://foo.bar/C/dep-user",
|
||||
],
|
||||
"timestampIso": "2020-01-02T22:01:57.700Z",
|
||||
"title": "Add test dependency",
|
||||
},
|
||||
],
|
||||
},
|
||||
"references": Object {
|
||||
"entries": Array [
|
||||
Object {
|
||||
"contributors": Array [
|
||||
"https://foo.bar/C/ref-user",
|
||||
],
|
||||
"title": "Add test reference",
|
||||
},
|
||||
],
|
||||
},
|
||||
"timestampIso": "2020-01-08T22:01:57.733Z",
|
||||
"title": "Initiative C",
|
||||
"weight": Object {
|
||||
"complete": 321,
|
||||
"incomplete": 123,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -56,6 +112,10 @@ exports[`plugins/initiatives/initiativesDirectory loadDirectory should handle an
|
|||
[
|
||||
\\"http://example.com/initiatives/initiative-B.json\\",
|
||||
\\"N\\\\u0000sourcecred\\\\u0000initiatives\\\\u0000initiative\\\\u0000INITIATIVE_FILE\\\\u0000http://example.com/initiatives\\\\u0000initiative-B.json\\\\u0000\\"
|
||||
],
|
||||
[
|
||||
\\"http://example.com/initiatives/initiative-C.json\\",
|
||||
\\"N\\\\u0000sourcecred\\\\u0000initiatives\\\\u0000initiative\\\\u0000INITIATIVE_FILE\\\\u0000http://example.com/initiatives\\\\u0000initiative-C.json\\\\u0000\\"
|
||||
]
|
||||
]"
|
||||
`;
|
||||
|
@ -113,6 +173,29 @@ exports[`plugins/initiatives/initiativesDirectory loadDirectory should handle an
|
|||
\\"complete\\": 69,
|
||||
\\"incomplete\\": 42
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"champions\\": [
|
||||
\\"http://foo.bar/C/champ\\"
|
||||
],
|
||||
\\"completed\\": false,
|
||||
\\"contributions\\": [
|
||||
],
|
||||
\\"dependencies\\": [
|
||||
],
|
||||
\\"id\\": [
|
||||
\\"INITIATIVE_FILE\\",
|
||||
\\"http://example.com/initiatives\\",
|
||||
\\"initiative-C.json\\"
|
||||
],
|
||||
\\"references\\": [
|
||||
],
|
||||
\\"timestampMs\\": 1578520917733,
|
||||
\\"title\\": \\"Initiative C\\",
|
||||
\\"weight\\": {
|
||||
\\"complete\\": 321,
|
||||
\\"incomplete\\": 123
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[
|
||||
{
|
||||
"type": "sourcecred/initiativeFile",
|
||||
"version": "0.1.0"
|
||||
"version": "0.2.0"
|
||||
},
|
||||
{
|
||||
"title": "Initiative B",
|
||||
|
@ -12,8 +12,8 @@
|
|||
},
|
||||
"completed": false,
|
||||
"champions": ["http://foo.bar/B/champ"],
|
||||
"contributions": ["http://foo.bar/B/contrib"],
|
||||
"dependencies": ["http://foo.bar/B/dep"],
|
||||
"references": ["http://foo.bar/B/ref"]
|
||||
"contributions": {"urls": ["http://foo.bar/B/contrib"]},
|
||||
"dependencies": {"urls": ["http://foo.bar/B/dep"]},
|
||||
"references": {"urls": ["http://foo.bar/B/ref"]}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
[
|
||||
{
|
||||
"type": "sourcecred/initiativeFile",
|
||||
"version": "0.2.0"
|
||||
},
|
||||
{
|
||||
"title": "Initiative C",
|
||||
"timestampIso": "2020-01-08T22:01:57.733Z",
|
||||
"weight": {
|
||||
"incomplete": 123,
|
||||
"complete": 321
|
||||
},
|
||||
"completed": false,
|
||||
"champions": ["http://foo.bar/C/champ"],
|
||||
"contributions": {
|
||||
"entries": [
|
||||
{
|
||||
"title": "Add test contrib",
|
||||
"weight": 10,
|
||||
"contributors": ["https://foo.bar/C/contrib-user"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"entries": [
|
||||
{
|
||||
"title": "Add test dependency",
|
||||
"timestampIso": "2020-01-02T22:01:57.700Z",
|
||||
"contributors": ["https://foo.bar/C/dep-user"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"references": {
|
||||
"entries": [
|
||||
{
|
||||
"title": "Add test reference",
|
||||
"contributors": ["https://foo.bar/C/ref-user"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
|
@ -4,9 +4,10 @@ import {type URL} from "../../core/references";
|
|||
import {type TimestampISO} from "../../util/timestamp";
|
||||
import {type NodeAddressT, NodeAddress} from "../../core/graph";
|
||||
import {type Compatible, fromCompat, toCompat} from "../../util/compat";
|
||||
import {initiativeNodeType} from "./declaration";
|
||||
import {type InitiativeWeight, type InitiativeId, createId} from "./initiative";
|
||||
import {type InitiativesDirectory} from "./initiativesDirectory";
|
||||
import {type EdgeSpecJson} from "./edgeSpec";
|
||||
import {initiativeNodeType} from "./declaration";
|
||||
|
||||
export const INITIATIVE_FILE_SUBTYPE = "INITIATIVE_FILE";
|
||||
|
||||
|
@ -16,7 +17,27 @@ export const INITIATIVE_FILE_SUBTYPE = "INITIATIVE_FILE";
|
|||
* Note: The file name will be used to derive the InitiativeId. So it doesn't
|
||||
* make sense to use this outside of the context of an InitiativesDirectory.
|
||||
*/
|
||||
export type InitiativeFile = {|
|
||||
export type InitiativeFile = InitiativeFileV020;
|
||||
|
||||
export type InitiativeFileV020 = {|
|
||||
+title: string,
|
||||
+timestampIso: TimestampISO,
|
||||
+weight: InitiativeWeight,
|
||||
+completed: boolean,
|
||||
+contributions?: EdgeSpecJson,
|
||||
+dependencies?: EdgeSpecJson,
|
||||
+references?: EdgeSpecJson,
|
||||
+champions?: $ReadOnlyArray<URL>,
|
||||
|};
|
||||
|
||||
const upgradeFrom010 = (file: InitiativeFileV010): InitiativeFileV020 => ({
|
||||
...file,
|
||||
contributions: {urls: file.contributions},
|
||||
dependencies: {urls: file.dependencies},
|
||||
references: {urls: file.references},
|
||||
});
|
||||
|
||||
export type InitiativeFileV010 = {|
|
||||
+title: string,
|
||||
+timestampIso: TimestampISO,
|
||||
+weight: InitiativeWeight,
|
||||
|
@ -27,10 +48,14 @@ export type InitiativeFile = {|
|
|||
+champions: $ReadOnlyArray<URL>,
|
||||
|};
|
||||
|
||||
const COMPAT_INFO = {type: "sourcecred/initiativeFile", version: "0.1.0"};
|
||||
const upgrades = {
|
||||
"0.1.0": upgradeFrom010,
|
||||
};
|
||||
|
||||
const COMPAT_INFO = {type: "sourcecred/initiativeFile", version: "0.2.0"};
|
||||
|
||||
export function fromJSON(j: Compatible<any>): InitiativeFile {
|
||||
return fromCompat(COMPAT_INFO, j);
|
||||
return fromCompat(COMPAT_INFO, j, upgrades);
|
||||
}
|
||||
|
||||
export function toJSON(m: InitiativeFile): Compatible<InitiativeFile> {
|
||||
|
|
|
@ -18,9 +18,18 @@ const exampleInitiativeFile = (): InitiativeFile => ({
|
|||
weight: {incomplete: 360, complete: 420},
|
||||
completed: false,
|
||||
champions: ["http://foo.bar/champ"],
|
||||
contributions: ["http://foo.bar/contrib"],
|
||||
dependencies: ["http://foo.bar/dep"],
|
||||
references: ["http://foo.bar/ref"],
|
||||
contributions: {
|
||||
urls: ["http://foo.bar/contrib"],
|
||||
entries: [{title: "Inline contrib"}],
|
||||
},
|
||||
dependencies: {
|
||||
urls: ["http://foo.bar/dep"],
|
||||
entries: [{title: "Inline dep"}],
|
||||
},
|
||||
references: {
|
||||
urls: ["http://foo.bar/ref"],
|
||||
entries: [{title: "Inline ref"}],
|
||||
},
|
||||
});
|
||||
|
||||
describe("plugins/initiatives/initiativeFile", () => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
type ReferenceDetector,
|
||||
MappedReferenceDetector,
|
||||
} from "../../core/references";
|
||||
import {type EdgeSpecJson} from "./edgeSpec";
|
||||
import {
|
||||
type Initiative,
|
||||
type InitiativeRepository,
|
||||
|
@ -148,18 +149,41 @@ export function _convertToInitiatives(
|
|||
): $ReadOnlyArray<Initiative> {
|
||||
const initiatives = [];
|
||||
for (const [fileName, initiativeFile] of map.entries()) {
|
||||
const {timestampIso, ...partialInitiativeFile} = initiativeFile;
|
||||
const {
|
||||
timestampIso,
|
||||
champions,
|
||||
contributions,
|
||||
dependencies,
|
||||
references,
|
||||
...partialInitiativeFile
|
||||
} = initiativeFile;
|
||||
|
||||
const timestampMs = Timestamp.fromISO(timestampIso);
|
||||
|
||||
const initiative: Initiative = {
|
||||
...partialInitiativeFile,
|
||||
id: initiativeFileId(directory, fileName),
|
||||
timestampMs,
|
||||
champions: champions || [],
|
||||
contributions: _lossyURLFromEdgeSpecJson(contributions),
|
||||
dependencies: _lossyURLFromEdgeSpecJson(dependencies),
|
||||
references: _lossyURLFromEdgeSpecJson(references),
|
||||
};
|
||||
|
||||
initiatives.push(initiative);
|
||||
}
|
||||
return initiatives;
|
||||
}
|
||||
|
||||
// TODO: this is a temporary function, which reverts an ?EdgeSpecJson to just
|
||||
// $ReadOnlyArray<URL>. It only exists to allow the `Initiative` type to add
|
||||
// support for EdgeSpec in a separate commit.
|
||||
export function _lossyURLFromEdgeSpecJson(
|
||||
json?: EdgeSpecJson
|
||||
): $ReadOnlyArray<URL> {
|
||||
return (json || {}).urls || [];
|
||||
}
|
||||
|
||||
// Creates a reference map using `initiativeFileURL`.
|
||||
export function _createReferenceMap(
|
||||
initiatives: $ReadOnlyArray<Initiative>
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
_validateUrl,
|
||||
_convertToInitiatives,
|
||||
_createReferenceMap,
|
||||
_lossyURLFromEdgeSpecJson,
|
||||
} from "./initiativesDirectory";
|
||||
import {type InitiativeFile} from "./initiativeFile";
|
||||
|
||||
|
@ -25,18 +26,38 @@ const exampleInitiativeFile = (): InitiativeFile => ({
|
|||
weight: {incomplete: 360, complete: 420},
|
||||
completed: false,
|
||||
champions: ["http://foo.bar/champ"],
|
||||
contributions: ["http://foo.bar/contrib"],
|
||||
dependencies: ["http://foo.bar/dep"],
|
||||
references: ["http://foo.bar/ref"],
|
||||
contributions: {
|
||||
urls: ["http://foo.bar/contrib"],
|
||||
entries: [{title: "Inline contrib"}],
|
||||
},
|
||||
dependencies: {
|
||||
urls: ["http://foo.bar/dep"],
|
||||
entries: [{title: "Inline dep"}],
|
||||
},
|
||||
references: {
|
||||
urls: ["http://foo.bar/ref"],
|
||||
entries: [{title: "Inline ref"}],
|
||||
},
|
||||
});
|
||||
|
||||
const exampleInitiative = (remoteUrl: string, fileName: string): Initiative => {
|
||||
const {timestampIso, ...partialInitiativeFile} = exampleInitiativeFile();
|
||||
const {
|
||||
timestampIso,
|
||||
contributions,
|
||||
dependencies,
|
||||
references,
|
||||
champions,
|
||||
...partialInitiativeFile
|
||||
} = exampleInitiativeFile();
|
||||
const timestampMs = Timestamp.fromISO(timestampIso);
|
||||
return {
|
||||
...partialInitiativeFile,
|
||||
id: createId("INITIATIVE_FILE", remoteUrl, fileName),
|
||||
timestampMs,
|
||||
champions: champions || [],
|
||||
contributions: _lossyURLFromEdgeSpecJson(contributions),
|
||||
dependencies: _lossyURLFromEdgeSpecJson(dependencies),
|
||||
references: _lossyURLFromEdgeSpecJson(references),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -92,7 +113,11 @@ describe("plugins/initiatives/initiativesDirectory", () => {
|
|||
// Then
|
||||
// Shallow copy to sort, because the array is read-only.
|
||||
const actualNames = [...fileNames].sort();
|
||||
expect(actualNames).toEqual(["initiative-A.json", "initiative-B.json"]);
|
||||
expect(actualNames).toEqual([
|
||||
"initiative-A.json",
|
||||
"initiative-B.json",
|
||||
"initiative-C.json",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -100,7 +125,11 @@ describe("plugins/initiatives/initiativesDirectory", () => {
|
|||
it("should read provided initiativeFiles, sorted by name", async () => {
|
||||
// Given
|
||||
const localPath = path.join(__dirname, "example");
|
||||
const fileNames = ["initiative-B.json", "initiative-A.json"];
|
||||
const fileNames = [
|
||||
"initiative-C.json",
|
||||
"initiative-B.json",
|
||||
"initiative-A.json",
|
||||
];
|
||||
|
||||
// When
|
||||
const map = await _readFiles(localPath, fileNames);
|
||||
|
@ -109,6 +138,7 @@ describe("plugins/initiatives/initiativesDirectory", () => {
|
|||
expect([...map.keys()]).toEqual([
|
||||
"initiative-A.json",
|
||||
"initiative-B.json",
|
||||
"initiative-C.json",
|
||||
]);
|
||||
expect(map).toMatchSnapshot();
|
||||
});
|
||||
|
@ -116,7 +146,11 @@ describe("plugins/initiatives/initiativesDirectory", () => {
|
|||
it("should throw when directory doesn't exist", async () => {
|
||||
// Given
|
||||
const localPath = path.join(tmp.dirSync().name, "findFiles_test");
|
||||
const fileNames = ["initiative-B.json", "initiative-A.json"];
|
||||
const fileNames = [
|
||||
"initiative-C.json",
|
||||
"initiative-B.json",
|
||||
"initiative-A.json",
|
||||
];
|
||||
|
||||
// When
|
||||
const p = _readFiles(localPath, fileNames);
|
||||
|
@ -128,7 +162,11 @@ describe("plugins/initiatives/initiativesDirectory", () => {
|
|||
it("should throw when directory is not a directory", async () => {
|
||||
// Given
|
||||
const localPath = path.join(tmp.dirSync().name, "findFiles_test");
|
||||
const fileNames = ["initiative-B.json", "initiative-A.json"];
|
||||
const fileNames = [
|
||||
"initiative-C.json",
|
||||
"initiative-B.json",
|
||||
"initiative-A.json",
|
||||
];
|
||||
await fs.writeFile(localPath, "");
|
||||
|
||||
// When
|
||||
|
@ -320,6 +358,7 @@ describe("plugins/initiatives/initiativesDirectory", () => {
|
|||
expect(urls).toEqual([
|
||||
"http://example.com/initiatives/initiative-A.json",
|
||||
"http://example.com/initiatives/initiative-B.json",
|
||||
"http://example.com/initiatives/initiative-C.json",
|
||||
]);
|
||||
expect(initiatives.map((i) => i.id)).toEqual([
|
||||
[
|
||||
|
@ -332,6 +371,11 @@ describe("plugins/initiatives/initiativesDirectory", () => {
|
|||
"http://example.com/initiatives",
|
||||
"initiative-B.json",
|
||||
],
|
||||
[
|
||||
"INITIATIVE_FILE",
|
||||
"http://example.com/initiatives",
|
||||
"initiative-C.json",
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue