Split initiativeFile from initiativesDirectory (#1748)

As we're looking to add more features to InitiativeFile, the single source
file would grow large. Anticipating this we're splitting one type and it's
related functions off into a new file.
This commit is contained in:
Robin van Boven 2020-04-15 14:42:49 +02:00 committed by GitHub
parent f362a2409a
commit 7caca360a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 161 additions and 132 deletions

View File

@ -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);

View File

@ -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<URL>,
+references: $ReadOnlyArray<URL>,
+contributions: $ReadOnlyArray<URL>,
+champions: $ReadOnlyArray<URL>,
|};
// 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<any>): InitiativeFile {
return fromCompat(COMPAT_INFO, j);
}
export function toJSON(m: InitiativeFile): Compatible<InitiativeFile> {
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);
}

View File

@ -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));
});
});
});

View File

@ -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<URL>,
+references: $ReadOnlyArray<URL>,
+contributions: $ReadOnlyArray<URL>,
+champions: $ReadOnlyArray<URL>,
|};
// 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<any>): InitiativeFile {
return fromCompat(COMPAT_INFO, j);
}
export function toJSON(m: InitiativeFile): Compatible<InitiativeFile> {
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<string> {
@ -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);

View File

@ -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