diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd1dfcf..c935c07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,8 @@ jobs: steps: - uses: actions/checkout@v2 + with: + submodules: 'true' - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d43f3f5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "conformance-profiles"] + path = conformance-profiles + url = https://github.com/openactive/conformance-profiles.git diff --git a/conformance-profiles b/conformance-profiles new file mode 160000 index 0000000..a966d06 --- /dev/null +++ b/conformance-profiles @@ -0,0 +1 @@ +Subproject commit a966d067c5bf0e4563c243925a6c50e5fc91dd18 diff --git a/package-lock.json b/package-lock.json index 6423295..aae032d 100755 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,17 @@ "negotiator": "0.6.2" } }, + "ajv": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -868,6 +879,16 @@ } } }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -1330,6 +1351,11 @@ "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", "dev": true }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, "jsonpath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.0.2.tgz", @@ -1944,6 +1970,11 @@ "once": "^1.3.1" } }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, "pupa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", @@ -2445,6 +2476,14 @@ } } }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, "uritemplate": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/uritemplate/-/uritemplate-0.3.4.tgz", diff --git a/package.json b/package.json index 9ba5aee..61b2b09 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "homepage": "https://github.com/openactive/feed-normaliser#readme", "dependencies": { "@openactive/data-model-validator": "^2.0.25", + "ajv": "^6.12.3", "cheerio": "^1.0.0-rc.3", "express": "^4.17.1", "node-fetch": "^2.6.0", diff --git a/src/lib/util-data-profile.js b/src/lib/util-data-profile.js new file mode 100644 index 0000000..b62237f --- /dev/null +++ b/src/lib/util-data-profile.js @@ -0,0 +1,63 @@ +import Ajv from 'ajv'; +import fs from 'fs'; +import Utils from "./utils.js"; + +async function apply_data_profile(data, data_profile_name) { + + // ---------------------- Load Data Profile Info + if (!(await does_data_profile_exist(data_profile_name))) { + return { + "done": false, + "error": "Data Profile Does Not Exist" + }; + } + + // ---------------------- Load Type Info + var data_type = data['@type']; + if (!(await does_data_profile_schema_exist(data_profile_name, data_type))) { + return { + "done": false, + "error": "Type Does Not Exist " + data_type + }; + } + + // ---------------------- Process + var validate = await getAjvValidate(data_profile_name, data_type); + var valid = await validate(data); + + return { + "done": true, + "results": validate.errors, + }; + +}; + +async function getAjvValidate(data_profile_name, type) { + // we will assume data_profile_name and type exists and you've already checked that with other functions + // TODO every time it's called, we load a lot from disk. Can we cache result anywhere in a "Thread"-safe manner? + const ajv = new Ajv({allErrors: true}); + // TODO load from an absolute path, not a relative one + const file_names = fs.readdirSync('conformance-profiles/'+data_profile_name); + for(var file_name of file_names) { + if (file_name != 'data-profile.json' && file_name.substring(file_name.length - 5) == '.json' && file_name != type+'.json') { + const json_schema_string = await fs.promises.readFile('conformance-profiles/'+data_profile_name+'/'+file_name, "utf8"); + const json_schema = await JSON.parse(json_schema_string); + await ajv.addSchema(json_schema); + } + }; + const type_json_schema_string = await fs.promises.readFile('conformance-profiles/'+data_profile_name+'/'+type+'.json', "utf8"); + const type_json_schema = await JSON.parse(type_json_schema_string); + return await ajv.compile(type_json_schema); +} + +async function does_data_profile_exist(data_profile_name) { + // TODO load from an absolute path, not a relative one + return await fs.existsSync('conformance-profiles/'+data_profile_name+'/data-profile.json'); +} + +async function does_data_profile_schema_exist(data_profile_name, schema_name) { + // TODO load from an absolute path, not a relative one + return await fs.existsSync('conformance-profiles/'+data_profile_name+'/'+schema_name+'.json'); +} + +export default apply_data_profile; diff --git a/test/test-util-data-profile.js b/test/test-util-data-profile.js new file mode 100644 index 0000000..2f092c3 --- /dev/null +++ b/test/test-util-data-profile.js @@ -0,0 +1,43 @@ +import assert from 'assert'; +import apply_data_profile from '../src/lib/util-data-profile.js'; + + +describe('util-data-profile', function() { + + it('Data Profile Name Does Not Exist', async function() { + + let results = await apply_data_profile({'@type': 'Event'}, 'kangaroos'); + + assert.equal(results.done,false); + assert.equal(results.error,"Data Profile Does Not Exist"); + + }); + + it('Test', function(done) { + + let results_promise = apply_data_profile( + { + '@type': 'Event', + 'location': { + '@type': 'Place' + } + }, + 'core' + ); + results_promise.then((results)=> { + + assert.equal(results.done, true); + assert.equal(results.results.length,22); + // Just test a couple + assert.deepEqual(results.results[0].message,"should have required property 'activity'"); + assert.deepEqual(results.results[0].dataPath,""); + assert.deepEqual(results.results[6].message,"should have required property 'address'"); + assert.deepEqual(results.results[6].dataPath,".location"); + + }) + .then(() => done(), done) + .catch((error) => { + done(error); + }); + }); +});