@luvio/jsonschema-validate
Advanced tools
Comparing version 5.23.0 to 5.24.0
@@ -7,3 +7,10 @@ /** | ||
import { ok, err } from '@luvio/utils'; | ||
class JsonSchemaViolationError extends Error { | ||
constructor(message, validationErrors) { | ||
super(message); | ||
this.validationErrors = []; | ||
this.validationErrors = validationErrors || []; | ||
} | ||
} | ||
@@ -22,2 +29,46 @@ class MinItemsViolationError extends JsonSchemaViolationError { | ||
} | ||
class JsonSchemaErrorCollector { | ||
constructor() { | ||
this.errors = []; | ||
} | ||
add(error) { | ||
this.errors.push(error); | ||
} | ||
append(response) { | ||
if (response.isErr()) { | ||
this.errors.push(...response.error); | ||
} | ||
} | ||
hasErrors() { | ||
return this.errors.length > 0; | ||
} | ||
prepend(error) { | ||
this.errors.unshift(error); | ||
} | ||
toValidationResponse() { | ||
return !this.hasErrors() ? ok(true) : err(this.errors); | ||
} | ||
} | ||
function createThrowableError(errors) { | ||
if (errors[0] instanceof MinItemsViolationError) { | ||
return new MinItemsViolationError(errors[0].message, errors); | ||
} | ||
if (errors[0] instanceof MaxItemsViolationError) { | ||
return new MaxItemsViolationError(errors[0].message, errors); | ||
} | ||
if (errors[0] instanceof IncorrectTypeError) { | ||
return new IncorrectTypeError(errors[0].message, errors); | ||
} | ||
if (errors[0] instanceof AdditionalPropertiesError) { | ||
return new AdditionalPropertiesError(errors[0].message, errors); | ||
} | ||
if (errors[0] instanceof MissingRequiredPropertyError) { | ||
return new MissingRequiredPropertyError(errors[0].message, errors); | ||
} | ||
if (errors[0] instanceof InvalidRefError) { | ||
return new InvalidRefError(errors[0].message, errors); | ||
} | ||
// any non-specific validation errors | ||
return new JsonSchemaViolationError(errors[0].message, errors); | ||
} | ||
/** | ||
@@ -31,79 +82,98 @@ * Validates a value against a schema in a TypeScript-friendly way. | ||
function assertIsValid(data, schema) { | ||
validateJsonSchema(data, schema); | ||
const validationResponse = validateJsonSchema(data, schema); | ||
if (validationResponse.isErr()) { | ||
throw createThrowableError(validationResponse.error); | ||
} | ||
} | ||
function incorrectTypeError(expected, actual, path) { | ||
throw new IncorrectTypeError(`Expected type ${expected} at path '${path}', found type ${actual}.`); | ||
return new IncorrectTypeError(`Expected type ${expected} at path '${path}', found type ${actual}.`); | ||
} | ||
function validSchemaResponse() { | ||
return ok(true); | ||
} | ||
function invalidSchemaResponseWithError(error) { | ||
return err([error]); | ||
} | ||
function validateJsonSchema(data, schema, path = '$', document = schema) { | ||
if (schema === true) | ||
return; | ||
return validSchemaResponse(); | ||
if (schema === false) | ||
throw new JsonSchemaViolationError(`Data at ${path} has schema 'false'`); | ||
return invalidSchemaResponseWithError(new JsonSchemaViolationError(`Data at ${path} has schema 'false'`)); | ||
const dataType = data === null ? 'null' : Array.isArray(data) ? 'array' : typeof data; | ||
const errorCollector = new JsonSchemaErrorCollector(); | ||
if ('anyOf' in schema) { | ||
validateAnyOf(data, schema, path, document); | ||
errorCollector.append(validateAnyOf(data, schema, path, document)); | ||
} | ||
else if ('allOf' in schema) { | ||
validateAllOf(data, schema, path, document); | ||
errorCollector.append(validateAllOf(data, schema, path, document)); | ||
} | ||
else if ('not' in schema) { | ||
validateNot(data, schema, path, document); | ||
errorCollector.append(validateNot(data, schema, path, document)); | ||
} | ||
else if ('$ref' in schema) { | ||
validateRef(data, schema, path, document); | ||
errorCollector.append(validateRef(data, schema, path, document)); | ||
} | ||
else if (schema.type === 'object') { | ||
if (dataType !== 'object') { | ||
incorrectTypeError('object', dataType, path); | ||
errorCollector.add(incorrectTypeError('object', dataType, path)); | ||
} | ||
validateObject(data, schema, path, document); | ||
else { | ||
errorCollector.append(validateObject(data, schema, path, document)); | ||
} | ||
} | ||
else if (schema.type === 'array') { | ||
if (dataType !== 'array') { | ||
incorrectTypeError('array', dataType, path); | ||
errorCollector.add(incorrectTypeError('array', dataType, path)); | ||
} | ||
validateArray(data, schema, path, document); | ||
else { | ||
errorCollector.append(validateArray(data, schema, path, document)); | ||
} | ||
} | ||
else { | ||
validateScalar(data, schema, path); | ||
errorCollector.append(validateScalar(data, schema, path)); | ||
} | ||
return errorCollector.toValidationResponse(); | ||
} | ||
function validateAnyOf(data, schema, path, document) { | ||
if (!schema.anyOf.find((element) => { | ||
try { | ||
validateJsonSchema(data, element, path, document); | ||
return true; | ||
let isValid = false; | ||
const errorCollector = new JsonSchemaErrorCollector(); | ||
for (let i = 0, { length } = schema.anyOf; i < length; i++) { | ||
const element = schema.anyOf[i]; | ||
const validationResponse = validateJsonSchema(data, element, path, document); | ||
if (validationResponse.isOk()) { | ||
isValid = true; | ||
break; | ||
} | ||
catch (err) { | ||
// swallow error for individual validations as only one needs to pass | ||
else { | ||
errorCollector.append(validationResponse); | ||
} | ||
})) { | ||
throw new JsonSchemaViolationError(`Data at ${path} did not match any subschema in anyOf.`); | ||
} | ||
if (!isValid) { | ||
errorCollector.prepend(new JsonSchemaViolationError(`Data at ${path} did not match any subschema in anyOf.`)); | ||
return errorCollector.toValidationResponse(); | ||
} | ||
return validSchemaResponse(); | ||
} | ||
function validateAllOf(data, schema, path, document) { | ||
if (!schema.allOf.every((element) => { | ||
try { | ||
validateJsonSchema(data, element, path, document); | ||
return true; | ||
let isValid = true; | ||
const errorCollector = new JsonSchemaErrorCollector(); | ||
for (let i = 0, { length } = schema.allOf; i < length; i++) { | ||
const element = schema.allOf[i]; | ||
const validationResponse = validateJsonSchema(data, element, path, document); | ||
if (!validationResponse.isOk()) { | ||
errorCollector.append(validationResponse); | ||
isValid = false; | ||
} | ||
catch (err) { | ||
// swallow error for individual validations as only one needs to pass | ||
} | ||
})) { | ||
throw new JsonSchemaViolationError(`Data at ${path} did not match some subschemas in allOf.`); | ||
} | ||
if (!isValid) { | ||
errorCollector.prepend(new JsonSchemaViolationError(`Data at ${path} did not match some subschemas in allOf.`)); | ||
} | ||
return errorCollector.toValidationResponse(); | ||
} | ||
function validateNot(data, schema, path, document) { | ||
try { | ||
validateJsonSchema(data, schema.not, path, document); | ||
const validationResponse = validateJsonSchema(data, schema.not, path, document); | ||
if (validationResponse.isOk()) { | ||
return invalidSchemaResponseWithError(new JsonSchemaViolationError(`Data at ${path} validated against the schema of a not clause.`)); | ||
} | ||
catch (e) { | ||
if (e instanceof JsonSchemaViolationError) { | ||
// Failed validation of the not schema means it is valid | ||
return; | ||
} | ||
throw e; | ||
} | ||
throw new JsonSchemaViolationError(`Data at ${path} validated against the schema of a not clause.`); | ||
return validSchemaResponse(); | ||
} | ||
@@ -114,5 +184,6 @@ function validateObject(data, schema, path, document) { | ||
const schemaKeySet = new Set(schemaKeys); | ||
const errorCollector = new JsonSchemaErrorCollector(); | ||
Object.keys(data).forEach((key) => { | ||
if (!schemaKeySet.has(key)) { | ||
validateJsonSchema(data[key], schema.additionalProperties, `${path}.additionalProperties[${key}]`, document); | ||
errorCollector.append(validateJsonSchema(data[key], schema.additionalProperties, `${path}.additionalProperties[${key}]`, document)); | ||
} | ||
@@ -124,17 +195,20 @@ }); | ||
if (requiredKeys.has(key) && !keyInData) { | ||
throw new MissingRequiredPropertyError(`Object at path '${path}' is missing required property '${key}'.`); | ||
errorCollector.add(new MissingRequiredPropertyError(`Object at path '${path}' is missing required property '${key}'.`)); | ||
} | ||
if (keyInData) { | ||
validateJsonSchema(data[key], schema.properties[key], `${path}.${key}`, document); | ||
errorCollector.append(validateJsonSchema(data[key], schema.properties[key], `${path}.${key}`, document)); | ||
} | ||
} | ||
return errorCollector.toValidationResponse(); | ||
} | ||
function validateArray(data, schema, path, document) { | ||
if (schema.minItems !== undefined && data.length < schema.minItems) { | ||
throw new MinItemsViolationError(`Array at path '${path}' fails minItems constraint. Has ${data.length} items, needs at least ${schema.minItems}.`); | ||
return invalidSchemaResponseWithError(new MinItemsViolationError(`Array at path '${path}' fails minItems constraint. Has ${data.length} items, needs at least ${schema.minItems}.`)); | ||
} | ||
if (schema.maxItems !== undefined && data.length > schema.maxItems) { | ||
throw new MaxItemsViolationError(`Array at path '${path}' fails maxItems constraint. Has ${data.length} items, needs at most ${schema.maxItems}.`); | ||
return invalidSchemaResponseWithError(new MaxItemsViolationError(`Array at path '${path}' fails maxItems constraint. Has ${data.length} items, needs at most ${schema.maxItems}.`)); | ||
} | ||
data.forEach((element, index) => validateJsonSchema(element, schema.items, `${path}[${index}]`, document)); | ||
const errorCollector = new JsonSchemaErrorCollector(); | ||
data.forEach((element, index) => errorCollector.append(validateJsonSchema(element, schema.items, `${path}[${index}]`, document))); | ||
return errorCollector.toValidationResponse(); | ||
} | ||
@@ -146,3 +220,3 @@ function validateScalar(data, schema, path) { | ||
if (dataType !== 'number' || !Number.isInteger(data)) { | ||
incorrectTypeError('integer', dataType, path); | ||
return invalidSchemaResponseWithError(incorrectTypeError('integer', dataType, path)); | ||
} | ||
@@ -152,3 +226,3 @@ } | ||
if (dataType !== 'number') { | ||
incorrectTypeError('number', dataType, path); | ||
return invalidSchemaResponseWithError(incorrectTypeError('number', dataType, path)); | ||
} | ||
@@ -158,3 +232,3 @@ } | ||
if (dataType !== 'string') { | ||
incorrectTypeError('string', dataType, path); | ||
return invalidSchemaResponseWithError(incorrectTypeError('string', dataType, path)); | ||
} | ||
@@ -164,3 +238,3 @@ } | ||
if (dataType !== 'boolean') { | ||
incorrectTypeError('boolean', dataType, path); | ||
return invalidSchemaResponseWithError(incorrectTypeError('boolean', dataType, path)); | ||
} | ||
@@ -170,15 +244,21 @@ } | ||
if (data !== null) { | ||
incorrectTypeError('null', dataType, path); | ||
return invalidSchemaResponseWithError(incorrectTypeError('null', dataType, path)); | ||
} | ||
} | ||
else { | ||
throw new IncorrectTypeError(`Unknown schema data type: ${schemaDataType}.`); | ||
return invalidSchemaResponseWithError(new IncorrectTypeError(`Unknown schema data type: ${schemaDataType}.`)); | ||
} | ||
return validSchemaResponse(); | ||
} | ||
function validateRef(data, schema, path, document) { | ||
if (!schema.$ref.startsWith('#')) { | ||
throw new InvalidRefError(`$ref values that do not refer to the current document are unsupported (must start with '#')`); | ||
return invalidSchemaResponseWithError(new InvalidRefError(`$ref values that do not refer to the current document are unsupported (must start with '#')`)); | ||
} | ||
const schemaToValidate = findSchemaAtPath(document, schema.$ref); | ||
validateJsonSchema(data, schemaToValidate, path, document); | ||
try { | ||
const schemaToValidate = findSchemaAtPath(document, schema.$ref); | ||
return validateJsonSchema(data, schemaToValidate, path, document); | ||
} | ||
catch (e) { | ||
return invalidSchemaResponseWithError(e); | ||
} | ||
} | ||
@@ -201,3 +281,3 @@ function findSchemaAtPath(document, ref) { | ||
export { AdditionalPropertiesError, IncorrectTypeError, InvalidRefError, JsonSchemaViolationError, MaxItemsViolationError, MinItemsViolationError, MissingRequiredPropertyError, assertIsValid, validateAllOf, validateAnyOf, validateArray, validateJsonSchema, validateNot, validateObject, validateScalar }; | ||
export { AdditionalPropertiesError, IncorrectTypeError, InvalidRefError, JsonSchemaViolationError, MaxItemsViolationError, MinItemsViolationError, MissingRequiredPropertyError, assertIsValid }; | ||
//# sourceMappingURL=jsonschema-validate.js.map |
@@ -42,3 +42,10 @@ export type JSONSchema = ObjectType | ArrayType | ScalarType | AnyOf | AllOf | Not | RefType | boolean; | ||
}; | ||
export type JsonSchemaValidationError = JsonSchemaViolationError | MinItemsViolationError | MaxItemsViolationError | IncorrectTypeError | AdditionalPropertiesError | MissingRequiredPropertyError | InvalidRefError; | ||
export interface JsonSchemaValidationResponse { | ||
isValid: boolean; | ||
errors: Array<JsonSchemaValidationError>; | ||
} | ||
export declare class JsonSchemaViolationError extends Error { | ||
validationErrors: JsonSchemaValidationError[]; | ||
constructor(message?: string, validationErrors?: Array<JsonSchemaValidationError>); | ||
} | ||
@@ -65,9 +72,2 @@ export declare class MinItemsViolationError extends JsonSchemaViolationError { | ||
export declare function assertIsValid<Type>(data: unknown, schema: JSONSchema): asserts data is Type; | ||
export declare function validateJsonSchema(data: unknown, schema: JSONSchema, path?: string, document?: JSONSchema): void; | ||
export declare function validateAnyOf(data: unknown, schema: AnyOf, path: string, document: JSONSchema): void; | ||
export declare function validateAllOf(data: unknown, schema: AllOf, path: string, document: JSONSchema): void; | ||
export declare function validateNot(data: unknown, schema: Not, path: string, document: JSONSchema): void; | ||
export declare function validateObject(data: Record<string, unknown>, schema: ObjectType, path: string, document: JSONSchema): void; | ||
export declare function validateArray(data: Array<unknown>, schema: ArrayType, path: string, document: JSONSchema): void; | ||
export declare function validateScalar(data: number | string | boolean | null, schema: ScalarType, path: string): void; | ||
export {}; |
{ | ||
"name": "@luvio/jsonschema-validate", | ||
"version": "5.23.0", | ||
"version": "5.24.0", | ||
"private": false, | ||
@@ -24,2 +24,5 @@ "description": "Luvio Next JSON Schema Validation", | ||
}, | ||
"dependencies": { | ||
"@luvio/utils": "5.24.0" | ||
}, | ||
"volta": { | ||
@@ -26,0 +29,0 @@ "extends": "../../../package.json" |
Sorry, the diff of this file is not supported yet
63102
341
1
+ Added@luvio/utils@5.24.0
+ Added@luvio/utils@5.24.0(transitive)