Commit 35c8c9ca authored by Karoly Erdos's avatar Karoly Erdos
Browse files

Use Elixir's JSON schema validator library

parent 087a7711
# JSON Schema Validator
# JSON Schema Validator service
[![Build Status](https://travis-ci.org/EMBL-EBI-SUBS/json-schema-validator.svg?branch=master)](https://travis-ci.org/EMBL-EBI-SUBS/json-schema-validator) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/7fbabc981e294249a9a0967965418058)](https://www.codacy.com/app/fpenim/json-schema-validator?utm_source=github.com&utm_medium=referral&utm_content=EMBL-EBI-SUBS/json-schema-validator&utm_campaign=Badge_Grade)
[![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest)
This repository contains a [JSON Schema](http://json-schema.org/) validator for the EMBL-EBI Submissions Project. This validator runs as a standalone node server that receives validation requests and gives back it's results.
This service uses [Elixir's JSON schema validator library](https://github.com/elixir-europe/json-schema-validator).
The validation is done using the [AJV](https://github.com/epoberezkin/ajv) library version ^6.0.0 that fully supports the JSON Schema **draft-07**.
Deployed for tests purposes on heroku: https://subs-json-schema-validator.herokuapp.com/validate
......@@ -103,6 +106,11 @@ The endpoint will expect the body to have the following structure:
}
```
Where the schema should be a valid json schema to validate the object against.
You also have to add this value to the header of the request:
```
Content-Type: application/json
```
**Example:**
Sending a POST request with the following body:
......@@ -178,68 +186,8 @@ HTTP status code `400`
```
## Custom keywords
The AJV library supports the implementation of custom json schema keywords to address validation scenarios that go beyond what json schema can handle.
This validator has two custom keywords implemented, `isChildTermOf` and `isValidTerm`.
### isChildTermOf
This custom keyword *evaluates if an ontology term is child of other*. This keyword is applied to a string (url) and **passes validation if the term is a child of the term defined in the schema**.
The keyword requires the **parent term** and the **ontology id**, both of which should exist in [OLS - Ontology Lookup Service](https://www.ebi.ac.uk/ols).
This keyword works by doing an asynchronous call to the [OLS API](https://www.ebi.ac.uk/ols/api/) that will respond with the required information to know if a given term is child of another.
Being an async validation step, whenever used in a schema, the schema must have the flag: `"$async": true` in its object root.
#### Usage
Schema:
```js
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$async": true,
"properties": {
"term": {
"type": "string",
"format": "uri",
"isChildTermOf": {
"parentTerm": "http://purl.obolibrary.org/obo/PATO_0000047",
"ontologyId": "pato"
}
}
}
}
```
JSON object:
```js
{
"term": "http://purl.obolibrary.org/obo/PATO_0000383"
}
```
### isValidTerm
This custom keyword *evaluates if a given ontology term url exists in OLS* ([Ontology Lookup Service](https://www.ebi.ac.uk/ols)). It is applied to a string (url) and **passes validation if the term exists in OLS**. It can be aplied to any string defined in the schema.
This keyword works by doing an asynchronous call to the [OLS API](https://www.ebi.ac.uk/ols/api/) that will respond with the required information to determine if the term exists in OLS or not.
Being an async validation step, whenever used in a schema, the schema must have the flag: `"$async": true` in its object root.
#### Usage
Schema:
```js
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$async": true,
"properties": {
"url": {
"type": "string",
"format": "uri",
"isValidTerm": true
}
}
}
```
JSON object:
```js
{
"url": "http://purl.obolibrary.org/obo/PATO_0000383"
}
```
The list of implemented custom keywords could be found in the Elixir's JSON Schema Validator library's documentation: [https://github.com/elixir-europe/json-schema-validator/blob/master/README.md#custom-keywords].
## License
For more details about licensing see the [LICENSE](LICENSE.md).
This diff is collapsed.
{
"name": "json-schema-validator",
"version": "1.6.0",
"version": "1.7.0",
"description": "A nodejs JSON schema validator.",
"main": "src/server.js",
"repository": "https://github.com/EMBL-EBI-SUBS/json-schema-validator.git",
......@@ -19,22 +19,18 @@
"schema",
"validator"
],
"author": "EMBL-EBI-SUBS, fpenim",
"author": "EMBL-EBI-SUBS, fpenim, ke4, haseeb-gh",
"license": "Apache-2.0",
"dependencies": {
"ajv": "^6.5.5",
"body-parser": "^1.18.3",
"express": "^4.16.4",
"elixir-jsonschema-validator": "^1.6.1",
"express": "^4.17.1",
"npid": "^0.4.0",
"request": "^2.88.0",
"update": "^0.7.4",
"winston": "^3.1.0",
"winston-daily-rotate-file": "^3.4.1",
"yargs": "^11.1.0",
"event-stream": "3.3.4"
"request": "^2.88.2",
"winston": "^3.2.1",
"winston-daily-rotate-file": "^3.10.0"
},
"devDependencies": {
"jest": "^23.6.0",
"nodemon": "^1.18.6"
"nodemon": "^1.19.4"
}
}
var Ajv = require("ajv");
var request = require("request");
const logger = require("../winston");
const CustomAjvError = require("../model/custom-ajv-error");
module.exports = function isChildTermOf(ajv) {
function findChildTerm(schema, data) {
return new Promise((resolve, reject) => {
const parentTerm = schema.parentTerm;
const ontologyId = schema.ontologyId;
let errors = [];
if(parentTerm && ontologyId) {
const olsSearchUrl = "https://www.ebi.ac.uk/ols/api/search?q=";
const termUri = encodeURIComponent(data);
const url = olsSearchUrl + termUri
+ "&exact=true&groupField=true&allChildrenOf=" + encodeURIComponent(parentTerm)
+ "&ontology=" + ontologyId + "&queryFields=iri";
logger.log("debug", `Evaluating isChildTermOf, query url: [${url}]`);
request(url, (error, response, body) => {
let jsonBody = JSON.parse(body);
if(jsonBody.response.numFound === 1) {
logger.log("debug", "It's a child term!");
resolve(true);
} else if(jsonBody.response.numFound === 0) {
logger.log("debug", `Provided term is not child of [${parentTerm}]`);
errors.push(
new CustomAjvError(
"isChildTermOf", `Provided term is not child of [${parentTerm}]`,
{keyword: "isChildTermOf"})
);
reject(new Ajv.ValidationError(errors));
} else {
errors.push(
new CustomAjvError(
"isChildTermOf", "Something went wrong while validating term, try again.",
{keyword: "isChildTermOf"})
);
reject(new Ajv.ValidationError(errors));
}
});
} else {
errors.push(
new CustomAjvError(
"isChildTermOf",
"Missing required variable in schema isChildTermOf, required properties are: parentTerm and ontologyId.",
{keyword: "isChildTermOf"})
);
reject(new Ajv.ValidationError(errors));
}
});
}
isChildTermOf.definition = {
async: true,
type: "string",
validate: findChildTerm,
errors: true
};
ajv.addKeyword("isChildTermOf", isChildTermOf.definition);
return ajv;
};
var Ajv = require("ajv");
var request = require("request");
const logger = require("../winston");
const CustomAjvError = require("../model/custom-ajv-error");
const NoResults = "No results.";
module.exports = function isValidTaxonomy(ajv) {
function findTaxonomy(schema, data) {
return new Promise((resolve, reject) => {
if(schema) {
const taxonomySearchUrl = "https://www.ebi.ac.uk/ena/taxonomy/rest/any-name";
let errors = [];
const taxonomyExpression = data;
const encodedTaxonomyUri = encodeURIComponent(taxonomyExpression);
const url = [taxonomySearchUrl, encodedTaxonomyUri].join("/");
logger.log("debug", `Looking for taxonomy [${taxonomyExpression}] with ENA taxonomy validator.`);
request(url, (error, Response, body) => {
logger.log("debug", `Raw response: ${body}`);
if (body === NoResults) {
generateNotExistsErrorMessage();
} else {
let jsonBody = JSON.parse(body);
if (jsonBody) {
let numFound = jsonBody.length;
if (numFound === 1 && jsonBody[0]["taxId"] && jsonBody[0]["submittable"] == "true") {
logger.log("debug", "Found 1 match!");
resolve(true);
} else if (numFound === 0) {
generateNotExistsErrorMessage()
} else {
errors.push(
new CustomAjvError(
"isValidTaxonomy", `Something went wrong while validating the given taxonomy expression [${taxonomyExpression}], try again.`,
{keyword: "isValidTaxonomy"})
);
reject(new Ajv.ValidationError(errors));
}
} else {
generateNotExistsErrorMessage();
}
}
function generateNotExistsErrorMessage() {
logger.log("debug", `Could not find the given taxonomy [${taxonomyExpression}].`);
errors.push(
new CustomAjvError(
"isValidTaxonomy", `provided taxonomy expression does not exist: [${taxonomyExpression}]`,
{keyword: "isValidTaxonomy"})
);
reject(new Ajv.ValidationError(errors));
}
});
} else {
resolve(true);
}
});
}
isValidTaxonomy.definition = {
async: true,
type: "string",
validate: findTaxonomy,
errors: true
};
ajv.addKeyword("isValidTaxonomy", isValidTaxonomy.definition);
return ajv;
};
var Ajv = require("ajv");
var request = require("request");
const logger = require("../winston");
const CustomAjvError = require("../model/custom-ajv-error");
module.exports = function isValidTerm(ajv) {
function findTerm(schema, data) {
return new Promise((resolve, reject) => {
if(schema) {
const olsSearchUrl = "https://www.ebi.ac.uk/ols/api/terms/findByIdAndIsDefiningOntology?id=";
let errors = [];
const termUri = data;
const encodedTermUri = encodeURIComponent(termUri);
const url = olsSearchUrl + encodedTermUri;
logger.log("debug", `Looking for term [${termUri}] in OLS.`);
request(url, (error, Response, body) => {
let jsonBody = JSON.parse(body);
function generateNotExistsErrorMessage() {
logger.log("debug", "Could not find term in OLS.");
errors.push(
new CustomAjvError(
"isValidTerm", `provided term does not exist in OLS: [${termUri}]`,
{keyword: "isValidTerm"})
);
reject(new Ajv.ValidationError(errors));
}
if (jsonBody["_embedded"]) {
let numFound = jsonBody["_embedded"]["terms"].length;
if (numFound === 1) {
logger.log("debug", "Found 1 match!");
resolve(true);
} else if (numFound === 0) {
generateNotExistsErrorMessage()
} else {
errors.push(
new CustomAjvError(
"isValidTerm", "Something went wrong while validating term, try again.",
{keyword: "isValidTerm"})
);
reject(new Ajv.ValidationError(errors));
}
} else {
generateNotExistsErrorMessage();
}
});
} else {
resolve(true);
}
});
}
isValidTerm.definition = {
async: true,
type: "string",
validate: findTerm,
errors: true
};
ajv.addKeyword("isValidTerm", isValidTerm.definition);
return ajv;
};
let Ajv = require("ajv");
const logger = require("./winston");
let IsChildTermOf = require("./custom/ischildtermof");
let IsValidTerm = require("./custom/isvalidterm");
let IsValidTaxonomy = require("./custom/isvalidtaxonomy");
const ValidationError = require("./model/validation-error");
const AppError = require("./model/application-error");
let ajv = new Ajv({allErrors: true});
let isChildTermOf = new IsChildTermOf(ajv);
let isValidTerm = new IsValidTerm(ajv);
new IsValidTaxonomy(ajv);
const { ElixirValidator, isChildTermOf, isValidTerm, isValidTaxonomy} = require('elixir-jsonschema-validator');
const validator = new ElixirValidator([
new isChildTermOf(null, "https://www.ebi.ac.uk/ols/api/search?q="),
new isValidTerm(null, "https://www.ebi.ac.uk/ols/api/search?q="),
new isValidTaxonomy(null)
]);
function convertToValidationErrors(ajvErrorObjects) {
let localErrors = [];
......@@ -29,23 +28,25 @@ function convertToValidationErrors(ajvErrorObjects) {
function runValidation(inputSchema, inputObject) {
logger.log("silly", "Running validation...");
return new Promise((resolve, reject) => {
var validate = ajv.compile(inputSchema);
Promise.resolve(validate(inputObject))
.then((data) => {
if (validate.errors) {
logger.log("debug", ajv.errorsText(validate.errors, {dataVar: inputObject.alias}));
resolve(convertToValidationErrors(validate.errors));
} else {
validator.validate(inputSchema, inputObject)
.then((validationResult) => {
if (validationResult.length == 0) {
resolve([]);
} else {
let ajvErrors = [];
validationResult.forEach(validationError => {
ajvErrors.push(validationError);
});
resolve(convertToValidationErrors(ajvErrors));
}
}
).catch((err, errors) => {
if (!(err instanceof Ajv.ValidationError)) {
logger.log("error", "An error ocurred while running the validation.");
reject(new AppError("An error ocurred while running the validation."));
).catch((error) => {
if (error.errors) {
reject(new AppError(error.errors));
} else {
logger.log("debug", ajv.errorsText(err.errors, {dataVar: inputObject.alias}));
resolve(convertToValidationErrors(err.errors));
logger.log("error", "An error ocurred while running the validation. Error : " + JSON.stringify(error));
reject(new AppError("An error ocurred while running the validation."));
}
});
});
......
const fs = require("fs");
const runValidation = require("../src/validator");
test("valid taxonomy expression should pass the validation", () => {
let inputSchema = fs.readFileSync("examples/schemas/isValidTaxonomy-schema.json");
let jsonSchema = JSON.parse(inputSchema);
let inputObj = fs.readFileSync("examples/objects/isValidTaxonomy.json");
let jsonObj = JSON.parse(inputObj);
return runValidation(jsonSchema, jsonObj).then( (data) => {
console.log(data);
expect(data).toBeDefined();
expect(data.length).toBe(0);
});
});
test("invalid taxonomy expresson should return an error", () => {
let inputSchema = fs.readFileSync("examples/schemas/isValidTaxonomy-schema.json");
let jsonSchema = JSON.parse(inputSchema);
let inputObj = fs.readFileSync("examples/objects/isInvalidTaxonomy.json");
let jsonObj = JSON.parse(inputObj);
return runValidation(jsonSchema, jsonObj).then( (data) => {
console.log(data);
expect(data).toBeDefined();
expect(data.length).toBe(1);
});
});
\ No newline at end of file
const fs = require("fs");
const runValidation = require("../src/validator");
test("isValidTerm", () => {
let inputSchema = fs.readFileSync("examples/schemas/isValidTerm-schema.json");
let jsonSchema = JSON.parse(inputSchema);
let inputObj = fs.readFileSync("examples/objects/isValidTerm.json");
let jsonObj = JSON.parse(inputObj);
return runValidation(jsonSchema, jsonObj).then( (data) => {
expect(data).toBeDefined();
expect(data[0]).toBeDefined();
});
});
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment