v8r 0.13.0 → 0.14.0
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/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/package.json +8 -3
- package/src/ajv.js +8 -4
- package/src/catalogs.js +57 -18
- package/src/cli.js +12 -1
- package/src/io.js +12 -4
- package/src/test-helpers.js +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 📦 [0.14.0](https://www.npmjs.com/package/v8r/v/0.14.0) - 2023-01-28
|
|
4
|
+
|
|
5
|
+
* Throw an error if multiple versions of a schema are found in the catalog,
|
|
6
|
+
instead of assuming the latest version
|
|
7
|
+
|
|
8
|
+
## 📦 [0.13.1](https://www.npmjs.com/package/v8r/v/0.13.1) - 2022-12-10
|
|
9
|
+
|
|
10
|
+
* Resolve external `$ref`s in local schemas
|
|
11
|
+
|
|
3
12
|
## 📦 [0.13.0](https://www.npmjs.com/package/v8r/v/0.13.0) - 2022-06-11
|
|
4
13
|
|
|
5
14
|
* Overhaul of CLI output/machine-readable output. Validation results are sent to stdout. Log messages are now sent to stderr only. Pass `--format [text|json] (default: text)` to specify what is sent to stdout.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
v8r is a command-line JSON and YAML validator that uses [Schema Store](https://www.schemastore.org/) to detect a suitable schema for your input files based on the filename.
|
|
10
10
|
|
|
11
11
|
## Getting Started
|
|
12
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "v8r",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "A command-line JSON and YAML validator that's on your wavelength",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "V8R_CACHE_NAME=v8r-test c8 --reporter=text mocha \"src/**/*.spec.js\"",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
},
|
|
16
16
|
"main": "src/index.js",
|
|
17
17
|
"exports": "./src/index.js",
|
|
18
|
+
"files": [
|
|
19
|
+
"src/**/!(*.spec).js",
|
|
20
|
+
"config-schema.json",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
18
23
|
"repository": {
|
|
19
24
|
"type": "git",
|
|
20
25
|
"url": "git+https://github.com/chris48s/v8r.git"
|
|
@@ -27,7 +32,7 @@
|
|
|
27
32
|
"ajv-draft-04": "^1.0.0",
|
|
28
33
|
"ajv-formats": "^2.1.1",
|
|
29
34
|
"chalk": "^5.0.0",
|
|
30
|
-
"cosmiconfig": "^
|
|
35
|
+
"cosmiconfig": "^8.0.0",
|
|
31
36
|
"decamelize": "^6.0.0",
|
|
32
37
|
"flat-cache": "^3.0.4",
|
|
33
38
|
"glob": "^8.0.1",
|
|
@@ -35,7 +40,7 @@
|
|
|
35
40
|
"is-url": "^1.2.4",
|
|
36
41
|
"js-yaml": "^4.0.0",
|
|
37
42
|
"json5": "^2.2.0",
|
|
38
|
-
"minimatch": "^
|
|
43
|
+
"minimatch": "^6.1.6",
|
|
39
44
|
"yargs": "^17.0.1"
|
|
40
45
|
},
|
|
41
46
|
"devDependencies": {
|
package/src/ajv.js
CHANGED
|
@@ -9,8 +9,12 @@ import Ajv2019 from "ajv/dist/2019.js";
|
|
|
9
9
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
10
10
|
import addFormats from "ajv-formats";
|
|
11
11
|
|
|
12
|
-
function _ajvFactory(
|
|
13
|
-
|
|
12
|
+
function _ajvFactory(
|
|
13
|
+
schema,
|
|
14
|
+
strictMode,
|
|
15
|
+
cache,
|
|
16
|
+
resolver = (url) => cache.fetch(url)
|
|
17
|
+
) {
|
|
14
18
|
const opts = { allErrors: true, loadSchema: resolver, strict: strictMode };
|
|
15
19
|
|
|
16
20
|
if (
|
|
@@ -53,8 +57,8 @@ function _ajvFactory(schema, strictMode, cache) {
|
|
|
53
57
|
*/
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
async function validate(data, schema, strictMode, cache) {
|
|
57
|
-
const ajv = _ajvFactory(schema, strictMode, cache);
|
|
60
|
+
async function validate(data, schema, strictMode, cache, resolver) {
|
|
61
|
+
const ajv = _ajvFactory(schema, strictMode, cache, resolver);
|
|
58
62
|
addFormats(ajv);
|
|
59
63
|
const validateFn = await ajv.compileAsync(schema);
|
|
60
64
|
const valid = validateFn(data);
|
package/src/catalogs.js
CHANGED
|
@@ -39,6 +39,41 @@ function getCatalogs(config) {
|
|
|
39
39
|
return catalogs;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function getMatchLogMessage(match) {
|
|
43
|
+
let outStr = "";
|
|
44
|
+
outStr += ` ${match.name}\n`;
|
|
45
|
+
if (match.description) {
|
|
46
|
+
outStr += ` ${match.description}\n`;
|
|
47
|
+
}
|
|
48
|
+
outStr += ` ${match.url || match.location}\n`;
|
|
49
|
+
return outStr;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getVersionLogMessage(match, versionId, versionSchemaUrl) {
|
|
53
|
+
let outStr = "";
|
|
54
|
+
outStr += ` ${match.name} (${versionId})\n`;
|
|
55
|
+
if (match.description) {
|
|
56
|
+
outStr += ` ${match.description}\n`;
|
|
57
|
+
}
|
|
58
|
+
outStr += ` ${versionSchemaUrl}\n`;
|
|
59
|
+
return outStr;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getMultipleMatchesLogMessage(matches) {
|
|
63
|
+
return matches
|
|
64
|
+
.map(function (match) {
|
|
65
|
+
if (Object.keys(match.versions || {}).length > 1) {
|
|
66
|
+
return Object.entries(match.versions)
|
|
67
|
+
.map(function ([versionId, versionSchemaUrl]) {
|
|
68
|
+
return getVersionLogMessage(match, versionId, versionSchemaUrl);
|
|
69
|
+
})
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
return getMatchLogMessage(match);
|
|
73
|
+
})
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
async function getMatchForFilename(catalogs, filename, cache) {
|
|
43
78
|
for (const [i, rec] of catalogs.entries()) {
|
|
44
79
|
const catalogLocation = rec.location;
|
|
@@ -67,32 +102,36 @@ async function getMatchForFilename(catalogs, filename, cache) {
|
|
|
67
102
|
const { schemas } = catalog;
|
|
68
103
|
const matches = getSchemaMatchesForFilename(schemas, filename);
|
|
69
104
|
logger.debug(`Searching for schema in ${catalogLocation} ...`);
|
|
70
|
-
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
(matches.length === 1 && matches[0].versions == null) ||
|
|
108
|
+
(matches.length === 1 && Object.keys(matches[0].versions).length === 1)
|
|
109
|
+
) {
|
|
71
110
|
logger.info(`Found schema in ${catalogLocation} ...`);
|
|
72
|
-
return coerceMatch(matches[0]); //
|
|
111
|
+
return coerceMatch(matches[0]); // Exactly one match found. We're done.
|
|
73
112
|
}
|
|
113
|
+
|
|
74
114
|
if (matches.length === 0 && i < catalogs.length - 1) {
|
|
75
|
-
continue; // No
|
|
115
|
+
continue; // No matches found. Try the next catalog in the array.
|
|
76
116
|
}
|
|
77
|
-
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
matches.length > 1 ||
|
|
120
|
+
(matches.length === 1 &&
|
|
121
|
+
Object.keys(matches[0].versions || {}).length > 1)
|
|
122
|
+
) {
|
|
78
123
|
// We found >1 matches in the same catalog. This is always a hard error.
|
|
79
|
-
const matchesLog = matches
|
|
80
|
-
.map(function (match) {
|
|
81
|
-
let outStr = "";
|
|
82
|
-
outStr += ` ${match.name}\n`;
|
|
83
|
-
if (match.description) {
|
|
84
|
-
outStr += ` ${match.description}\n`;
|
|
85
|
-
}
|
|
86
|
-
outStr += ` ${match.url || match.location}\n`;
|
|
87
|
-
return outStr;
|
|
88
|
-
})
|
|
89
|
-
.join("\n");
|
|
124
|
+
const matchesLog = getMultipleMatchesLogMessage(matches);
|
|
90
125
|
logger.info(
|
|
91
|
-
`Found multiple possible
|
|
126
|
+
`Found multiple possible matches for ${filename}. Possible matches:\n\n${matchesLog}`
|
|
127
|
+
);
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Found multiple possible schemas to validate ${filename}`
|
|
92
130
|
);
|
|
93
131
|
}
|
|
94
|
-
|
|
95
|
-
// in the last catalog
|
|
132
|
+
|
|
133
|
+
// We found 0 matches in the last catalog
|
|
134
|
+
// and there are no more catalogs left to try
|
|
96
135
|
throw new Error(`Could not find a schema to validate ${filename}`);
|
|
97
136
|
}
|
|
98
137
|
}
|
package/src/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import flatCache from "flat-cache";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import os from "os";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import isUrl from "is-url";
|
|
5
6
|
import { validate } from "./ajv.js";
|
|
6
7
|
import { Cache } from "./cache.js";
|
|
7
8
|
import { getCatalogs, getMatchForFilename } from "./catalogs.js";
|
|
@@ -60,7 +61,17 @@ async function validateFile(filename, config, cache) {
|
|
|
60
61
|
);
|
|
61
62
|
|
|
62
63
|
const strictMode = config.verbose >= 2 ? "log" : false;
|
|
63
|
-
const
|
|
64
|
+
const resolver = isUrl(schemaLocation)
|
|
65
|
+
? (location) => getFromUrlOrFile(location, cache)
|
|
66
|
+
: (location) =>
|
|
67
|
+
getFromUrlOrFile(location, cache, path.dirname(schemaLocation));
|
|
68
|
+
const { valid, errors } = await validate(
|
|
69
|
+
data,
|
|
70
|
+
schema,
|
|
71
|
+
strictMode,
|
|
72
|
+
cache,
|
|
73
|
+
resolver
|
|
74
|
+
);
|
|
64
75
|
result.valid = valid;
|
|
65
76
|
result.errors = errors;
|
|
66
77
|
if (valid) {
|
package/src/io.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
2
3
|
import isUrl from "is-url";
|
|
3
4
|
|
|
4
|
-
async function getFromUrlOrFile(location, cache) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
async function getFromUrlOrFile(location, cache, base = null) {
|
|
6
|
+
if (isUrl(location)) {
|
|
7
|
+
return await cache.fetch(location);
|
|
8
|
+
} else {
|
|
9
|
+
if (base != null) {
|
|
10
|
+
return JSON.parse(
|
|
11
|
+
await fs.promises.readFile(path.join(base, location), "utf8")
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return JSON.parse(await fs.promises.readFile(location, "utf8"));
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
export { getFromUrlOrFile };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chai from "chai";
|
|
2
|
+
import chaiAsPromised from "chai-as-promised";
|
|
3
|
+
import flatCache from "flat-cache";
|
|
4
|
+
import logger from "./logger.js";
|
|
5
|
+
|
|
6
|
+
chai.use(chaiAsPromised);
|
|
7
|
+
|
|
8
|
+
const origWriteOut = logger.writeOut;
|
|
9
|
+
const origWriteErr = logger.writeErr;
|
|
10
|
+
const testCacheName = process.env.V8R_CACHE_NAME;
|
|
11
|
+
|
|
12
|
+
function setUp() {
|
|
13
|
+
flatCache.clearCacheById(testCacheName);
|
|
14
|
+
logger.resetStdout();
|
|
15
|
+
logger.resetStderr();
|
|
16
|
+
logger.writeOut = function () {};
|
|
17
|
+
logger.writeErr = function () {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tearDown() {
|
|
21
|
+
flatCache.clearCacheById(testCacheName);
|
|
22
|
+
logger.resetStdout();
|
|
23
|
+
logger.resetStderr();
|
|
24
|
+
logger.writeOut = origWriteOut;
|
|
25
|
+
logger.writeErr = origWriteErr;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isString(el) {
|
|
29
|
+
return typeof el === "string" || el instanceof String;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function logContainsSuccess(expectedString, expectedCount = 1) {
|
|
33
|
+
const counter = (count, el) =>
|
|
34
|
+
count + (isString(el) && el.includes("✔") && el.includes(expectedString));
|
|
35
|
+
return logger.stderr.reduce(counter, 0) === expectedCount;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function logContainsInfo(expectedString, expectedCount = 1) {
|
|
39
|
+
const counter = (count, el) =>
|
|
40
|
+
count + (isString(el) && el.includes("ℹ") && el.includes(expectedString));
|
|
41
|
+
return logger.stderr.reduce(counter, 0) === expectedCount;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function logContainsError(expectedString, expectedCount = 1) {
|
|
45
|
+
const counter = (count, el) =>
|
|
46
|
+
count + (isString(el) && el.includes("✖") && el.includes(expectedString));
|
|
47
|
+
return logger.stderr.reduce(counter, 0) === expectedCount;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
chai,
|
|
52
|
+
testCacheName,
|
|
53
|
+
setUp,
|
|
54
|
+
tearDown,
|
|
55
|
+
logContainsSuccess,
|
|
56
|
+
logContainsInfo,
|
|
57
|
+
logContainsError,
|
|
58
|
+
};
|