5etools-utils 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Api.js +6 -2
- package/lib/TestJson.js +121 -0
- package/lib/UtilFs.js +64 -0
- package/lib/UtilMisc.js +22 -0
- package/package.json +4 -2
- package/lib/HelloWorld.js +0 -3
package/lib/Api.js
CHANGED
package/lib/TestJson.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
|
|
3
|
+
import Ajv from "ajv";
|
|
4
|
+
import * as jsonSourceMap from "json-source-map";
|
|
5
|
+
|
|
6
|
+
import * as uf from "./UtilFs.js";
|
|
7
|
+
import * as um from "./UtilMisc.js";
|
|
8
|
+
|
|
9
|
+
const _IS_SORT_RESULTS = !process.env.VET_TEST_JSON_RESULTS_UNSORTED;
|
|
10
|
+
const _IS_TRIM_RESULTS = !process.env.VET_TEST_JSON_RESULTS_UNTRIMMED;
|
|
11
|
+
|
|
12
|
+
class JsonTester {
|
|
13
|
+
static _RE_DATE = /^\d\d\d\d-\d\d-\d\d$/;
|
|
14
|
+
|
|
15
|
+
constructor (
|
|
16
|
+
{
|
|
17
|
+
dirSchema,
|
|
18
|
+
tagLog = "JSON",
|
|
19
|
+
},
|
|
20
|
+
) {
|
|
21
|
+
this._dirSchema = dirSchema;
|
|
22
|
+
this._tagLog = tagLog;
|
|
23
|
+
|
|
24
|
+
// region Set up validator
|
|
25
|
+
this._ajv = new Ajv({
|
|
26
|
+
allowUnionTypes: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this._ajv.addKeyword({
|
|
30
|
+
keyword: "version",
|
|
31
|
+
validate: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this._ajv.addFormat(
|
|
35
|
+
"date",
|
|
36
|
+
{
|
|
37
|
+
validate: str => JsonTester._RE_DATE.test(str),
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
// endregion
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Add any implicit data to the JSON
|
|
45
|
+
*/
|
|
46
|
+
_addImplicits (obj, lastKey) {
|
|
47
|
+
if (typeof obj !== "object") return;
|
|
48
|
+
if (obj == null) return;
|
|
49
|
+
if (obj instanceof Array) obj.forEach(it => this._addImplicits(it, lastKey));
|
|
50
|
+
else {
|
|
51
|
+
// "obj.mode" will be set if this is in a "_copy" etc. block
|
|
52
|
+
if (lastKey === "spellcasting" && !obj.mode) obj.type = obj.type || "spellcasting";
|
|
53
|
+
|
|
54
|
+
Object.entries(obj).forEach(([k, v]) => this._addImplicits(v, k));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_getFileErrors ({filePath}) { return `${filePath}\n${JSON.stringify(this._ajv.errors, null, 2)}`; }
|
|
59
|
+
|
|
60
|
+
_handleError ({filePath, errors, errorsFull, data}) {
|
|
61
|
+
if (!process.env.CI) errorsFull.push(this._getFileErrors({filePath}));
|
|
62
|
+
|
|
63
|
+
// Sort the deepest errors to the bottom, as these are the ones we're most likely to be the ones we care about
|
|
64
|
+
// manually checking.
|
|
65
|
+
if (_IS_SORT_RESULTS) {
|
|
66
|
+
this._ajv.errors.sort((a, b) => (a.instancePath.length ?? -1) - (b.instancePath.length ?? -1));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// If there are an excessive number of errors, it's probably a junk entry; show only the first error and let the
|
|
70
|
+
// user figure it out.
|
|
71
|
+
if (_IS_TRIM_RESULTS && this._ajv.errors.length > 5) {
|
|
72
|
+
console.error(`(${this._ajv.errors.length} errors found, showing (hopefully) most-relevant one)`);
|
|
73
|
+
this._ajv.errors = this._ajv.errors.slice(-1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add line numbers
|
|
77
|
+
const sourceMap = jsonSourceMap.stringify(data, null, "\t");
|
|
78
|
+
this._ajv.errors.forEach(it => {
|
|
79
|
+
const errorPointer = sourceMap.pointers[it.instancePath];
|
|
80
|
+
it.lineNumberStart = errorPointer.value.line;
|
|
81
|
+
it.lineNumberEnd = errorPointer.valueEnd.line;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const error = this._getFileErrors({filePath});
|
|
85
|
+
um.error(this._tagLog, error);
|
|
86
|
+
errors.push(error);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getErrors () {
|
|
90
|
+
um.info(this._tagLog, `Validating JSON against schema`);
|
|
91
|
+
|
|
92
|
+
uf.listJsonFiles(this._dirSchema)
|
|
93
|
+
.forEach(filePath => {
|
|
94
|
+
filePath = path.normalize(filePath);
|
|
95
|
+
const contents = uf.readJSON(filePath);
|
|
96
|
+
|
|
97
|
+
const relativeFilePath = path.relative(this._dirSchema, filePath)
|
|
98
|
+
.replace(/\\/g, "/");
|
|
99
|
+
|
|
100
|
+
this._ajv.addSchema(contents, relativeFilePath);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const errors = [];
|
|
104
|
+
const errorsFull = [];
|
|
105
|
+
|
|
106
|
+
uf.runOnDirs((dir) => {
|
|
107
|
+
uf.listJsonFiles(dir)
|
|
108
|
+
.forEach(filePath => {
|
|
109
|
+
const data = uf.readJSON(filePath);
|
|
110
|
+
this._addImplicits(data);
|
|
111
|
+
const valid = this._ajv.validate("homebrew.json", data);
|
|
112
|
+
|
|
113
|
+
if (!valid) this._handleError({filePath, errors, errorsFull, data});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {errors, errorsFull};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export {JsonTester};
|
package/lib/UtilFs.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
|
|
3
|
+
function isDirectory (path) {
|
|
4
|
+
return fs.lstatSync(path).isDirectory();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function readJSON (path) {
|
|
8
|
+
try {
|
|
9
|
+
if (fs.existsSync(path)) {
|
|
10
|
+
let str = fs.readFileSync(path, "utf8");
|
|
11
|
+
// Strip BOM(s)
|
|
12
|
+
while (str.charCodeAt(0) === 0xFEFF) str = str.slice(1);
|
|
13
|
+
return JSON.parse(str);
|
|
14
|
+
} else {
|
|
15
|
+
const parts = path.split(/[\\/]+/g);
|
|
16
|
+
const dir = parts.slice(0, -1).join("/");
|
|
17
|
+
const originalName = parts[parts.length - 1];
|
|
18
|
+
const filenames = fs.readdirSync(dir, "utf8");
|
|
19
|
+
const filename = filenames.find(it => it.toLowerCase() === originalName.toLowerCase());
|
|
20
|
+
if (filename) return JSON.parse(fs.readFileSync(`${dir}/${filename}`, "utf8"));
|
|
21
|
+
else throw new Error(`Could not find file "${path}"`);
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
e.message += ` (Path: ${path})`;
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listJsonFiles (dir) {
|
|
30
|
+
const dirContent = fs.readdirSync(dir, "utf8")
|
|
31
|
+
.map(file => `${dir}/${file}`);
|
|
32
|
+
return dirContent.reduce((acc, file) => {
|
|
33
|
+
if (isDirectory(file)) acc.push(...listJsonFiles(file));
|
|
34
|
+
else {
|
|
35
|
+
if (file.toLowerCase().endsWith(".json")) acc.push(file);
|
|
36
|
+
}
|
|
37
|
+
return acc;
|
|
38
|
+
}, []);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runOnDirs (fn) {
|
|
42
|
+
fs.readdirSync(".", "utf8")
|
|
43
|
+
.filter(dir => isDirectory(dir) && !dir.startsWith(".") && !dir.startsWith("_") && dir !== "node_modules")
|
|
44
|
+
.forEach(dir => fn(dir));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mkDirs (pathToCreate) {
|
|
48
|
+
pathToCreate
|
|
49
|
+
.split(/[\\/]/g)
|
|
50
|
+
.reduce((currentPath, folder) => {
|
|
51
|
+
currentPath += `${folder}/`;
|
|
52
|
+
if (!fs.existsSync(currentPath)) {
|
|
53
|
+
fs.mkdirSync(currentPath);
|
|
54
|
+
}
|
|
55
|
+
return currentPath;
|
|
56
|
+
}, "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
readJSON,
|
|
61
|
+
listJsonFiles,
|
|
62
|
+
runOnDirs,
|
|
63
|
+
mkDirs,
|
|
64
|
+
};
|
package/lib/UtilMisc.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function _taggedConsole (fn, tag, ...args) {
|
|
2
|
+
const expandedTag = tag.padStart(12, " ");
|
|
3
|
+
fn(`[${expandedTag}]`, ...args);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function warn (tag, ...args) {
|
|
7
|
+
_taggedConsole(console.warn, tag, ...args);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function info (tag, ...args) {
|
|
11
|
+
_taggedConsole(console.info, tag, ...args);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function error (tag, ...args) {
|
|
15
|
+
_taggedConsole(console.error, tag, ...args);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
warn,
|
|
20
|
+
info,
|
|
21
|
+
error,
|
|
22
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "5etools-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Shared utilities for the 5etools ecosystem.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/Api.js",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"lib": "./lib"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "
|
|
14
|
+
"test": "npm run test:js",
|
|
15
|
+
"test:js": "eslint lib",
|
|
16
|
+
"lint:js": "eslint lib --fix"
|
|
15
17
|
},
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
package/lib/HelloWorld.js
DELETED