diff --git a/src/plugins/initiatives/__snapshots__/initiativesDirectory.test.js.snap b/src/plugins/initiatives/__snapshots__/initiativesDirectory.test.js.snap index 90565ab..6ae751b 100644 --- a/src/plugins/initiatives/__snapshots__/initiativesDirectory.test.js.snap +++ b/src/plugins/initiatives/__snapshots__/initiativesDirectory.test.js.snap @@ -7,15 +7,21 @@ Map { "http://foo.bar/A/champ", ], "completed": true, - "contributions": Array [ - "http://foo.bar/A/contrib", - ], - "dependencies": Array [ - "http://foo.bar/A/dep", - ], - "references": Array [ - "http://foo.bar/A/ref", - ], + "contributions": Object { + "urls": Array [ + "http://foo.bar/A/contrib", + ], + }, + "dependencies": Object { + "urls": Array [ + "http://foo.bar/A/dep", + ], + }, + "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 [ - "http://foo.bar/B/contrib", - ], - "dependencies": Array [ - "http://foo.bar/B/dep", - ], - "references": Array [ - "http://foo.bar/B/ref", - ], + "contributions": Object { + "urls": Array [ + "http://foo.bar/B/contrib", + ], + }, + "dependencies": Object { + "urls": Array [ + "http://foo.bar/B/dep", + ], + }, + "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 + } } ]" `; diff --git a/src/plugins/initiatives/example/initiative-B.json b/src/plugins/initiatives/example/initiative-B.json index af1fc9b..5425e17 100644 --- a/src/plugins/initiatives/example/initiative-B.json +++ b/src/plugins/initiatives/example/initiative-B.json @@ -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"]} } ] diff --git a/src/plugins/initiatives/example/initiative-C.json b/src/plugins/initiatives/example/initiative-C.json new file mode 100644 index 0000000..f99faa4 --- /dev/null +++ b/src/plugins/initiatives/example/initiative-C.json @@ -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"] + } + ] + } + } +] diff --git a/src/plugins/initiatives/initiativeFile.js b/src/plugins/initiatives/initiativeFile.js index ab62c5d..ffe7732 100644 --- a/src/plugins/initiatives/initiativeFile.js +++ b/src/plugins/initiatives/initiativeFile.js @@ -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, +|}; + +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, |}; -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): InitiativeFile { - return fromCompat(COMPAT_INFO, j); + return fromCompat(COMPAT_INFO, j, upgrades); } export function toJSON(m: InitiativeFile): Compatible { diff --git a/src/plugins/initiatives/initiativeFile.test.js b/src/plugins/initiatives/initiativeFile.test.js index e465495..e0c766e 100644 --- a/src/plugins/initiatives/initiativeFile.test.js +++ b/src/plugins/initiatives/initiativeFile.test.js @@ -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", () => { diff --git a/src/plugins/initiatives/initiativesDirectory.js b/src/plugins/initiatives/initiativesDirectory.js index 725e148..31887bc 100644 --- a/src/plugins/initiatives/initiativesDirectory.js +++ b/src/plugins/initiatives/initiativesDirectory.js @@ -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 { 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. It only exists to allow the `Initiative` type to add +// support for EdgeSpec in a separate commit. +export function _lossyURLFromEdgeSpecJson( + json?: EdgeSpecJson +): $ReadOnlyArray { + return (json || {}).urls || []; +} + // Creates a reference map using `initiativeFileURL`. export function _createReferenceMap( initiatives: $ReadOnlyArray diff --git a/src/plugins/initiatives/initiativesDirectory.test.js b/src/plugins/initiatives/initiativesDirectory.test.js index 74a7647..1672e24 100644 --- a/src/plugins/initiatives/initiativesDirectory.test.js +++ b/src/plugins/initiatives/initiativesDirectory.test.js @@ -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", + ], ]); }); });