Skip to content
This repository has been archived by the owner on Nov 8, 2024. It is now read-only.

Commit

Permalink
feat: adds JSON Schema Draft 6/7 support
Browse files Browse the repository at this point in the history
  • Loading branch information
artem-zakharchenko committed Nov 26, 2019
1 parent 816b7ab commit 5e9f09e
Show file tree
Hide file tree
Showing 15 changed files with 791 additions and 345 deletions.
4 changes: 3 additions & 1 deletion lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class UnknownValidatorError extends Error {}
class NotValidatableError extends Error {}
class NotEnoughDataError extends Error {}
class JsonSchemaNotValid extends Error {}
class JsonSchemaNotSupported extends Error {}

module.exports = {
DataNotJsonParsableError,
Expand All @@ -18,4 +19,5 @@ module.exports = {
NotValidatableError,
NotEnoughDataError,
JsonSchemaNotValid,
}
JsonSchemaNotSupported
};
182 changes: 182 additions & 0 deletions lib/validators/json-schema-legacy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
const amanda = require('amanda');
const tv4 = require('tv4');
const jsonPointer = require('json-pointer');

const { JsonSchemaValidator, META_SCHEMA } = require('./json-schema-next');
const { ValidationErrors } = require('./validation-errors');

/**
* Returns a proper article for a given string.
* @param {string} str
* @returns {string}
*/
function getArticle(str) {
return ['a', 'e', 'i', 'o', 'u'].includes(str.toLowerCase()) ? 'an' : 'a';
}

const jsonSchemaOptions = {
singleError: false,
messages: {
minLength: (prop, val, validator) =>
`The ${prop} property must be at least ${validator} characters long (currently ${val.length} characters long).`,
maxLength: (prop, val, validator) =>
`The ${prop} property must not exceed ${validator} characters (currently${val.length} characters long).`,
length: (prop, val, validator) =>
`The ${prop} property must be exactly ${validator} characters long (currently ${val.length} characters long).`,
format: (prop, val, validator) =>
`The ${prop} property must be ${getArticle(
validator[0]
)} ${validator} (current value is ${JSON.stringify(val)}).`,
type: (prop, val, validator) =>
`The ${prop} property must be ${getArticle(
validator[0]
)} ${validator} (current value is ${JSON.stringify(val)})."`,
except: (prop, val) => `The ${prop} property must not be ${val}.`,
minimum: (prop, val, validator) =>
`The minimum value of the ${prop} must be ${validator} (current value is ${JSON.stringify(
val
)}).`,
maximum: (prop, val, validator) =>
`The maximum value of the ${prop} must be ${validator} (current value is ${JSON.stringify(
val
)}).`,
pattern: (prop, val, validator) =>
`The ${prop} value (${val}) does not match the ${validator} pattern.`,
maxItems: (prop, val, validator) =>
`The ${prop} property must not contain more than ${validator} items (currently contains ${val.length} items).`,
minItems: (prop, val, validator) =>
`The ${prop} property must contain at least ${validator} items (currently contains ${val.length} items).`,
divisibleBy: (prop, val, validator) =>
`The ${prop} property is not divisible by ${validator} (current value is ${JSON.stringify(
val
)}).`,
uniqueItems: (prop) => `All items in the ${prop} property must be unique.`
}
};

class JsonSchemaLegacy extends JsonSchemaValidator {
validateSchema() {
const { jsonSchema, jsonMetaSchema } = this;
const metaSchema = jsonMetaSchema || META_SCHEMA.v3;

tv4.reset();
tv4.addSchema('', metaSchema);
tv4.addSchema(metaSchema.$schema, metaSchema);
const validationResult = tv4.validateResult(jsonSchema, metaSchema);

return validationResult.valid;
}

validate(data) {
switch (this.jsonSchemaVersion) {
case 'v3':
return this.validateUsingAmanda(data);
case 'v4':
return this.validateUsingTV4(data);
default:
throw new Error(
'Attempted to use JsonSchemaLegacy on non-legacy JSON Schema!'
);
}
}

validateUsingAmanda(data) {
let errors = {
length: 0,
errorMessages: {}
};

try {
amanda.validate(data, this.jsonSchema, jsonSchemaOptions, (error) => {
if (error && error.length > 0) {
for (let i = 0; i < error.length; i++) {
if (error[i].property === '') {
error[i].property = [];
}
}

errors = new ValidationErrors(error);
}
});
} catch (error) {
errors = new ValidationErrors({
'0': {
property: [],
attributeValue: true,
message: `Validator internal error: ${error.message}`,
validatorName: 'error'
},
length: 1,
errorMessages: {}
});
}

return this.toGavelResult(errors);
}

validateUsingTV4(data) {
const result = tv4.validateMultiple(data, this.jsonSchema);
const validationErrors = result.errors.concat(result.missing);

const amandaCompatibleError = {
length: validationErrors.length,
errorMessages: {}
};

for (let index = 0; index < validationErrors.length; index++) {
const validationError = validationErrors[index];
let error;

if (validationError instanceof Error) {
error = validationError;
} else {
error = new Error('Missing schema');
error.params = { key: validationError };
error.dataPath = '';
}

const pathArray = jsonPointer
.parse(error.dataPath)
.concat(error.params.key || []);
const pointer = jsonPointer.compile(pathArray);

amandaCompatibleError[index] = {
message: `At '${pointer}' ${error.message}`,
property: pathArray,
attributeValue: true,
validatorName: 'error'
};
}

const errors = new ValidationErrors(amandaCompatibleError);
return this.toGavelResult(errors);
}

/**
* Converts Amanda-like validation result to the
* unified Gavel public validation result.
*/
toGavelResult(amandaLikeResult) {
const results = Array.from(
{ length: amandaLikeResult.length },
(_, index) => {
const item = amandaLikeResult[index];
const { property, message } = item;
const pathArray = [].concat(property).filter(Boolean);
const pointer = jsonPointer.compile(pathArray);

return {
message,
location: {
pointer,
property
}
};
}
);

return results;
}
}

module.exports = { JsonSchemaLegacy };
133 changes: 133 additions & 0 deletions lib/validators/json-schema-next.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const Ajv = require('ajv');
const metaSchemaV6 = require('ajv/lib/refs/json-schema-draft-06.json');
const metaSchemaV7 = require('ajv/lib/refs/json-schema-draft-07.json');

const metaSchemaV4 = require('../meta-schema-v4');
const metaSchemaV3 = require('../meta-schema-v3');
const errors = require('../errors');

const SCHEMA_VERSIONS = {
v3: 'http://json-schema.org/draft-03/schema',
v4: 'http://json-schema.org/draft-04/schema',
v6: 'http://json-schema.org/draft-06/schema',
v7: 'http://json-schema.org/draft-07/schema'
};

const META_SCHEMA = {
v3: metaSchemaV3,
v4: metaSchemaV4,
v6: metaSchemaV6,
v7: metaSchemaV7
};

/**
* Returns a JSON Schema Draft version of the given JSON Schema.
*/
const getSchemaVersion = (jsonSchema) => {
const jsonSchemaVersion = Object.keys(SCHEMA_VERSIONS).find((version) => {
const jsonSchemaAnnotation = SCHEMA_VERSIONS[version];
return (
jsonSchema.$schema && jsonSchema.$schema.includes(jsonSchemaAnnotation)
);
});

if (jsonSchemaVersion == null) {
throw new errors.JsonSchemaNotSupported(
`Provided JSON Schema version is missing, or not supported. Please provide a JSON Schema Draft ${Object.keys(
SCHEMA_VERSIONS
).join('/')}.`
);
}

return jsonSchemaVersion;
};

class JsonSchemaValidator {
constructor(jsonSchema) {
this.jsonSchema = jsonSchema;
this.jsonSchemaVersion = getSchemaVersion(jsonSchema);
this.jsonMetaSchema = this.getMetaSchema();

const isSchemaValid = this.validateSchema();
if (!isSchemaValid) {
throw new errors.JsonSchemaNotValid(
`Provided JSON Schema is not a valid JSON Schema Draft ${this.jsonSchemaVersion}.`
);
}
}

/**
* Returns a meta schema for the provided JSON Schema.
*/
getMetaSchema() {
return META_SCHEMA[this.jsonSchemaVersion];
}

/**
* Validates the schema against its version specification.
* @return {boolean}
*/
validateSchema() {
const { jsonSchemaVersion, jsonSchema } = this;
let isSchemaValid = true;

// use AJV to validate modern schema (6/7)
const ajv = new Ajv();

const metaSchema = META_SCHEMA[jsonSchemaVersion];
ajv.addMetaSchema(metaSchema, 'meta');

isSchemaValid = ajv.validateSchema(jsonSchema);

// Clean up the added meta schema
ajv.removeSchema('meta');

return isSchemaValid;
}

/**
* Validates the given data.
*/
validate(data) {
const { jsonSchemaVersion } = this;

console.log(
`validating json schema ${jsonSchemaVersion} against the data:\n`,
data
);

const ajv = new Ajv({
// meta: false,
validateSchema: false,
jsonPointers: true,
// Make AJV point to the property in "error.dataPath",
// so it could be used as a complete pointer.
errorDataPath: 'property'
});

// ajv.removeSchema(this.jsonSchema);
const validate = ajv.compile(this.jsonSchema);
validate(this.jsonSchema, data);

console.log(ajv.errors);

// Convert AJV validation errors to the Gavel public validation errors.
return ajv.errors.map((ajvError) => {
const { missingProperty: property } = ajvError.params;

return {
message: `${property} ${ajvError.message}`,
location: {
pointer: ajvError.dataPath,
property
}
};
});
}
}

module.exports = {
JsonSchemaValidator,
getSchemaVersion,
META_SCHEMA
};
Loading

0 comments on commit 5e9f09e

Please sign in to comment.