This repository has been archived by the owner on Nov 8, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds JSON Schema Draft 6/7 support
- Loading branch information
1 parent
816b7ab
commit 5e9f09e
Showing
15 changed files
with
791 additions
and
345 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
Oops, something went wrong.