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:
parent
a8f353b33f
commit
bca825b535
|
@ -5,6 +5,7 @@ import fs from "fs-extra";
|
|||
|
||||
import type {PluginDirectoryContext} from "./cliPlugin";
|
||||
import {parse as parseConfig, type InstanceConfig} from "./instanceConfig";
|
||||
import * as C from "../util/combo";
|
||||
|
||||
export async function loadInstanceConfig(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue