v8r 0.5.0 → 0.8.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## đŸ“Ļ [0.8.0](https://www.npmjs.com/package/v8r/v/0.9.0) - 2021-12-25
4
+
5
+ * Switch from CommonJS to ESModules internally
6
+ * Requires node `^12.20.0 || ^14.13.1 || >=15.0.0`
7
+
8
+ ## đŸ“Ļ [0.7.0](https://www.npmjs.com/package/v8r/v/0.7.0) - 2021-11-30
9
+
10
+ * Upgrade to ajv 8 internally
11
+ Adds compatibility for JSON Schema draft 2019-09 and draft 2020-12
12
+ * Docs/logging improvements to clarify behaviour of `--catalogs` param
13
+
14
+ ## đŸ“Ļ [0.6.1](https://www.npmjs.com/package/v8r/v/0.6.1) - 2021-08-06
15
+
16
+ * Refactor cache module to remove global state
17
+
18
+ ## đŸ“Ļ [0.6.0](https://www.npmjs.com/package/v8r/v/0.6.0) - 2021-07-28
19
+
20
+ * Add the ability to search custom schema catalogs using the `--catalogs` param
21
+
3
22
  ## đŸ“Ļ [0.5.0](https://www.npmjs.com/package/v8r/v/0.5.0) - 2021-01-13
4
23
 
5
24
  * Allow validation against a local schema
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # v8r
2
2
 
3
- ![Build status](https://github.com/chris48s/v8r/workflows/Run%20tests/badge.svg?branch=main)
4
- [![codecov](https://codecov.io/gh/chris48s/v8r/branch/main/graph/badge.svg?token=KL998A5CJH)](https://codecov.io/gh/chris48s/v8r)
5
- ![NPM version](https://img.shields.io/npm/v/v8r.svg)
6
- ![License](https://img.shields.io/npm/l/v8r.svg)
7
- ![Node Compatibility](https://img.shields.io/node/v/v8r.svg)
3
+ ![build](https://raw.githubusercontent.com/chris48s/v8r/badges/.badges/main/build-status.svg)
4
+ ![coverage](https://raw.githubusercontent.com/chris48s/v8r/badges/.badges/main/coverage.svg)
5
+ ![version](https://raw.githubusercontent.com/chris48s/v8r/badges/.badges/main/package-version.svg)
6
+ ![license](https://raw.githubusercontent.com/chris48s/v8r/badges/.badges/main/package-license.svg)
7
+ ![node](https://raw.githubusercontent.com/chris48s/v8r/badges/.badges/main/package-node-version.svg)
8
8
 
9
9
  A command-line JSON and YAML validator that's on your wavelength.
10
10
 
@@ -50,10 +50,25 @@ Validating action.yml against schema from https://json.schemastore.org/github-ac
50
50
  $ v8r feature.geojson
51
51
  ❌ Could not find a schema to validate feature.geojson
52
52
 
53
- # ..you can specify one
53
+ # ..you can specify a schema
54
54
  $ v8r feature.geojson -s https://json.schemastore.org/geojson
55
55
  Validating feature.geojson against schema from https://json.schemastore.org/geojson ...
56
56
  ✅ feature.geojson is valid
57
+
58
+ # ..or use a custom catalog
59
+ # v8r will search any custom catalogs before falling back to Schema Store
60
+ $ cat > my-catalog.json <<EOF
61
+ { "\$schema": "https://json.schemastore.org/schema-catalog.json",
62
+ "version": 1,
63
+ "schemas": [ { "name": "geojson",
64
+ "description": "geojson",
65
+ "url": "https://json.schemastore.org/geojson.json",
66
+ "fileMatch": ["*.geojson"] } ] }
67
+ EOF
68
+ $ v8r feature.geojson -c my-catalog.json
69
+ â„šī¸ Found schema in my-catalog.json ...
70
+ Validating feature.geojson against schema from https://json.schemastore.org/geojson ...
71
+ ✅ feature.geojson is valid
57
72
  ```
58
73
 
59
74
  ## Exit codes
@@ -89,7 +104,13 @@ Validating feature.geojson against schema from https://json.schemastore.org/geoj
89
104
 
90
105
  ### ❓ What JSON schema versions are supported?
91
106
 
92
- 💡 `v8r` works with JSON schema draft-04, draft-06 and draft-07.
107
+ 💡 `v8r` works with JSON schema drafts:
108
+
109
+ * draft-04
110
+ * draft-06
111
+ * draft-07
112
+ * draft 2019-09
113
+ * draft 2020-12
93
114
 
94
115
  ### ❓ Will 100% of the schemas on schemastore.org work with this tool?
95
116
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "v8r",
3
- "version": "0.5.0",
3
+ "version": "0.8.0",
4
4
  "description": "A command-line JSON and YAML validator that's on your wavelength",
5
5
  "scripts": {
6
- "test": "V8R_CACHE_NAME=v8r-test nyc --reporter=text mocha \"src/**/*.spec.js\"",
6
+ "test": "V8R_CACHE_NAME=v8r-test c8 --reporter=text mocha \"src/**/*.spec.js\"",
7
7
  "lint": "eslint \"src/**/*.js\"",
8
- "coverage": "nyc report --reporter=text-lcov > coverage.lcov",
8
+ "coverage": "c8 report --reporter=cobertura",
9
9
  "prettier": "prettier --write \"**/*.js\"",
10
10
  "prettier:check": "prettier --check \"**/*.js\"",
11
11
  "v8r": "src/index.js"
@@ -14,6 +14,7 @@
14
14
  "v8r": "src/index.js"
15
15
  },
16
16
  "main": "src/index.js",
17
+ "exports": "./src/index.js",
17
18
  "repository": {
18
19
  "type": "git",
19
20
  "url": "git+https://github.com/chris48s/v8r.git"
@@ -22,29 +23,32 @@
22
23
  "author": "chris48s",
23
24
  "license": "MIT",
24
25
  "dependencies": {
25
- "ajv": "^6.12.6",
26
+ "ajv": "^8.8.2",
27
+ "ajv-draft-04": "^1.0.0",
28
+ "ajv-formats": "^2.1.1",
26
29
  "flat-cache": "^3.0.4",
27
30
  "got": "^11.8.0",
28
31
  "is-url": "^1.2.4",
29
32
  "js-yaml": "^4.0.0",
30
33
  "minimatch": "^3.0.4",
31
- "yargs": "^16.1.0"
34
+ "yargs": "^17.0.1"
32
35
  },
33
36
  "devDependencies": {
37
+ "c8": "^7.10.0",
34
38
  "chai": "^4.2.0",
35
39
  "chai-as-promised": "^7.1.1",
36
- "eslint": "^7.16.0",
37
- "eslint-config-prettier": "^7.1.0",
38
- "eslint-plugin-mocha": "^8.0.0",
39
- "eslint-plugin-prettier": "^3.3.0",
40
- "mocha": "^8.2.1",
40
+ "eslint": "^8.0.1",
41
+ "eslint-config-prettier": "^8.1.0",
42
+ "eslint-plugin-mocha": "^9.0.0",
43
+ "eslint-plugin-prettier": "^4.0.0",
44
+ "mocha": "^9.0.0",
41
45
  "nock": "^13.0.4",
42
- "nyc": "^15.1.0",
43
46
  "prettier": "^2.1.2"
44
47
  },
45
48
  "engines": {
46
- "node": ">= 12"
49
+ "node": "^12.20.0 || ^14.13.1 || >=15.0.0"
47
50
  },
51
+ "type": "module",
48
52
  "keywords": [
49
53
  "JSON",
50
54
  "YAML",
package/src/ajv.js ADDED
@@ -0,0 +1,69 @@
1
+ import { createRequire } from "module";
2
+ // TODO: once JSON modules is stable these requires could become imports
3
+ // https://nodejs.org/api/esm.html#esm_experimental_json_modules
4
+ const require = createRequire(import.meta.url);
5
+
6
+ import AjvDraft4 from "ajv-draft-04";
7
+ import Ajv from "ajv";
8
+ import Ajv2019 from "ajv/dist/2019.js";
9
+ import Ajv2020 from "ajv/dist/2020.js";
10
+ import addFormats from "ajv-formats";
11
+
12
+ function _ajvFactory(schema, cache) {
13
+ const resolver = (url) => cache.fetch(url);
14
+ const opts = { loadSchema: resolver, strict: "log" };
15
+
16
+ if (
17
+ typeof schema["$schema"] === "string" ||
18
+ schema["$schema"] instanceof String
19
+ ) {
20
+ if (schema["$schema"].includes("json-schema.org/draft-04/schema")) {
21
+ opts.schemaId = "auto";
22
+ return new AjvDraft4(opts);
23
+ } else if (schema["$schema"].includes("json-schema.org/draft-06/schema")) {
24
+ const ajvDraft06 = new Ajv(opts);
25
+ ajvDraft06.addMetaSchema(
26
+ require("ajv/lib/refs/json-schema-draft-06.json")
27
+ );
28
+ return ajvDraft06;
29
+ } else if (schema["$schema"].includes("json-schema.org/draft-07/schema")) {
30
+ return new Ajv(opts);
31
+ } else if (
32
+ schema["$schema"].includes("json-schema.org/draft/2019-09/schema")
33
+ ) {
34
+ return new Ajv2019(opts);
35
+ } else if (
36
+ schema["$schema"].includes("json-schema.org/draft/2020-12/schema")
37
+ ) {
38
+ return new Ajv2020(opts);
39
+ }
40
+ }
41
+
42
+ // hedge our bets as best we can
43
+ const ajv = new Ajv(opts);
44
+ ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json"));
45
+ return ajv;
46
+
47
+ /* TODO:
48
+ const ajv = new Ajv2019(opts);
49
+ ajv.addMetaSchema(require("ajv/dist/refs/json-schema-draft-07.json"));
50
+ return ajv
51
+
52
+ might also be an equally valid fallback here
53
+ */
54
+ }
55
+
56
+ async function validate(data, schema, cache) {
57
+ const ajv = _ajvFactory(schema, cache);
58
+ addFormats(ajv);
59
+ const validateFn = await ajv.compileAsync(schema);
60
+ const valid = validateFn(data);
61
+ if (!valid) {
62
+ console.log("\nErrors:");
63
+ console.log(validateFn.errors);
64
+ console.log("");
65
+ }
66
+ return valid;
67
+ }
68
+
69
+ export { _ajvFactory, validate };
package/src/cache.js CHANGED
@@ -1,66 +1,73 @@
1
- "use strict";
1
+ import got from "got";
2
2
 
3
- const got = require("got");
4
- const callCounter = {};
5
- const callLimit = 10;
6
-
7
- function expire(cache, ttl) {
8
- Object.entries(cache.all()).forEach(function ([url, cachedResponse]) {
9
- if (!("timestamp" in cachedResponse) || !("body" in cachedResponse)) {
10
- console.debug(`â„šī¸ Cache error: deleting malformed response`);
11
- cache.removeKey(url);
12
- } else if (Date.now() > cachedResponse.timestamp + ttl) {
13
- console.debug(`â„šī¸ Cache stale: deleting cached response from ${url}`);
14
- cache.removeKey(url);
15
- }
16
- cache.save(true);
17
- });
18
- }
19
-
20
- function limitDepth(url) {
21
- /*
22
- It is possible to create cyclic dependencies with external references
23
- in JSON schema. Ajv doesn't detect this when resolving external references,
24
- so we keep a count of how many times we've called the same URL.
25
- If we are calling the same URL over and over we've probably hit a circular
26
- external reference and we need to break the loop.
27
- */
28
- if (url in callCounter) {
29
- callCounter[url]++;
30
- } else {
31
- callCounter[url] = 1;
3
+ class Cache {
4
+ constructor(flatCache, ttl) {
5
+ this.cache = flatCache;
6
+ this.ttl = ttl;
7
+ this.callCounter = {};
8
+ this.callLimit = 10;
32
9
  }
33
- if (callCounter[url] > callLimit) {
34
- throw new Error(
35
- `❌ Called ${url} ${callLimit} times. Possible circular reference.`
10
+
11
+ expire() {
12
+ Object.entries(this.cache.all()).forEach(
13
+ function ([url, cachedResponse]) {
14
+ if (!("timestamp" in cachedResponse) || !("body" in cachedResponse)) {
15
+ console.debug(`â„šī¸ Cache error: deleting malformed response`);
16
+ this.cache.removeKey(url);
17
+ } else if (Date.now() > cachedResponse.timestamp + this.ttl) {
18
+ console.debug(`â„šī¸ Cache stale: deleting cached response from ${url}`);
19
+ this.cache.removeKey(url);
20
+ }
21
+ this.cache.save(true);
22
+ }.bind(this)
36
23
  );
37
24
  }
38
- }
39
25
 
40
- async function cachedFetch(url, cache, ttl) {
41
- limitDepth(url);
42
- expire(cache, ttl);
43
- const cachedResponse = cache.getKey(url);
44
- if (cachedResponse !== undefined) {
45
- console.debug(`â„šī¸ Cache hit: using cached response from ${url}`);
46
- return cachedResponse.body;
26
+ limitDepth(url) {
27
+ /*
28
+ It is possible to create cyclic dependencies with external references
29
+ in JSON schema. Ajv doesn't detect this when resolving external references,
30
+ so we keep a count of how many times we've called the same URL.
31
+ If we are calling the same URL over and over we've probably hit a circular
32
+ external reference and we need to break the loop.
33
+ */
34
+ if (url in this.callCounter) {
35
+ this.callCounter[url]++;
36
+ } else {
37
+ this.callCounter[url] = 1;
38
+ }
39
+ if (this.callCounter[url] > this.callLimit) {
40
+ throw new Error(
41
+ `❌ Called ${url} >${this.callLimit} times. Possible circular reference.`
42
+ );
43
+ }
47
44
  }
48
45
 
49
- try {
50
- console.debug(`â„šī¸ Cache miss: calling ${url}`);
51
- const resp = await got(url);
52
- const parsedBody = JSON.parse(resp.body);
53
- if (ttl > 0) {
54
- cache.setKey(url, { timestamp: Date.now(), body: parsedBody });
55
- cache.save(true);
46
+ async fetch(url) {
47
+ this.limitDepth(url);
48
+ this.expire();
49
+ const cachedResponse = this.cache.getKey(url);
50
+ if (cachedResponse !== undefined) {
51
+ console.debug(`â„šī¸ Cache hit: using cached response from ${url}`);
52
+ return cachedResponse.body;
56
53
  }
57
- return parsedBody;
58
- } catch (error) {
59
- if (error.response) {
60
- throw new Error(`❌ Failed fetching ${url}\n${error.response.body}`);
54
+
55
+ try {
56
+ console.debug(`â„šī¸ Cache miss: calling ${url}`);
57
+ const resp = await got(url);
58
+ const parsedBody = JSON.parse(resp.body);
59
+ if (this.ttl > 0) {
60
+ this.cache.setKey(url, { timestamp: Date.now(), body: parsedBody });
61
+ this.cache.save(true);
62
+ }
63
+ return parsedBody;
64
+ } catch (error) {
65
+ if (error.response) {
66
+ throw new Error(`❌ Failed fetching ${url}\n${error.response.body}`);
67
+ }
68
+ throw new Error(`❌ Failed fetching ${url}`);
61
69
  }
62
- throw new Error(`❌ Failed fetching ${url}`);
63
70
  }
64
71
  }
65
72
 
66
- module.exports = { cachedFetch, expire };
73
+ export { Cache };
package/src/cli.js CHANGED
@@ -1,25 +1,70 @@
1
- "use strict";
2
-
3
- const Ajv = require("ajv");
4
- const flatCache = require("flat-cache");
5
- const fs = require("fs");
6
- const isUrl = require("is-url");
7
- const minimatch = require("minimatch");
8
- const os = require("os");
9
- const path = require("path");
10
- const yaml = require("js-yaml");
11
- const yargs = require("yargs/yargs");
12
- const { hideBin } = require("yargs/helpers");
13
- const { cachedFetch } = require("./cache.js");
14
- const logging = require("./logging.js");
1
+ import flatCache from "flat-cache";
2
+ import fs from "fs";
3
+ import isUrl from "is-url";
4
+ import minimatch from "minimatch";
5
+ import os from "os";
6
+ import path from "path";
7
+ import yaml from "js-yaml";
8
+ import yargs from "yargs";
9
+ import { hideBin } from "yargs/helpers";
10
+ import { validate } from "./ajv.js";
11
+ import { Cache } from "./cache.js";
12
+ import logging from "./logging.js";
15
13
 
16
14
  const SCHEMASTORE_CATALOG_URL =
17
15
  "https://www.schemastore.org/api/json/catalog.json";
16
+
17
+ const SCHEMASTORE_CATALOG_SCHEMA_URL =
18
+ "https://json.schemastore.org/schema-catalog.json";
19
+
18
20
  const CACHE_DIR = path.join(os.tmpdir(), "flat-cache");
19
21
 
20
- async function getSchemaUrlForFilename(filename, cache, ttl) {
21
- const { schemas } = await cachedFetch(SCHEMASTORE_CATALOG_URL, cache, ttl);
22
+ async function getFromUrlOrFile(location, cache) {
23
+ return isUrl(location)
24
+ ? await cache.fetch(location)
25
+ : JSON.parse(fs.readFileSync(location, "utf8").toString());
26
+ }
27
+
28
+ async function getSchemaUrlForFilename(catalogs, filename, cache) {
29
+ for (const [i, catalogLocation] of catalogs.entries()) {
30
+ const catalog = await getFromUrlOrFile(catalogLocation, cache);
31
+ const catalogSchema = await getFromUrlOrFile(
32
+ SCHEMASTORE_CATALOG_SCHEMA_URL,
33
+ cache
34
+ );
35
+
36
+ // Validate the catalog
37
+ const valid = await validate(catalog, catalogSchema, cache);
38
+ if (!valid || catalog.schemas === undefined) {
39
+ throw new Error(`❌ Malformed catalog at ${catalogLocation}`);
40
+ }
41
+
42
+ const { schemas } = catalog;
43
+ const matches = getSchemaMatchesForFilename(schemas, filename);
44
+ console.debug(`â„šī¸ Searching for schema in ${catalogLocation} ...`);
45
+ if (matches.length === 1) {
46
+ console.log(`â„šī¸ Found schema in ${catalogLocation} ...`);
47
+ return matches[0].url; // Match found. We're done.
48
+ }
49
+ if (matches.length === 0 && i < catalogs.length - 1) {
50
+ continue; // No match found. Try the next catalog in the array.
51
+ }
52
+ if (matches.length > 1) {
53
+ // We found >1 matches in the same catalog. This is always a hard error.
54
+ console.log(
55
+ `Found multiple possible schemas for ${filename}. Possible matches:`
56
+ );
57
+ matches.forEach(function (match) {
58
+ console.log(`${match.description}: ${match.url}`);
59
+ });
60
+ }
61
+ // Either we found >1 matches in the same catalog or we found 0 matches
62
+ // in the last catalog and there are no more catalogs left to try.
63
+ throw new Error(`❌ Could not find a schema to validate ${filename}`);
64
+ }
65
+ }
22
66
 
67
+ function getSchemaMatchesForFilename(schemas, filename) {
23
68
  const matches = [];
24
69
  schemas.forEach(function (schema) {
25
70
  if ("fileMatch" in schema) {
@@ -35,33 +80,7 @@ async function getSchemaUrlForFilename(filename, cache, ttl) {
35
80
  }
36
81
  }
37
82
  });
38
-
39
- if (matches.length === 1) {
40
- return matches[0].url;
41
- }
42
- if (matches.length > 1) {
43
- console.log(
44
- `Found multiple possible schemas for ${filename}. Possible matches:`
45
- );
46
- matches.forEach(function (match) {
47
- console.log(`${match.description}: ${match.url}`);
48
- });
49
- }
50
- throw new Error(`❌ Could not find a schema to validate ${filename}`);
51
- }
52
-
53
- async function validate(data, schema, resolver) {
54
- const ajv = new Ajv({ schemaId: "auto", loadSchema: resolver });
55
- ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-04.json"));
56
- ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json"));
57
- const validate = await ajv.compileAsync(schema);
58
- const valid = validate(data);
59
- if (!valid) {
60
- console.log("\nErrors:");
61
- console.log(validate.errors);
62
- console.log("");
63
- }
64
- return valid;
83
+ return matches;
65
84
  }
66
85
 
67
86
  function parseFile(contents, format) {
@@ -82,7 +101,7 @@ function secondsToMilliseconds(seconds) {
82
101
  return seconds * 1000;
83
102
  }
84
103
 
85
- function getCache() {
104
+ function getFlatCache() {
86
105
  if (process.env.V8R_CACHE_NAME) {
87
106
  return flatCache.load(process.env.V8R_CACHE_NAME);
88
107
  }
@@ -90,35 +109,29 @@ function getCache() {
90
109
  }
91
110
 
92
111
  function Validator() {
93
- const cache = getCache();
94
-
95
112
  return async function (args) {
96
113
  const filename = args.filename;
97
114
  const ttl = secondsToMilliseconds(args.cacheTtl || 0);
115
+ const cache = new Cache(getFlatCache(), ttl);
98
116
 
99
117
  const data = parseFile(
100
118
  fs.readFileSync(filename, "utf8").toString(),
101
119
  path.extname(filename)
102
120
  );
121
+
103
122
  const schemaLocation =
104
- args.schema || (await getSchemaUrlForFilename(filename, cache, ttl));
105
- const schema = isUrl(schemaLocation)
106
- ? await cachedFetch(schemaLocation, cache, ttl)
107
- : JSON.parse(fs.readFileSync(schemaLocation, "utf8").toString());
108
- if (
109
- "$schema" in schema &&
110
- schema.$schema.includes("json-schema.org/draft/2019-09/schema")
111
- ) {
112
- throw new Error(`❌ Unsupported JSON schema version ${schema.$schema}`);
113
- }
123
+ args.schema ||
124
+ (await getSchemaUrlForFilename(
125
+ (args.catalogs || []).concat([SCHEMASTORE_CATALOG_URL]),
126
+ filename,
127
+ cache
128
+ ));
129
+ const schema = await getFromUrlOrFile(schemaLocation, cache);
114
130
  console.log(
115
131
  `Validating ${filename} against schema from ${schemaLocation} ...`
116
132
  );
117
133
 
118
- const resolver = function (url) {
119
- return cachedFetch(url, cache, ttl);
120
- };
121
- const valid = await validate(data, schema, resolver);
134
+ const valid = await validate(data, schema, cache);
122
135
  if (valid) {
123
136
  console.log(`✅ ${filename} is valid`);
124
137
  } else {
@@ -169,6 +182,14 @@ function parseArgs(argv) {
169
182
  describe:
170
183
  "Local path or URL of schema to validate file against. If not supplied, we will attempt to find an appropriate schema on schemastore.org using the filename",
171
184
  })
185
+ .option("catalogs", {
186
+ type: "string",
187
+ alias: "c",
188
+ array: true,
189
+ describe:
190
+ "Local path or URL of custom catalogs to use prior to schemastore.org",
191
+ })
192
+ .conflicts("schema", "catalogs")
172
193
  .option("ignore-errors", {
173
194
  type: "boolean",
174
195
  default: false,
@@ -183,4 +204,4 @@ function parseArgs(argv) {
183
204
  }).argv;
184
205
  }
185
206
 
186
- module.exports = { cli, parseArgs };
207
+ export { cli, parseArgs };
package/src/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- "use strict";
4
-
5
- const { cli, parseArgs } = require("./cli.js");
3
+ import { cli, parseArgs } from "./cli.js";
6
4
 
7
5
  (async () => {
8
6
  const exitCode = await cli(parseArgs(process.argv));
package/src/logging.js CHANGED
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  const origWarn = console.warn;
4
2
  const origInfo = console.info;
5
3
  const origDebug = console.debug;
@@ -19,4 +17,5 @@ function cleanup() {
19
17
  console.debug = origDebug;
20
18
  }
21
19
 
22
- module.exports = { cleanup, init };
20
+ const logging = { cleanup, init };
21
+ export default logging;