Skip to content

Commit

Permalink
Implement custom validation + doc improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
m-mohr committed Sep 7, 2023
1 parent 50c66e3 commit a74e9fe
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 6 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Further options to add to the commands above:
- Add `--strict` to enable strict mode in validation for schemas and numbers (as defined by [ajv](https://ajv.js.org/strict-mode.html) for options `strictSchema`, `strictNumbers` and `strictTuples`)
- To lint local JSON files: `--lint` (add `--verbose` to get a diff with the changes required)
- To format / pretty-print local JSON files: `--format` (Attention: this will override the source files without warning!)
- To run custom validation code: `--custom ./path/to/validation.js` - The validation.js needs to contain a class that implements the `BaseValidator` interface. See [custom.example.js](./custom.example.js) for an example.

**Note on API support:** Validating lists of STAC items/collections (i.e. `GET /collections` and `GET /collections/:id/items`) is partially supported.
It only checks the contained items/collections, but not the other parts of the response (e.g. `links`).
Expand Down Expand Up @@ -74,7 +75,8 @@ The schema map is an object instead of string separated with a `=` character.
"lint": true,
"format": false,
"strict": true,
"all": false
"all": false,
"custom": null
}
```

Expand Down
25 changes: 25 additions & 0 deletions custom.example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const BaseValidator = require('./src/baseValidator.js');

class CustomValidator extends BaseValidator {

/**
* Any custom validation routines you want to run.
*
* You can either create a list of errors using the test interface
* or just throw on the first error.
*
* @param {STAC} data
* @param {Test} test
* @param {import('.').Report} report
* @param {import('.').Config} config
* @throws {Error}
*/
async afterValidation(data, test, report, config) {
if (data.id === 'solid-earth') {
test.deepEqual(data.example, [1,2,3]);
}
}

}

module.exports = CustomValidator;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
"dependencies": {
"ajv": "^8.8.2",
"ajv-formats": "^2.1.1",
"assert": "^2.0.0",
"axios": "^1.1.3",
"compare-versions": "^6.1.0",
"fs-extra": "^10.0.0",
"jest-diff": "^29.0.1",
"klaw": "^4.0.1",
"stac-js": "^0.0.8",
"yargs": "^17.7.2"
},
"devDependencies": {
Expand Down
41 changes: 41 additions & 0 deletions src/baseValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const { STAC } = import('stac-js');

class BaseValidator {

/**
*
*/
constructor() {
}

/**
* Any preprocessing work you want to do on the data.
*
* @param {Object} data
* @param {import('.').Report} report
* @param {import('.').Config} config
* @returns {Object}
*/
async afterLoading(data, report, config) {
return data;
}

/**
* Any custom validation routines you want to run.
*
* You can either create a list of errors using the test interface
* or just throw on the first error.
*
* @param {STAC} data
* @param {Test} test
* @param {import('.').Report} report
* @param {import('.').Config} config
* @throws {Error}
*/
async afterValidation(data, test, report, config) {

}

}

module.exports = BaseValidator;
7 changes: 7 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const fs = require('fs-extra');
const path = require('path');
const { version } = require('../package.json');
const ConfigSource = require('./config.js');
const validate = require('../src/index.js');
Expand Down Expand Up @@ -71,6 +72,12 @@ async function run() {
}
}

if (config.custom) {
const absPath = path.resolve(process.cwd(), config.custom);
const validator = require(absPath);
config.customValidator = new validator();
}

// Finally run validation
const result = await validate(data, config);

Expand Down
5 changes: 5 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ function fromCLI() {
description: 'Validate against a specific local schema (e.g. an external extension). Provide the schema URI and the local path separated by an equal sign.\nExample: https://stac-extensions.github.io/foobar/v1.0.0/schema.json=./json-schema/schema.json',
coerce: strArrayToObject
})
.option('custom', {
type: 'string',
default: null,
description: 'Load a custom validation routine from a JavaScript file.'
})
.option('ignoreCerts', {
type: 'boolean',
default: false,
Expand Down
39 changes: 36 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const versions = require('compare-versions');

const { createAjv, isUrl, loadSchemaFromUri, normalizePath, isObject } = require('./utils');
const defaultLoader = require('./loader/default');
const BaseValidator = require('./baseValidator');
const Test = require('./test');

/**
* @typedef Config
Expand All @@ -10,6 +12,7 @@ const defaultLoader = require('./loader/default');
* @property {string|null} [schemas=null] Validate against schemas in a local or remote STAC folder.
* @property {Object.<string, string>} [schemaMap={}] Validate against a specific local schema (e.g. an external extension). Provide the schema URI as key and the local path as value.
* @property {boolean} [strict=false] Enable strict mode in validation for schemas and numbers (as defined by ajv for options `strictSchema`, `strictNumbers` and `strictTuples
* @property {BaseValidator} [customValidator=null] A validator with custom rules.
*/

/**
Expand All @@ -20,12 +23,19 @@ const defaultLoader = require('./loader/default');
* @property {string} version
* @property {boolean} valid
* @property {Array.<string>} messages
* @property {Array.<*>} results
* @property {Array.<Report>} children
* @property {Extensions.<Object>} extensions
* @property {Results} results
* @property {boolean} apiList
*/

/**
* @typedef Results
* @type {Object}
* @property {OArray.<Error>} core
* @property {Object.<string, Array.<Error>>} extensions
* @property {Array.<Error>} custom
*/

/**
* @returns {Report}
*/
Expand All @@ -40,7 +50,8 @@ function createReport() {
children: [],
results: {
core: [],
extensions: {}
extensions: {},
custom: []
},
apiList: false
};
Expand Down Expand Up @@ -129,6 +140,10 @@ async function validateOne(source, config, report = null) {
report.version = data.stac_version;
report.type = data.type;

if (config.customValidator) {
data = await config.customValidator.afterLoading(data, report, config);
}

if (typeof config.lintFn === 'function') {
report = await config.lintFn(source, report, config);
}
Expand Down Expand Up @@ -181,6 +196,24 @@ async function validateOne(source, config, report = null) {
await validateSchema('extensions', schema, data, report, config);
}

if (config.customValidator) {
const { default: create } = await import('stac-js');
const stac = create(data, false, false);
try {
const test = new Test();
await config.customValidator.afterValidation(stac, test, report, config);
report.results.custom = test.errors;
} catch (error) {
report.results.custom = [
error
];
} finally {
if (report.results.custom.length > 0) {
report.valid = false;
}
}
}

return report;
}

Expand Down
7 changes: 5 additions & 2 deletions src/nodeUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const klaw = require('klaw');
const fs = require('fs-extra');
const path = require('path');

const { isUrl } = require('./utils');
const { isUrl, isObject } = require('./utils');

const SCHEMA_CHOICE = ['anyOf', 'oneOf'];

Expand Down Expand Up @@ -109,6 +109,9 @@ function printReport(report, config) {
console.info("Extensions: None");
}
}
if (config.custom) {
printAjvValidationResult(report.results.custom, 'Custom', report.valid, config);
}
}

report.children.forEach(child => printReport(child, config));
Expand Down Expand Up @@ -172,7 +175,7 @@ function isSchemaChoice(schemaPath) {

function makeAjvErrorMessage(error) {
let message = error.message;
if (Object.keys(error.params).length > 0) {
if (isObject(error.params) && Object.keys(error.params).length > 0) {
let params = Object.entries(error.params)
.map(([key, value]) => {
let label = key.replace(/([^A-Z]+)([A-Z])/g, "$1 $2").toLowerCase();
Expand Down
155 changes: 155 additions & 0 deletions src/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
const assert = require('assert');

class Test {

constructor() {
this.errors = [];
}

truthy(...args) {
try {
assert(...args);
} catch (error) {
this.errors.push(error);
}
}

deepEqual(...args) {
try {
assert.deepEqual(...args);
} catch (error) {
this.errors.push(error);
}
}

deepStrictEqual(...args) {
try {
assert.deepStrictEqual(...args);
} catch (error) {
this.errors.push(error);
}
}

doesNotMatch(...args) {
try {
assert.doesNotMatch(...args);
} catch (error) {
this.errors.push(error);
}
}

async doesNotReject(...args) {
try {
await assert.doesNotReject(...args);
} catch (error) {
this.errors.push(error);
}
}

doesNotThrow(...args) {
try {
assert.doesNotThrow(...args);
} catch (error) {
this.errors.push(error);
}
}

equal(...args) {
try {
assert.equal(...args);
} catch (error) {
this.errors.push(error);
}
}

fail(...args) {
try {
assert.fail(...args);
} catch (error) {
this.errors.push(error);
}
}

ifError(...args) {
try {
assert.ifError(...args);
} catch (error) {
this.errors.push(error);
}
}

match(...args) {
try {
assert.match(...args);
} catch (error) {
this.errors.push(error);
}
}

notDeepEqual(...args) {
try {
assert.notDeepEqual(...args);
} catch (error) {
this.errors.push(error);
}
}

notDeepStrictEqual(...args) {
try {
assert.notDeepStrictEqual(...args);
} catch (error) {
this.errors.push(error);
}
}

notEqual(...args) {
try {
assert.notEqual(...args);
} catch (error) {
this.errors.push(error);
}
}

notStrictEqual(...args) {
try {
assert.notStrictEqual(...args);
} catch (error) {
this.errors.push(error);
}
}

ok(...args) {
try {
assert.ok(...args);
} catch (error) {
this.errors.push(error);
}
}

async rejects(...args) {
try {
await assert.rejects(...args);
} catch (error) {
this.errors.push(error);
}
}

strictEqual(...args) {
try {
assert.strictEqual(...args);
} catch (error) {
this.errors.push(error);
}
}

throws(...args) {
try {
assert.throws(...args);
} catch (error) {
this.errors.push(error);
}
}

}

module.exports = Test;

0 comments on commit a74e9fe

Please sign in to comment.