Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

machine readable output #212

Merged
merged 7 commits into from
Jun 11, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions src/ajv.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import Ajv2019 from "ajv/dist/2019.js";
import Ajv2020 from "ajv/dist/2020.js";
import addFormats from "ajv-formats";

function _ajvFactory(schema, cache) {
function _ajvFactory(schema, strictMode, cache) {
const resolver = (url) => cache.fetch(url);
const opts = { allErrors: true, loadSchema: resolver, strict: "log" };
const opts = { allErrors: true, loadSchema: resolver, strict: strictMode };

if (
typeof schema["$schema"] === "string" ||
Expand Down Expand Up @@ -53,17 +53,12 @@ function _ajvFactory(schema, cache) {
*/
}

async function validate(data, schema, cache) {
const ajv = _ajvFactory(schema, cache);
async function validate(data, schema, strictMode, cache) {
const ajv = _ajvFactory(schema, strictMode, cache);
addFormats(ajv);
const validateFn = await ajv.compileAsync(schema);
const valid = validateFn(data);
if (!valid) {
console.log("\nErrors:");
console.log(validateFn.errors);
console.log("");
}
return valid;
return { valid, errors: validateFn.errors ? validateFn.errors : [] };
}

export { _ajvFactory, validate };
14 changes: 9 additions & 5 deletions src/ajv.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ const expect = chai.expect;

describe("_ajvFactory", function () {
describe("schema drafts compatibility", function () {
const messages = {};
const testCache = new Cache(flatCache.load(testCacheName), 3000);
beforeEach(() => setUp(messages));
beforeEach(() => setUp());
afterEach(() => tearDown());

it("should support draft-04", function () {
const ajv = _ajvFactory(
{ $schema: "http://json-schema.org/draft-04/schema#" },
false,
testCache
);
expect(ajv.schemas).to.have.own.property(
Expand All @@ -25,6 +25,7 @@ describe("_ajvFactory", function () {
it("should support draft-06", function () {
const ajv = _ajvFactory(
{ $schema: "http://json-schema.org/draft-06/schema#" },
false,
testCache
);
expect(ajv.schemas).to.have.own.property(
Expand All @@ -35,6 +36,7 @@ describe("_ajvFactory", function () {
it("should support draft-07", function () {
const ajv = _ajvFactory(
{ $schema: "http://json-schema.org/draft-07/schema#" },
false,
testCache
);
expect(ajv.schemas).to.have.own.property(
Expand All @@ -45,6 +47,7 @@ describe("_ajvFactory", function () {
it("should support draft-2019-09", function () {
const ajv = _ajvFactory(
{ $schema: "https://json-schema.org/draft/2019-09/schema" },
false,
testCache
);
expect(ajv.schemas).to.have.own.property(
Expand All @@ -55,6 +58,7 @@ describe("_ajvFactory", function () {
it("should support draft-2020-12", function () {
const ajv = _ajvFactory(
{ $schema: "https://json-schema.org/draft/2020-12/schema" },
false,
testCache
);
expect(ajv.schemas).to.have.own.property(
Expand All @@ -63,7 +67,7 @@ describe("_ajvFactory", function () {
});

it("should fall back to draft-06/draft-07 mode if $schema key is missing", function () {
const ajv = _ajvFactory({}, testCache);
const ajv = _ajvFactory({}, false, testCache);
expect(ajv.schemas).to.have.own.property(
"http://json-schema.org/draft-06/schema"
);
Expand All @@ -73,7 +77,7 @@ describe("_ajvFactory", function () {
});

it("should fall back to draft-06/draft-07 mode if $schema key is invalid (str)", function () {
const ajv = _ajvFactory({ $schema: "foobar" }, testCache);
const ajv = _ajvFactory({ $schema: "foobar" }, false, testCache);
expect(ajv.schemas).to.have.own.property(
"http://json-schema.org/draft-06/schema"
);
Expand All @@ -83,7 +87,7 @@ describe("_ajvFactory", function () {
});

it("should fall back to draft-06/draft-07 mode if $schema key is invalid (not str)", function () {
const ajv = _ajvFactory({ $schema: true }, testCache);
const ajv = _ajvFactory({ $schema: true }, false, testCache);
expect(ajv.schemas).to.have.own.property(
"http://json-schema.org/draft-06/schema"
);
Expand Down
10 changes: 5 additions & 5 deletions src/cache.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import got from "got";
import logging from "./logging.js";
import logger from "./logger.js";

class Cache {
constructor(flatCache, ttl) {
Expand All @@ -13,10 +13,10 @@ class Cache {
Object.entries(this.cache.all()).forEach(
function ([url, cachedResponse]) {
if (!("timestamp" in cachedResponse) || !("body" in cachedResponse)) {
logging.debug(`Cache error: deleting malformed response`);
logger.debug(`Cache error: deleting malformed response`);
this.cache.removeKey(url);
} else if (Date.now() > cachedResponse.timestamp + this.ttl) {
logging.debug(`Cache stale: deleting cached response from ${url}`);
logger.debug(`Cache stale: deleting cached response from ${url}`);
this.cache.removeKey(url);
}
this.cache.save(true);
Expand Down Expand Up @@ -53,12 +53,12 @@ class Cache {
this.expire();
const cachedResponse = this.cache.getKey(url);
if (cachedResponse !== undefined) {
logging.debug(`Cache hit: using cached response from ${url}`);
logger.debug(`Cache hit: using cached response from ${url}`);
return cachedResponse.body;
}

try {
logging.debug(`Cache miss: calling ${url}`);
logger.debug(`Cache miss: calling ${url}`);
const resp = await got(url);
const parsedBody = JSON.parse(resp.body);
if (this.ttl > 0) {
Expand Down
9 changes: 3 additions & 6 deletions src/cache.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ const expect = chai.expect;

describe("Cache", function () {
describe("fetch function", function () {
const messages = {};
const testCache = new Cache(flatCache.load(testCacheName), 3000);
beforeEach(() => setUp(messages));
beforeEach(() => setUp());
afterEach(() => tearDown());

it("should use cached response if valid", async function () {
Expand Down Expand Up @@ -41,9 +40,8 @@ describe("Cache", function () {
});

describe("cyclic detection", function () {
const messages = {};
const testCache = new Cache(flatCache.load(testCacheName), 3000);
beforeEach(() => setUp(messages));
beforeEach(() => setUp());
afterEach(() => tearDown());

it("throws if callLimit is exceeded", async function () {
Expand All @@ -68,9 +66,8 @@ describe("Cache", function () {
});

describe("expire function", function () {
const messages = {};
const testCache = new Cache(flatCache.load(testCacheName), 3000);
beforeEach(() => setUp(messages));
beforeEach(() => setUp());
afterEach(() => tearDown());

it("should delete expired and malformed cache objects", async function () {
Expand Down
16 changes: 11 additions & 5 deletions src/catalogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import minimatch from "minimatch";
import path from "path";
import { validate } from "./ajv.js";
import { getFromUrlOrFile } from "./io.js";
import logging from "./logging.js";
import logger from "./logger.js";

const SCHEMASTORE_CATALOG_URL =
"https://www.schemastore.org/api/json/catalog.json";
Expand Down Expand Up @@ -52,17 +52,23 @@ async function getMatchForFilename(catalogs, filename, cache) {
);

// Validate the catalog
const valid = await validate(catalog, catalogSchema, cache);
const strictMode = false;
const { valid } = await validate(
catalog,
catalogSchema,
strictMode,
cache
);
if (!valid || catalog.schemas === undefined) {
throw new Error(`Malformed catalog at ${catalogLocation}`);
}
}

const { schemas } = catalog;
const matches = getSchemaMatchesForFilename(schemas, filename);
logging.debug(`Searching for schema in ${catalogLocation} ...`);
logger.debug(`Searching for schema in ${catalogLocation} ...`);
if (matches.length === 1) {
logging.info(`Found schema in ${catalogLocation} ...`);
logger.info(`Found schema in ${catalogLocation} ...`);
return coerceMatch(matches[0]); // Match found. We're done.
}
if (matches.length === 0 && i < catalogs.length - 1) {
Expand All @@ -81,7 +87,7 @@ async function getMatchForFilename(catalogs, filename, cache) {
return outStr;
})
.join("\n");
logging.info(
logger.info(
`Found multiple possible schemas for ${filename}. Possible matches:\n${matchesLog}`
);
}
Expand Down
75 changes: 53 additions & 22 deletions src/cli.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Ajv from "ajv";
import flatCache from "flat-cache";
import fs from "fs";
import os from "os";
Expand All @@ -8,7 +9,7 @@ import { getCatalogs, getMatchForFilename } from "./catalogs.js";
import { getConfig } from "./config.js";
import { getFiles } from "./glob.js";
import { getFromUrlOrFile } from "./io.js";
import logging from "./logging.js";
import logger from "./logger.js";
import { parseFile } from "./parser.js";

const EXIT = {
Expand All @@ -33,15 +34,23 @@ function getFlatCache() {
}

async function validateFile(filename, config, cache) {
logging.info(`Processing ${filename}`);
logger.info(`Processing ${filename}`);
let result = {
fileLocation: filename,
schemaLocation: null,
valid: null,
errors: [],
code: null,
};
try {
const catalogs = getCatalogs(config);
const catalogMatch = config.schema
? {}
: await getMatchForFilename(catalogs, filename, cache);
const schemaLocation = config.schema || catalogMatch.location;
result.schemaLocation = schemaLocation;
const schema = await getFromUrlOrFile(schemaLocation, cache);
logging.info(
logger.info(
`Validating ${filename} against schema from ${schemaLocation} ...`
);

Expand All @@ -50,25 +59,27 @@ async function validateFile(filename, config, cache) {
catalogMatch.parser ? `.${catalogMatch.parser}` : path.extname(filename)
);

const valid = await validate(data, schema, cache);
const strictMode = config.verbose >= 2 ? "log" : false;
const { valid, errors } = await validate(data, schema, strictMode, cache);
result.valid = valid;
result.errors = errors;
if (valid) {
logging.success(`${filename} is valid\n`);
logger.success(`${filename} is valid\n`);
} else {
logging.error(`${filename} is invalid\n`);
logger.error(`${filename} is invalid\n`);
}

if (valid) {
return EXIT.VALID;
}
return EXIT.INVALID;
result.code = valid ? EXIT.VALID : EXIT.INVALID;
return result;
} catch (e) {
logging.error(`${e.message}\n`);
return EXIT.ERROR;
logger.error(`${e.message}\n`);
result.code = EXIT.ERROR;
return result;
}
}

function mergeResults(results, ignoreErrors) {
const codes = Object.values(results);
function resultsToStatusCode(results, ignoreErrors) {
const codes = Object.values(results).map((result) => result.code);
if (codes.includes(EXIT.INVALID)) {
return EXIT.INVALID;
}
Expand All @@ -78,13 +89,24 @@ function mergeResults(results, ignoreErrors) {
return EXIT.VALID;
}

function logErrors(filename, errors) {
const ajv = new Ajv();
logger.log(
ajv.errorsText(errors, {
separator: "\n",
dataVar: filename + "#",
})
);
logger.log("");
}

function Validator() {
return async function (config) {
let filenames = [];
for (const pattern of config.patterns) {
const matches = await getFiles(pattern);
if (matches.length === 0) {
logging.error(`Pattern '${pattern}' did not match any files`);
logger.error(`Pattern '${pattern}' did not match any files`);
return EXIT.NOT_FOUND;
}
filenames = filenames.concat(matches);
Expand All @@ -96,9 +118,20 @@ function Validator() {
const results = Object.fromEntries(filenames.map((key) => [key, null]));
for (const [filename] of Object.entries(results)) {
results[filename] = await validateFile(filename, config, cache);

if (!results[filename].valid && config.format === "text") {
logErrors(filename, results[filename].errors);
}
// else: silence is golden

cache.resetCounters();
}
return mergeResults(results, config.ignoreErrors);

if (config.format === "json") {
logger.log(JSON.stringify({ results }, null, 2));
chris48s marked this conversation as resolved.
Show resolved Hide resolved
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO:

if (config.format === "sarif") {
...

return resultsToStatusCode(results, config.ignoreErrors);
};
}

Expand All @@ -107,22 +140,20 @@ async function cli(config) {
try {
config = await getConfig(process.argv);
} catch (e) {
logging.error(e.message);
logger.error(e.message);
return EXIT.INVALID_CONFIG;
}
}

logging.init(config.verbose);
logging.debug(`Merged args/config: ${JSON.stringify(config, null, 2)}`);
logger.setVerbosity(config.verbose);
logger.debug(`Merged args/config: ${JSON.stringify(config, null, 2)}`);

try {
const validate = new Validator();
return await validate(config);
} catch (e) {
logging.error(e.message);
logger.error(e.message);
return EXIT.ERROR;
} finally {
logging.cleanup();
}
}

Expand Down
Loading