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