diff --git a/README.md b/README.md index 84b8f57..223bcd8 100644 --- a/README.md +++ b/README.md @@ -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`). @@ -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 } ``` diff --git a/custom.example.js b/custom.example.js new file mode 100644 index 0000000..426da3d --- /dev/null +++ b/custom.example.js @@ -0,0 +1,24 @@ +const BaseValidator = require('./src/baseValidator.js'); +const assert = require('assert'); + +class CustomValidator extends BaseValidator { + + /** + * Any custom validation routines you want to run. + * + * You can either create a list of errors or just throw on the first error (e.g. using `assert` functions). + * + * @param {STAC} data + * @param {import('.').Report} report + * @param {import('.').Config} config + * @throws {Error} + * @returns {Array.} + */ + async afterValidation(data, report, config) { + // assert.deepEqual(data.example, [1,2,3]); + return []; + } + +} + +module.exports = CustomValidator; diff --git a/package.json b/package.json index 135c03d..884f251 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "fs-extra": "^10.0.0", "jest-diff": "^29.0.1", "klaw": "^4.0.1", + "stac-js": "^0.0.8", "yargs": "^17.7.2" }, "devDependencies": { diff --git a/src/baseValidator.js b/src/baseValidator.js new file mode 100644 index 0000000..804baad --- /dev/null +++ b/src/baseValidator.js @@ -0,0 +1,40 @@ +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 or just throw on the first error (e.g. using `assert` functions). + * + * @param {STAC} data + * @param {import('.').Report} report + * @param {import('.').Config} config + * @throws {Error} + * @returns {Array.} + */ + async afterValidation(data, report, config) { + return []; + } + +} + +module.exports = BaseValidator; diff --git a/src/cli.js b/src/cli.js index 3db0c2a..5e16b26 100644 --- a/src/cli.js +++ b/src/cli.js @@ -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'); @@ -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); diff --git a/src/config.js b/src/config.js index c6c997d..9a0fafa 100644 --- a/src/config.js +++ b/src/config.js @@ -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, diff --git a/src/index.js b/src/index.js index 3d2e18c..b8f82d3 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ const versions = require('compare-versions'); const { createAjv, isUrl, loadSchemaFromUri, normalizePath, isObject } = require('./utils'); const defaultLoader = require('./loader/default'); +const BaseValidator = require('./baseValidator'); /** * @typedef Config @@ -10,6 +11,7 @@ const defaultLoader = require('./loader/default'); * @property {string|null} [schemas=null] Validate against schemas in a local or remote STAC folder. * @property {Object.} [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. */ /** @@ -20,12 +22,19 @@ const defaultLoader = require('./loader/default'); * @property {string} version * @property {boolean} valid * @property {Array.} messages - * @property {Array.<*>} results * @property {Array.} children - * @property {Extensions.} extensions + * @property {Results} results * @property {boolean} apiList */ +/** + * @typedef Results + * @type {Object} + * @property {OArray.} core + * @property {Object.>} extensions + * @property {Array.} custom + */ + /** * @returns {Report} */ @@ -40,7 +49,8 @@ function createReport() { children: [], results: { core: [], - extensions: {} + extensions: {}, + custom: [] }, apiList: false }; @@ -129,6 +139,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); } @@ -181,6 +195,22 @@ 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 { + report.results.custom = await config.customValidator.afterValidation(stac, report, config); + } catch (error) { + report.results.custom = [ + error + ]; + } finally { + if (report.results.custom.length > 0) { + report.valid = false; + } + } + } + return report; } diff --git a/src/nodeUtils.js b/src/nodeUtils.js index 040edab..dc866cf 100644 --- a/src/nodeUtils.js +++ b/src/nodeUtils.js @@ -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']; @@ -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)); @@ -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();