cli2 common: add util to load json files (#1868)

This adds two methods to cli2/common.js:
- loadJson, which loads a JSON file from disk and then parses it
- loadJsonWithDefault, which loads a JSON file from disk and parses it,
  or returns a default value if the file is not present.

Both methods are well documented and well tested.

Test plan: `yarn test`; see included unit tests which are thorough.
This commit is contained in:
Dandelion Mané 2020-06-17 22:53:17 -07:00 committed by GitHub
parent a8f353b33f
commit bca825b535
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 121 additions and 0 deletions

View File

@ -5,6 +5,7 @@ import fs from "fs-extra";
import type {PluginDirectoryContext} from "./cliPlugin"; import type {PluginDirectoryContext} from "./cliPlugin";
import {parse as parseConfig, type InstanceConfig} from "./instanceConfig"; import {parse as parseConfig, type InstanceConfig} from "./instanceConfig";
import * as C from "../util/combo";
export async function loadInstanceConfig( export async function loadInstanceConfig(
baseDir: string baseDir: string
@ -59,3 +60,53 @@ export function pluginDirectoryContext(
}, },
}; };
} }
/**
* Load and parse a JSON file from disk.
*
* If the file cannot be read, then an error is thrown.
* If parsing fails, an error is thrown.
*/
export async function loadJson<T>(
path: string,
parser: C.Parser<T>
): Promise<T> {
const contents = await fs.readFile(path);
return parser.parseOrThrow(JSON.parse(contents));
}
/**
* Load and parse a JSON file from disk, with a default to use if the file is
* not found.
*
* This is intended as a convenience for situations where the user may
* optionally provide configuration in a json file saved to disk.
*
* The default must be provided as a function that returns a default, to
* accommodate situations where the object may be mutable, or where constructing
* the default may be expensive.
*
* If no file is present at that location, then the default constructor is
* invoked to create a default value, and that is returned.
*
* If attempting to load the file fails for any reason other than ENOENT
* (e.g. the path actually is a directory), then the error is thrown.
*
* If parsing fails, an error is thrown.
*/
export async function loadJsonWithDefault<T>(
path: string,
parser: C.Parser<T>,
def: () => T
): Promise<T> {
try {
const contents = await fs.readFile(path);
return parser.parseOrThrow(JSON.parse(contents));
} catch (e) {
if (e.code === "ENOENT") {
return def();
} else {
throw e;
}
}
}

70
src/cli2/common.test.js Normal file
View File

@ -0,0 +1,70 @@
// @flow
import {loadJson, loadJsonWithDefault} from "./common";
import tmp from "tmp";
import fs from "fs-extra";
import * as C from "../util/combo";
import {join as pathJoin} from "path";
describe("cli2/common", () => {
function tmpWithContents(contents: mixed) {
const name = tmp.tmpNameSync();
fs.writeFileSync(name, JSON.stringify(contents));
return name;
}
describe("loadJson / loadJsonWithDefault", () => {
const badPath = () => pathJoin(tmp.dirSync().name, "not-a-real-path");
const fooParser = C.object({foo: C.number});
const fooInstance = Object.freeze({foo: 42});
const fooDefault = () => ({foo: 1337});
const barInstance = Object.freeze({bar: "1337"});
it("loadJson works when valid file is present", async () => {
const f = tmpWithContents(fooInstance);
expect(await loadJson(f, fooParser)).toEqual(fooInstance);
});
it("loadJson errors if the path does not exist", async () => {
const fail = async () => await loadJson(badPath(), fooParser);
await expect(fail).rejects.toThrow("ENOENT");
});
it("loadJson errors if the combo parse fails", async () => {
const f = tmpWithContents(barInstance);
const fail = async () => await loadJson(f, fooParser);
await expect(fail).rejects.toThrow("missing key");
});
it("loadJson errors if JSON.parse fails", async () => {
const f = tmp.tmpNameSync();
fs.writeFileSync(f, "zzz");
const fail = async () => await loadJson(f, C.raw);
await expect(fail).rejects.toThrow();
});
it("loadJsonWithDefault works when valid file is present", async () => {
const f = tmpWithContents(fooInstance);
expect(await loadJsonWithDefault(f, fooParser, fooDefault)).toEqual(
fooInstance
);
});
it("loadJsonWithDefault loads default if file not present", async () => {
expect(
await loadJsonWithDefault(badPath(), fooParser, fooDefault)
).toEqual(fooDefault());
});
it("loadJsonWithDefault errors if parse fails", async () => {
const f = tmpWithContents(barInstance);
const fail = async () =>
await loadJsonWithDefault(f, fooParser, fooDefault);
await expect(fail).rejects.toThrow("missing key");
});
it("loadJsonWithDefault errors if JSON.parse fails", async () => {
const f = tmp.tmpNameSync();
fs.writeFileSync(f, "zzz");
const fail = async () => await loadJsonWithDefault(f, C.raw, fooDefault);
await expect(fail).rejects.toThrow();
});
it("loadJsonWithDefault errors if file loading fails for a non-ENOENT reason", async () => {
const directoryPath = tmp.dirSync().name;
const fail = async () =>
await loadJsonWithDefault(directoryPath, fooParser, fooDefault);
await expect(fail).rejects.toThrow("EISDIR");
});
});
});