Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
Command line interface for [ajv](https://github.com/epoberezkin/ajv), one of the [fastest json schema validators](https://github.com/ebdrup/json-schema-benchmark).
Supports [JSON](http://json.org/), [JSON5](http://json5.org/), and [YAML](http://yaml.org/).

[![build](https://github.com/ajv-validator/ajv-cli/workflows/build/badge.svg)](https://github.com/ajv-validator/ajv-cli/actions?query=workflow%3Abuild)
[![npm](https://img.shields.io/npm/v/ajv-cli.svg)](https://www.npmjs.com/package/ajv-cli)
[![coverage](https://coveralls.io/repos/github/ajv-validator/ajv-cli/badge.svg?branch=master)](https://coveralls.io/github/ajv-validator/ajv-cli?branch=master)
[![gitter](https://img.shields.io/gitter/room/ajv-validator/ajv.svg)](https://gitter.im/ajv-validator/ajv)
[![build](https://github.com/ContinuousSecurityTooling/ajv-cli/actions/workflows/build.yml/badge.svg)](https://github.com/ContinuousSecurityTooling/ajv-cli/actions/workflows/build.yml)
[![NPM Version](https://img.shields.io/npm/v/:packageName)](https://www.npmjs.com/package/@continuoussecuritytooling/ajv-cli)
[![Coverage Status](https://coveralls.io/repos/github/ContinuousSecurityTooling/ajv-cli/badge.svg?branch=develop)](https://coveralls.io/github/ContinuousSecurityTooling/ajv-cli?branch=develop)

- [ajv-cli](#ajv-cli)
- [Installation](#installation)
Expand Down Expand Up @@ -153,6 +152,7 @@ For example, you can use `-c ajv-keywords` to add all keywords from [ajv-keyword

- `js` (default): JavaScript object
- `json`: JSON with indentation and line-breaks
- `code-climate` emits a JSON array of CodeClimate issues to **stdout** (for easy pipe/redirect to a file). Stderr still receives the `<file> invalid` message.
- `line`: JSON without indentation/line-breaks (for easy parsing)
- `text`: human readable error messages with data paths

Expand Down
44 changes: 42 additions & 2 deletions src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ import type {ParsedArgs} from "minimist"
import {compile, getFiles, openFile, logJSON} from "./util"
import getAjv from "./ajv"
import * as jsonPatch from "fast-json-patch"
import {createHash} from "crypto"
import type {ErrorObject} from "ajv"

// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
// https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
interface CodeClimateIssue {
description: string
check_name: string
fingerprint: string
severity: "info" | "minor" | "major" | "critical" | "blocker"
location: {
path: string
lines: {begin: number}
}
}

const cmd: Command = {
execute,
Expand All @@ -18,7 +33,7 @@ const cmd: Command = {
r: {$ref: "#/$defs/stringOrArray"},
m: {$ref: "#/$defs/stringOrArray"},
c: {$ref: "#/$defs/stringOrArray"},
errors: {enum: ["json", "line", "text", "js", "no"]},
errors: {enum: ["json", "line", "text", "js", "no", "code-climate"]},
changes: {enum: [true, "json", "line", "js"]},
spec: {enum: ["draft7", "draft2019", "draft2020", "jtd"]},
},
Expand All @@ -28,6 +43,27 @@ const cmd: Command = {

export default cmd

function formatCodeClimate(file: string, errors: ErrorObject[]): string {
const issues: CodeClimateIssue[] = errors.map((err) => {
const instancePath = err.instancePath || "/"
const message = err.message || "validation error"
const fingerprint = createHash("sha1")
.update(`${file}\0${instancePath}\0${message}`)
.digest("hex")
return {
description: `[schema] #${instancePath} ${message}`,
check_name: "json-schema",
fingerprint,
severity: "major",
location: {
path: file,
lines: {begin: 1},
},
}
})
return JSON.stringify(issues, null, " ")
}

function execute(argv: ParsedArgs): boolean {
const ajv = getAjv(argv)
const validate = compile(ajv, argv.s)
Expand All @@ -54,7 +90,11 @@ function execute(argv: ParsedArgs): boolean {
}
} else {
console.error(file, "invalid")
console.error(logJSON(argv.errors, validate.errors, ajv))
if (argv.errors === "code-climate") {
console.log(formatCodeClimate(file, validate.errors as ErrorObject[]))
} else {
console.error(logJSON(argv.errors, validate.errors, ajv))
}
}
return validData
}
Expand Down
62 changes: 62 additions & 0 deletions test/validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,68 @@ describe("validate", function () {
})
})

describe('option "errors" code-climate', () => {
it("should output valid CodeClimate JSON for invalid data", (done) => {
cli(
"-s test/schema.json -d test/invalid_data.json --errors=code-climate",
(error, stdout, stderr) => {
assert(error instanceof Error)
assert(stderr.includes("invalid"))
const issues = JSON.parse(stdout)
assert(Array.isArray(issues))
assert(issues.length > 0)
const issue = issues[0]
assert.strictEqual(typeof issue.description, "string")
assert.strictEqual(issue.check_name, "json-schema")
assert.strictEqual(typeof issue.fingerprint, "string")
assert.strictEqual(issue.fingerprint.length, 40)
assert.strictEqual(issue.severity, "major")
assert.strictEqual(typeof issue.location.path, "string")
assert.strictEqual(typeof issue.location.lines.begin, "number")
done()
}
)
})

it("should produce no issues for valid data", (done) => {
cli(
"-s test/schema.json -d test/valid_data.json --errors=code-climate",
(error, stdout, stderr) => {
assert.strictEqual(error, null)
assert(stdout.includes("valid"))
assert.strictEqual(stderr, "")
done()
}
)
})

it("should include the file path in the location", (done) => {
cli(
"-s test/schema.json -d test/invalid_data.json --errors=code-climate",
(error, stdout, _stderr) => {
assert(error instanceof Error)
const issues = JSON.parse(stdout)
assert(issues[0].location.path.includes("invalid_data"))
done()
}
)
})

it("should produce unique fingerprints per error", (done) => {
cli(
"-s test/schema.json -d test/invalid_data.json --errors=code-climate --all-errors",
(error, stdout, _stderr) => {
assert(error instanceof Error)
const issues = JSON.parse(stdout)
const fingerprints = issues.map((i: {fingerprint: string}) => i.fingerprint)
const unique = new Set(fingerprints)
assert.strictEqual(fingerprints.length, unique.size)
done()
}
)
})
})

describe('option "changes"', () => {
it("should log changes in the object after validation", (done) => {
cli(
Expand Down
Loading