diff --git a/src/plugins/initiatives/createGraph.js b/src/plugins/initiatives/createGraph.js index 82d4831..4ad9d17 100644 --- a/src/plugins/initiatives/createGraph.js +++ b/src/plugins/initiatives/createGraph.js @@ -20,7 +20,7 @@ import { contributesToEdgeType, championsEdgeType, } from "./declaration"; -import {initiativeFileURL} from "./initiativesDirectory"; +import {initiativeFileURL} from "./initiativeFile"; function initiativeAddress(initiative: Initiative): NodeAddressT { return addressFromId(initiative.id); diff --git a/src/plugins/initiatives/initiativeFile.js b/src/plugins/initiatives/initiativeFile.js new file mode 100644 index 0000000..3f7f556 --- /dev/null +++ b/src/plugins/initiatives/initiativeFile.js @@ -0,0 +1,68 @@ +// @flow + +import {type URL} from "../../core/references"; +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"; + +export const INITIATIVE_FILE_SUBTYPE = "INITIATIVE_FILE"; + +/** + * Represents a single Initiative using a file as source. + * + * 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 = {| + +title: string, + +timestampIso: ISOTimestamp, + +weight: InitiativeWeight, + +completed: boolean, + +dependencies: $ReadOnlyArray, + +references: $ReadOnlyArray, + +contributions: $ReadOnlyArray, + +champions: $ReadOnlyArray, +|}; + +// Note: setting this to opaque forces us to convert it to timestampMs. +opaque type ISOTimestamp: string = string; + +const COMPAT_INFO = {type: "sourcecred/initiativeFile", version: "0.1.0"}; + +export function fromJSON(j: Compatible): InitiativeFile { + return fromCompat(COMPAT_INFO, j); +} + +export function toJSON(m: InitiativeFile): Compatible { + return toCompat(COMPAT_INFO, m); +} + +/** + * When provided with the initiative NodeAddressT of an InitiativeFile this extracts + * the URL from it. Or null when the address is not for an InitiativeFile. + */ +export function initiativeFileURL(address: NodeAddressT): string | null { + const initiativeFilePrefix = NodeAddress.append( + initiativeNodeType.prefix, + INITIATIVE_FILE_SUBTYPE + ); + + if (!NodeAddress.hasPrefix(address, initiativeFilePrefix)) { + return null; + } + + const parts = NodeAddress.toParts(address); + const remoteUrl = parts[4]; + const fileName = parts[5]; + return `${remoteUrl}/${fileName}`; +} + +// Creates the InitiativeId for an InitiativeFile. +export function initiativeFileId( + {remoteUrl}: InitiativesDirectory, + fileName: string +): InitiativeId { + return createId(INITIATIVE_FILE_SUBTYPE, remoteUrl, fileName); +} diff --git a/src/plugins/initiatives/initiativeFile.test.js b/src/plugins/initiatives/initiativeFile.test.js new file mode 100644 index 0000000..2f496ba --- /dev/null +++ b/src/plugins/initiatives/initiativeFile.test.js @@ -0,0 +1,83 @@ +// @flow + +import {NodeAddress} from "../../core/graph"; +import {createId, addressFromId} from "./initiative"; +import {type InitiativesDirectory} from "./initiativesDirectory"; +import { + type InitiativeFile, + fromJSON, + toJSON, + initiativeFileURL, + initiativeFileId, +} from "./initiativeFile"; + +const exampleInitiativeFile = (): InitiativeFile => ({ + title: "Sample initiative", + timestampIso: ("2020-01-08T22:01:57.766Z": any), + 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"], +}); + +describe("plugins/initiatives/initiativeFile", () => { + describe("toJSON/fromJSON", () => { + it("should handle an example round-trip", () => { + // Given + const initiativeFile = exampleInitiativeFile(); + + // When + const actual = fromJSON(toJSON(initiativeFile)); + + // Then + expect(actual).toEqual(initiativeFile); + }); + }); + + describe("initiativeFileURL", () => { + it("should return null for a different prefix", () => { + // Given + const address = NodeAddress.fromParts(["foobar"]); + + // When + const url = initiativeFileURL(address); + + // Then + expect(url).toEqual(null); + }); + + it("should detect the correct prefix and create a URL", () => { + // Given + const remoteUrl = "http://foo.bar/dir"; + const fileName = "sample.json"; + const address = addressFromId( + createId("INITIATIVE_FILE", remoteUrl, fileName) + ); + + // When + const url = initiativeFileURL(address); + + // Then + expect(url).toEqual(`${remoteUrl}/${fileName}`); + }); + }); + + describe("initiativeFileId", () => { + it("should add the correct prefix to a remoteUrl and fileName", () => { + // Given + const dir: InitiativesDirectory = { + localPath: "should-not-be-used", + remoteUrl: "http://foo.bar/dir", + }; + const fileName = "sample.json"; + + // When + const id = initiativeFileId(dir, fileName); + + // Then + expect(id).toEqual(createId("INITIATIVE_FILE", dir.remoteUrl, fileName)); + }); + }); +}); diff --git a/src/plugins/initiatives/initiativesDirectory.js b/src/plugins/initiatives/initiativesDirectory.js index c27a1b8..81cce49 100644 --- a/src/plugins/initiatives/initiativesDirectory.js +++ b/src/plugins/initiatives/initiativesDirectory.js @@ -4,24 +4,23 @@ import path from "path"; import fs from "fs-extra"; import globby from "globby"; import {type URL} from "../../core/references"; -import {type NodeAddressT, NodeAddress} from "../../core/graph"; -import {type Compatible, fromCompat, toCompat} from "../../util/compat"; +import {type NodeAddressT} from "../../core/graph"; import {compatReader} from "../../backend/compatIO"; -import {initiativeNodeType} from "./declaration"; import { type ReferenceDetector, MappedReferenceDetector, } from "../../core/references"; import { type Initiative, - type InitiativeWeight, - type InitiativeId, type InitiativeRepository, - createId, addressFromId, } from "./initiative"; - -export const INITIATIVE_FILE_SUBTYPE = "INITIATIVE_FILE"; +import { + type InitiativeFile, + fromJSON, + initiativeFileURL, + initiativeFileId, +} from "./initiativeFile"; /** * Represents an Initiatives directory. @@ -74,64 +73,6 @@ export async function loadDirectory( }; } -/** - * Represents a single Initiative using a file as source. - * - * 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 = {| - +title: string, - +timestampIso: ISOTimestamp, - +weight: InitiativeWeight, - +completed: boolean, - +dependencies: $ReadOnlyArray, - +references: $ReadOnlyArray, - +contributions: $ReadOnlyArray, - +champions: $ReadOnlyArray, -|}; - -// Note: setting this to opaque forces us to convert it to timestampMs. -opaque type ISOTimestamp = string; - -const COMPAT_INFO = {type: "sourcecred/initiativeFile", version: "0.1.0"}; - -export function fromJSON(j: Compatible): InitiativeFile { - return fromCompat(COMPAT_INFO, j); -} - -export function toJSON(m: InitiativeFile): Compatible { - return toCompat(COMPAT_INFO, m); -} - -/** - * When provided with the initiative NodeAddressT of an InitiativeFile this extracts - * the URL from it. Or null when the address is not for an InitiativeFile. - */ -export function initiativeFileURL(address: NodeAddressT): string | null { - const initiativeFilePrefix = NodeAddress.append( - initiativeNodeType.prefix, - INITIATIVE_FILE_SUBTYPE - ); - - if (!NodeAddress.hasPrefix(address, initiativeFilePrefix)) { - return null; - } - - const parts = NodeAddress.toParts(address); - const remoteUrl = parts[4]; - const fileName = parts[5]; - return `${remoteUrl}/${fileName}`; -} - -// Creates the InitiativeId for an InitiativeFile. -export function _initiativeFileId( - {remoteUrl}: InitiativesDirectory, - fileName: string -): InitiativeId { - return createId(INITIATIVE_FILE_SUBTYPE, remoteUrl, fileName); -} - // Checks the path exists and is a directory. // Returns the absolute path or throws. export async function _validatePath(localPath: string): Promise { @@ -209,7 +150,7 @@ export function _convertToInitiatives( const {timestampIso, ...partialInitiativeFile} = initiativeFile; const initiative: Initiative = { ...partialInitiativeFile, - id: _initiativeFileId(directory, fileName), + id: initiativeFileId(directory, fileName), timestampMs: Date.parse(timestampIso), }; initiatives.push(initiative); diff --git a/src/plugins/initiatives/initiativesDirectory.test.js b/src/plugins/initiatives/initiativesDirectory.test.js index 05d9efe..188a2a9 100644 --- a/src/plugins/initiatives/initiativesDirectory.test.js +++ b/src/plugins/initiatives/initiativesDirectory.test.js @@ -4,17 +4,11 @@ import tmp from "tmp"; import path from "path"; import fs from "fs-extra"; import stringify from "json-stable-stringify"; -import {NodeAddress} from "../../core/graph"; import {MappedReferenceDetector} from "../../core/references"; import {type Initiative, createId, addressFromId} from "./initiative"; import { - type InitiativeFile, type InitiativesDirectory, - fromJSON, - toJSON, - initiativeFileURL, loadDirectory, - _initiativeFileId, _validatePath, _findFiles, _readFiles, @@ -22,6 +16,7 @@ import { _convertToInitiatives, _createReferenceMap, } from "./initiativesDirectory"; +import {type InitiativeFile} from "./initiativeFile"; const exampleInitiativeFile = (): InitiativeFile => ({ title: "Sample initiative", @@ -44,64 +39,6 @@ const exampleInitiative = (remoteUrl: string, fileName: string): Initiative => { }; describe("plugins/initiatives/initiativesDirectory", () => { - describe("toJSON/fromJSON", () => { - it("should handle an example round-trip", () => { - // Given - const initiativeFile = exampleInitiativeFile(); - - // When - const actual = fromJSON(toJSON(initiativeFile)); - - // Then - expect(actual).toEqual(initiativeFile); - }); - }); - - describe("initiativeFileURL", () => { - it("should return null for a different prefix", () => { - // Given - const address = NodeAddress.fromParts(["foobar"]); - - // When - const url = initiativeFileURL(address); - - // Then - expect(url).toEqual(null); - }); - - it("should detect the correct prefix and create a URL", () => { - // Given - const remoteUrl = "http://foo.bar/dir"; - const fileName = "sample.json"; - const address = addressFromId( - createId("INITIATIVE_FILE", remoteUrl, fileName) - ); - - // When - const url = initiativeFileURL(address); - - // Then - expect(url).toEqual(`${remoteUrl}/${fileName}`); - }); - }); - - describe("_initiativeFileId", () => { - it("should add the correct prefix to a remoteUrl and fileName", () => { - // Given - const dir: InitiativesDirectory = { - localPath: "should-not-be-used", - remoteUrl: "http://foo.bar/dir", - }; - const fileName = "sample.json"; - - // When - const id = _initiativeFileId(dir, fileName); - - // Then - expect(id).toEqual(createId("INITIATIVE_FILE", dir.remoteUrl, fileName)); - }); - }); - describe("_validatePath", () => { it("should resolve relative paths", async () => { // Given