Skip to content

Commit fa53441

Browse files
committed
Implement custom validation + doc improvements
1 parent 50c66e3 commit fa53441

File tree

8 files changed

+118
-6
lines changed

8 files changed

+118
-6
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Further options to add to the commands above:
4444
- 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`)
4545
- To lint local JSON files: `--lint` (add `--verbose` to get a diff with the changes required)
4646
- To format / pretty-print local JSON files: `--format` (Attention: this will override the source files without warning!)
47+
- 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.
4748

4849
**Note on API support:** Validating lists of STAC items/collections (i.e. `GET /collections` and `GET /collections/:id/items`) is partially supported.
4950
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.
7475
"lint": true,
7576
"format": false,
7677
"strict": true,
77-
"all": false
78+
"all": false,
79+
"custom": null
7880
}
7981
```
8082

custom.example.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const BaseValidator = require('./src/baseValidator.js');
2+
const assert = require('assert');
3+
4+
class CustomValidator extends BaseValidator {
5+
6+
/**
7+
* Any custom validation routines you want to run.
8+
*
9+
* You can either create a list of errors or just throw on the first error (e.g. using `assert` functions).
10+
*
11+
* @param {STAC} data
12+
* @param {import('.').Report} report
13+
* @param {import('.').Config} config
14+
* @throws {Error}
15+
* @returns {Array.<Error>}
16+
*/
17+
async afterValidation(data, report, config) {
18+
// assert.deepEqual(data.example, [1,2,3]);
19+
return [];
20+
}
21+
22+
}
23+
24+
module.exports = CustomValidator;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"fs-extra": "^10.0.0",
3333
"jest-diff": "^29.0.1",
3434
"klaw": "^4.0.1",
35+
"stac-js": "^0.0.8",
3536
"yargs": "^17.7.2"
3637
},
3738
"devDependencies": {

src/baseValidator.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const { STAC } = import('stac-js');
2+
3+
class BaseValidator {
4+
5+
/**
6+
*
7+
*/
8+
constructor() {
9+
}
10+
11+
/**
12+
* Any preprocessing work you want to do on the data.
13+
*
14+
* @param {Object} data
15+
* @param {import('.').Report} report
16+
* @param {import('.').Config} config
17+
* @returns {Object}
18+
*/
19+
async afterLoading(data, report, config) {
20+
return data;
21+
}
22+
23+
/**
24+
* Any custom validation routines you want to run.
25+
*
26+
* You can either create a list of errors or just throw on the first error (e.g. using `assert` functions).
27+
*
28+
* @param {STAC} data
29+
* @param {import('.').Report} report
30+
* @param {import('.').Config} config
31+
* @throws {Error}
32+
* @returns {Array.<Error>}
33+
*/
34+
async afterValidation(data, report, config) {
35+
return [];
36+
}
37+
38+
}
39+
40+
module.exports = BaseValidator;

src/cli.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const fs = require('fs-extra');
2+
const path = require('path');
23
const { version } = require('../package.json');
34
const ConfigSource = require('./config.js');
45
const validate = require('../src/index.js');
@@ -71,6 +72,12 @@ async function run() {
7172
}
7273
}
7374

75+
if (config.custom) {
76+
const absPath = path.resolve(process.cwd(), config.custom);
77+
const validator = require(absPath);
78+
config.customValidator = new validator();
79+
}
80+
7481
// Finally run validation
7582
const result = await validate(data, config);
7683

src/config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ function fromCLI() {
3737
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',
3838
coerce: strArrayToObject
3939
})
40+
.option('custom', {
41+
type: 'string',
42+
default: null,
43+
description: 'Load a custom validation routine from a JavaScript file.'
44+
})
4045
.option('ignoreCerts', {
4146
type: 'boolean',
4247
default: false,

src/index.js

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const versions = require('compare-versions');
22

33
const { createAjv, isUrl, loadSchemaFromUri, normalizePath, isObject } = require('./utils');
44
const defaultLoader = require('./loader/default');
5+
const BaseValidator = require('./baseValidator');
56

67
/**
78
* @typedef Config
@@ -10,6 +11,7 @@ const defaultLoader = require('./loader/default');
1011
* @property {string|null} [schemas=null] Validate against schemas in a local or remote STAC folder.
1112
* @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.
1213
* @property {boolean} [strict=false] Enable strict mode in validation for schemas and numbers (as defined by ajv for options `strictSchema`, `strictNumbers` and `strictTuples
14+
* @property {BaseValidator} [customValidator=null] A validator with custom rules.
1315
*/
1416

1517
/**
@@ -20,12 +22,19 @@ const defaultLoader = require('./loader/default');
2022
* @property {string} version
2123
* @property {boolean} valid
2224
* @property {Array.<string>} messages
23-
* @property {Array.<*>} results
2425
* @property {Array.<Report>} children
25-
* @property {Extensions.<Object>} extensions
26+
* @property {Results} results
2627
* @property {boolean} apiList
2728
*/
2829

30+
/**
31+
* @typedef Results
32+
* @type {Object}
33+
* @property {OArray.<Error>} core
34+
* @property {Object.<string, Array.<Error>>} extensions
35+
* @property {Array.<Error>} custom
36+
*/
37+
2938
/**
3039
* @returns {Report}
3140
*/
@@ -40,7 +49,8 @@ function createReport() {
4049
children: [],
4150
results: {
4251
core: [],
43-
extensions: {}
52+
extensions: {},
53+
custom: []
4454
},
4555
apiList: false
4656
};
@@ -129,6 +139,10 @@ async function validateOne(source, config, report = null) {
129139
report.version = data.stac_version;
130140
report.type = data.type;
131141

142+
if (config.customValidator) {
143+
data = await config.customValidator.afterLoading(data, report, config);
144+
}
145+
132146
if (typeof config.lintFn === 'function') {
133147
report = await config.lintFn(source, report, config);
134148
}
@@ -181,6 +195,22 @@ async function validateOne(source, config, report = null) {
181195
await validateSchema('extensions', schema, data, report, config);
182196
}
183197

198+
if (config.customValidator) {
199+
const { default: create } = await import('stac-js');
200+
const stac = create(data, false, false);
201+
try {
202+
report.results.custom = await config.customValidator.afterValidation(stac, report, config);
203+
} catch (error) {
204+
report.results.custom = [
205+
error
206+
];
207+
} finally {
208+
if (report.results.custom.length > 0) {
209+
report.valid = false;
210+
}
211+
}
212+
}
213+
184214
return report;
185215
}
186216

src/nodeUtils.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const klaw = require('klaw');
22
const fs = require('fs-extra');
33
const path = require('path');
44

5-
const { isUrl } = require('./utils');
5+
const { isUrl, isObject } = require('./utils');
66

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

@@ -109,6 +109,9 @@ function printReport(report, config) {
109109
console.info("Extensions: None");
110110
}
111111
}
112+
if (config.custom) {
113+
printAjvValidationResult(report.results.custom, 'Custom', report.valid, config);
114+
}
112115
}
113116

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

173176
function makeAjvErrorMessage(error) {
174177
let message = error.message;
175-
if (Object.keys(error.params).length > 0) {
178+
if (isObject(error.params) && Object.keys(error.params).length > 0) {
176179
let params = Object.entries(error.params)
177180
.map(([key, value]) => {
178181
let label = key.replace(/([^A-Z]+)([A-Z])/g, "$1 $2").toLowerCase();

0 commit comments

Comments
 (0)