diff --git a/package-lock.json b/package-lock.json index 8b839ed..f30fa7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "jsonpath": "^1.1.1" }, "devDependencies": { + "@json-schema-org/tests": "^2.0.0", + "@json-schema-tools/dereferencer": "^1.5.7", "@json-schema-tools/meta-schema": "^1.7.0", "@types/jest": "^29.1.1", "@types/jsonpath": "^0.2.0", @@ -953,12 +955,48 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@json-schema-org/tests": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@json-schema-org/tests/-/tests-2.0.0.tgz", + "integrity": "sha512-plJrcsV6SqgxI+K0L93m21hyTjGdssoT6Cj3vqAifEJ0jAsP5Pi/HBFrtUb+PTDwjdaxIWkH8UdN2CJCGUIsgg==", + "dev": true, + "dependencies": { + "json-schema-test-suite": "github:json-schema-org/JSON-Schema-Test-Suite#0a0f0cd" + } + }, + "node_modules/@json-schema-spec/json-pointer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@json-schema-spec/json-pointer/-/json-pointer-0.1.2.tgz", + "integrity": "sha512-BYY7IavBjwsWWSmVcMz2A9mKiDD9RvacnsItgmy1xV8cmgbtxFfKmKMtkVpD7pYtkx4mIW4800yZBXueVFIWPw==", + "dev": true + }, + "node_modules/@json-schema-tools/dereferencer": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@json-schema-tools/dereferencer/-/dereferencer-1.5.7.tgz", + "integrity": "sha512-7oIkXhPGiG5ucKBWOGTE9zMQ8i01QklP0Mue+bDLakEmG+TvGJqL+SZepqIYdtYvikQsO6bhCuSSv0VD4cQ0rg==", + "dev": true, + "dependencies": { + "@json-schema-tools/reference-resolver": "^1.2.5", + "@json-schema-tools/traverse": "^1.10.0", + "fast-safe-stringify": "^2.1.1" + } + }, "node_modules/@json-schema-tools/meta-schema": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@json-schema-tools/meta-schema/-/meta-schema-1.7.0.tgz", "integrity": "sha512-3pDzVUssW3hVnf8gvSu1sKaVIpLyvmpbxgGfkUoaBiErFKRS2CZOufHD0pUFoa5e6Cd5oa72s402nJbnDz76CA==", "dev": true }, + "node_modules/@json-schema-tools/reference-resolver": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@json-schema-tools/reference-resolver/-/reference-resolver-1.2.5.tgz", + "integrity": "sha512-xNQgX/ABnwvbIeexL5Czv08lXjHAL80HEUogza7E19eIL/EXD8HM4FvxG1JuTGyi5fA+sSP64C9pabELizcBBw==", + "dev": true, + "dependencies": { + "@json-schema-spec/json-pointer": "^0.1.2", + "isomorphic-fetch": "^3.0.0" + } + }, "node_modules/@json-schema-tools/traverse": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@json-schema-tools/traverse/-/traverse-1.10.0.tgz", @@ -1565,9 +1603,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.271", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.271.tgz", - "integrity": "sha512-BCPBtK07xR1/uY2HFDtl3wK2De66AW4MSiPlLrnPNxKC/Qhccxd59W73654S3y6Rb/k3hmuGJOBnhjfoutetXA==", + "version": "1.4.272", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.272.tgz", + "integrity": "sha512-KS6gPPGNrzpVv9HzFVq+Etd0AjZEPr5pvaTBn2yD6KV4+cKW4I0CJoJNgmTG6gUQPAMZ4wIPtcOuoou3qFAZCA==", "dev": true }, "node_modules/emittery": { @@ -1723,6 +1761,12 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2000,6 +2044,16 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -2676,6 +2730,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema-test-suite": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/json-schema-org/JSON-Schema-Test-Suite.git#0a0f0cd1b5cc7af1fc826bc0f58d7fc310b0500d", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", @@ -2882,6 +2942,26 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3481,6 +3561,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, "node_modules/ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -3692,6 +3778,28 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4519,12 +4627,48 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@json-schema-org/tests": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@json-schema-org/tests/-/tests-2.0.0.tgz", + "integrity": "sha512-plJrcsV6SqgxI+K0L93m21hyTjGdssoT6Cj3vqAifEJ0jAsP5Pi/HBFrtUb+PTDwjdaxIWkH8UdN2CJCGUIsgg==", + "dev": true, + "requires": { + "json-schema-test-suite": "github:json-schema-org/JSON-Schema-Test-Suite#0a0f0cd" + } + }, + "@json-schema-spec/json-pointer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@json-schema-spec/json-pointer/-/json-pointer-0.1.2.tgz", + "integrity": "sha512-BYY7IavBjwsWWSmVcMz2A9mKiDD9RvacnsItgmy1xV8cmgbtxFfKmKMtkVpD7pYtkx4mIW4800yZBXueVFIWPw==", + "dev": true + }, + "@json-schema-tools/dereferencer": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@json-schema-tools/dereferencer/-/dereferencer-1.5.7.tgz", + "integrity": "sha512-7oIkXhPGiG5ucKBWOGTE9zMQ8i01QklP0Mue+bDLakEmG+TvGJqL+SZepqIYdtYvikQsO6bhCuSSv0VD4cQ0rg==", + "dev": true, + "requires": { + "@json-schema-tools/reference-resolver": "^1.2.5", + "@json-schema-tools/traverse": "^1.10.0", + "fast-safe-stringify": "^2.1.1" + } + }, "@json-schema-tools/meta-schema": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@json-schema-tools/meta-schema/-/meta-schema-1.7.0.tgz", "integrity": "sha512-3pDzVUssW3hVnf8gvSu1sKaVIpLyvmpbxgGfkUoaBiErFKRS2CZOufHD0pUFoa5e6Cd5oa72s402nJbnDz76CA==", "dev": true }, + "@json-schema-tools/reference-resolver": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@json-schema-tools/reference-resolver/-/reference-resolver-1.2.5.tgz", + "integrity": "sha512-xNQgX/ABnwvbIeexL5Czv08lXjHAL80HEUogza7E19eIL/EXD8HM4FvxG1JuTGyi5fA+sSP64C9pabELizcBBw==", + "dev": true, + "requires": { + "@json-schema-spec/json-pointer": "^0.1.2", + "isomorphic-fetch": "^3.0.0" + } + }, "@json-schema-tools/traverse": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@json-schema-tools/traverse/-/traverse-1.10.0.tgz", @@ -5015,9 +5159,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.4.271", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.271.tgz", - "integrity": "sha512-BCPBtK07xR1/uY2HFDtl3wK2De66AW4MSiPlLrnPNxKC/Qhccxd59W73654S3y6Rb/k3hmuGJOBnhjfoutetXA==", + "version": "1.4.272", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.272.tgz", + "integrity": "sha512-KS6gPPGNrzpVv9HzFVq+Etd0AjZEPr5pvaTBn2yD6KV4+cKW4I0CJoJNgmTG6gUQPAMZ4wIPtcOuoou3qFAZCA==", "dev": true }, "emittery": { @@ -5127,6 +5271,12 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -5328,6 +5478,16 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -5847,6 +6007,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "json-schema-test-suite": { + "version": "git+ssh://git@github.com/json-schema-org/JSON-Schema-Test-Suite.git#0a0f0cd1b5cc7af1fc826bc0f58d7fc310b0500d", + "dev": true, + "from": "json-schema-test-suite@github:json-schema-org/JSON-Schema-Test-Suite#0a0f0cd" + }, "json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", @@ -6006,6 +6171,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6450,6 +6624,12 @@ "is-number": "^7.0.0" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, "ts-jest": { "version": "29.0.3", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", @@ -6582,6 +6762,28 @@ "makeerror": "1.0.12" } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index c5c5573..f11c45f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "scripts": { "build": "tsc", "build:docs": "typedoc", - "test": "jest --coverage" + "test": "jest --coverage --verbose" }, "author": "Zachary Belford", "license": "Apache-2.0", @@ -26,6 +26,8 @@ "!build/**/*.test.*" ], "devDependencies": { + "@json-schema-org/tests": "^2.0.0", + "@json-schema-tools/dereferencer": "^1.5.7", "@json-schema-tools/meta-schema": "^1.7.0", "@types/jest": "^29.1.1", "@types/jsonpath": "^0.2.0", diff --git a/src/base-validators/array.ts b/src/base-validators/array.ts index 902df29..a946a01 100644 --- a/src/base-validators/array.ts +++ b/src/base-validators/array.ts @@ -1,14 +1,14 @@ import { JSONSchemaObject } from "@json-schema-tools/meta-schema"; -export class ArrayValidationError implements Error { +export class ArrayValidationError extends ValidationError { public name = "ArrayValidationError"; - public message: string; - constructor(schema: JSONSchemaObject, data: any, reason: string) { - this.message = [ - "invalid data provided is not a valid Array", - `reason: ${reason}`, - ].join("\n"); + constructor(schema: JSONSchemaObject, data: any, public message: string) { + const msg = [ + "Invalid array value", + message, + ]; + super(schema, data, msg.join("\n")); } } diff --git a/src/base-validators/integer.test.ts b/src/base-validators/integer.test.ts index d3fb181..913438c 100644 --- a/src/base-validators/integer.test.ts +++ b/src/base-validators/integer.test.ts @@ -1,4 +1,4 @@ -import validator, { IntegerValidationError } from "./integer"; +import validator, { IntegerValidationError, IntegerValidationErrorBadType } from "./integer"; import { NumberValidationError } from "./number"; describe("validator", () => { @@ -6,5 +6,6 @@ describe("validator", () => { expect(validator({ type: "integer" }, 123)).toBe(true); expect(validator({ type: "integer" }, "this is an integer")).toBeInstanceOf(NumberValidationError); expect(validator({ type: "integer" }, 123.10)).toBeInstanceOf(IntegerValidationError); + expect(validator({ type: "integer" }, 123.10)).toBeInstanceOf(IntegerValidationErrorBadType); }); }); diff --git a/src/base-validators/integer.ts b/src/base-validators/integer.ts index c2b0f17..b60bdaa 100644 --- a/src/base-validators/integer.ts +++ b/src/base-validators/integer.ts @@ -2,15 +2,27 @@ import ValidationError from "../validation-error"; import { JSONSchemaObject } from "@json-schema-tools/meta-schema"; import NumberValidator, { NumberValidationError } from "./number"; -export class IntegerValidationError implements Error { +export class IntegerValidationError extends ValidationError { public name = "IntegerValidationError"; - public message: string; - constructor(schema: JSONSchemaObject, data: any, reason: string) { - this.message = [ - "invalid data provided is not a valid integer", - `reason: ${reason}`, - ].join("\n"); + constructor(schema: JSONSchemaObject, data: any, message: string) { + const msg = [ + "Invalid integer value", + message, + ]; + + super(schema, data, msg.join("\n")); + } +} + +export class IntegerValidationErrorBadType extends IntegerValidationError { + public name = "IntegerValidationErrorBadType"; + + constructor(schema: JSONSchemaObject, data: number) { + super(schema, data, [ + "Value must be an integer", + `Received value has a decimal component: ${data - Math.floor(data)}`, + ].join('\n')); } } @@ -23,13 +35,13 @@ const isInt = (num: number) => { export default (schema: JSONSchemaObject, d: any): true | ValidationError => { - const validNumber = NumberValidator(schema, d); + const validNumber = NumberValidator(schema, d); if (validNumber !== true) { return validNumber; } if (!isInt(d)) { - return new IntegerValidationError(schema, d, "provided number is a float, not an integer"); + return new IntegerValidationErrorBadType(schema, d); } return true; diff --git a/src/base-validators/number.test.ts b/src/base-validators/number.test.ts index 00951de..1d5e53a 100644 --- a/src/base-validators/number.test.ts +++ b/src/base-validators/number.test.ts @@ -1,53 +1,68 @@ -import validator, { NumberValidationError } from "./number"; +import validator, { + NumberValidationError, + NumberValidationErrorBadType, + NumberValidationErrorConst, + NumberValidationErrorEnum, + NumberValidationErrorExclusiveMaximum, + NumberValidationErrorExclusiveMinimum, + NumberValidationErrorMaximum, + NumberValidationErrorMinimum, + NumberValidationErrorMultipleOf +} from "./number"; describe("validator", () => { + it("general NumberValidationError", () => { + expect(validator({ type: "number" }, "123")) + .toBeInstanceOf(NumberValidationError); + }); + it("can validate and invalidate a number", () => { - expect(validator({ type: "number" }, "im a number")).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number" }, "im a number")).toBeInstanceOf(NumberValidationErrorBadType); expect(validator({ type: "number" }, 123)).toBe(true); - expect(validator({ type: "number" }, {})).toBeInstanceOf(NumberValidationError); - expect(validator({ type: "number" }, true)).toBeInstanceOf(NumberValidationError); - expect(validator({ type: "number" }, false)).toBeInstanceOf(NumberValidationError); - expect(validator({ type: "number" }, null)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number" }, {})).toBeInstanceOf(NumberValidationErrorBadType); + expect(validator({ type: "number" }, true)).toBeInstanceOf(NumberValidationErrorBadType); + expect(validator({ type: "number" }, false)).toBeInstanceOf(NumberValidationErrorBadType); + expect(validator({ type: "number" }, null)).toBeInstanceOf(NumberValidationErrorBadType); expect(validator({ type: "number" }, 123.123)).toBe(true); }); it("multipleOf", () => { expect(validator({ type: "number", multipleOf: 10 }, 100)).toBe(true); - expect(validator({ type: "number", multipleOf: 100 }, 10)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number", multipleOf: 100 }, 10)).toBeInstanceOf(NumberValidationErrorMultipleOf); }); it("maximum", () => { expect(validator({ type: "number", maximum: 10 }, 10)).toBe(true); - expect(validator({ type: "number", maximum: 10 }, 11)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number", maximum: 10 }, 11)).toBeInstanceOf(NumberValidationErrorMaximum); }); it("exclusiveMaximum", () => { expect(validator({ type: "number", exclusiveMaximum: 10 }, 9)).toBe(true); - expect(validator({ type: "number", exclusiveMaximum: 10 }, 10)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number", exclusiveMaximum: 10 }, 10)).toBeInstanceOf(NumberValidationErrorExclusiveMaximum); }); it("minimum", () => { expect(validator({ type: "number", minimum: 10 }, 10)).toBe(true); - expect(validator({ type: "number", minimum: 10 }, 9)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number", minimum: 10 }, 9)).toBeInstanceOf(NumberValidationErrorMinimum); }); it("exclusiveMinimum", () => { expect(validator({ type: "number", exclusiveMinimum: 10 }, 11)).toBe(true); - expect(validator({ type: "number", exclusiveMinimum: 10 }, 10)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number", exclusiveMinimum: 10 }, 10)).toBeInstanceOf(NumberValidationErrorExclusiveMinimum); }); it("const", () => { expect(validator({ type: "number", const: 123 }, 123)).toBe(true); - expect(validator({ type: "number", const: 456 }, 123)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number", const: 456 }, 123)).toBeInstanceOf(NumberValidationErrorConst); }); it("enum", () => { expect(validator({ type: "number", enum: [123] }, 123)).toBe(true); - expect(validator({ type: "number", enum: [123] }, 456)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number", enum: [123] }, 456)).toBeInstanceOf(NumberValidationErrorEnum); expect(validator({ type: "number", enum: [123, 456] }, 123)).toBe(true); - expect(validator({ type: "number", enum: [123, 456] }, 1)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number", enum: [123, 456] }, 1)).toBeInstanceOf(NumberValidationErrorEnum); - expect(validator({ type: "number", enum: ["foo", "123", "bar"] }, 123)).toBeInstanceOf(NumberValidationError); + expect(validator({ type: "number", enum: ["foo", "123", "bar"] }, 123)).toBeInstanceOf(NumberValidationErrorEnum); }); }); diff --git a/src/base-validators/number.ts b/src/base-validators/number.ts index 8378437..04051a0 100644 --- a/src/base-validators/number.ts +++ b/src/base-validators/number.ts @@ -1,69 +1,154 @@ import ValidationError from "../validation-error"; import { JSONSchemaObject } from "@json-schema-tools/meta-schema"; -export class NumberValidationError implements Error { +export class NumberValidationError extends ValidationError { public name = "NumberValidationError"; - public message: string; - constructor(schema: JSONSchemaObject, data: any, reason: string) { - this.message = [ - "invalid data provided is not a valid integer", - `reason: ${reason}`, - ].join("\n"); + constructor(schema: JSONSchemaObject, data: any, message: string) { + const msg = [ + "Invalid number value", + message, + ]; + + super(schema, data, msg.join("\n")); + } +} + +export class NumberValidationErrorBadType extends NumberValidationError { + public name = "NumberValidationErrorBadType"; + + constructor(schema: JSONSchemaObject, data: any) { + super(schema, data, [ + "Value must be of type 'number'", + `Received type: ${typeof data}`, + ].join('\n')); + } +} + +export class NumberValidationErrorMultipleOf extends NumberValidationError { + public name = "NumberValidationErrorMultipleOf"; + + constructor(schema: JSONSchemaObject, data: number, remainder: number) { + super(schema, data, [ + `Value is not a multiple of ${schema.multipleOf}`, + `remainder: ${remainder}`, + ].join('\n')); + } +} + +export class NumberValidationErrorMaximum extends NumberValidationError { + public name = "NumberValidationErrorMaximum"; + + constructor(schema: JSONSchemaObject, data: number) { + const max = schema.maximum as number; + super(schema, data, [ + `Value must be less than or equal to ${max}`, + `difference: ${data} - ${max} = ${data - max}`, + ].join('\n')); + } +} + +export class NumberValidationErrorMinimum extends NumberValidationError { + public name = "NumberValidationErrorMinimum"; + + constructor(schema: JSONSchemaObject, data: number) { + const min = schema.minimum as number; + super(schema, data, [ + `Value must be greater than or equal to ${schema.minimum}`, + `difference: ${min} - ${data} = ${min - data}`, + ].join('\n')); } } + +export class NumberValidationErrorExclusiveMaximum extends NumberValidationError { + public name = "NumberValidationErrorExclusiveMaximum"; + + constructor(schema: JSONSchemaObject, data: number) { + const max = schema.exclusiveMaximum as number; + super(schema, data, [ + `Value must be less than ${max}`, + `difference: ${data} - ${max} = ${data - max}`, + ].join('\n')); + } +} + +export class NumberValidationErrorExclusiveMinimum extends NumberValidationError { + public name = "NumberValidationErrorExclusiveMinimum"; + + constructor(schema: JSONSchemaObject, data: number) { + const min = schema.exclusiveMinimum as number; + super(schema, data, [ + `Value must be greater than ${min}`, + `difference: ${min} - ${data} = ${min - data}`, + ].join('\n')); + } +} + +export class NumberValidationErrorConst extends NumberValidationError { + public name = "NumberValidationErrorConst"; + + constructor(schema: JSONSchemaObject, data: number) { + super(schema, data, `Value must be equal to: ${schema.const}`); + } +} + +export class NumberValidationErrorEnum extends NumberValidationError { + public name = "NumberValidationErrorEnum"; + + constructor(schema: JSONSchemaObject, data: number) { + super( + schema, + data, + `Value must be equal to one of: ${(schema as any).enum.join(", ")}` + ); + } +} + export default (schema: JSONSchemaObject, d: any): true | ValidationError => { if (typeof d !== "number") { - return new NumberValidationError(schema, d, "Not a number type"); + return new NumberValidationErrorBadType(schema, d); } if (schema.multipleOf) { - if (d % schema.multipleOf !== 0) { - return new NumberValidationError(schema, d, `number is not a multiple of ${schema.multipleOf}`); + const r = d % schema.multipleOf; + if (r !== 0) { + return new NumberValidationErrorMultipleOf(schema, d, r); } } if (schema.maximum) { if (d > schema.maximum) { - return new NumberValidationError(schema, d, `number exceeds maximum of ${schema.maximum}`); + return new NumberValidationErrorMaximum(schema, d); } } if (schema.exclusiveMaximum) { if (d >= schema.exclusiveMaximum) { - return new NumberValidationError( - schema, - d, - `number is greater than or equal to exclusive maximum of ${schema.exclusiveMaximum}` - ); + return new NumberValidationErrorExclusiveMaximum(schema, d); } } if (schema.minimum) { if (d < schema.minimum) { - return new NumberValidationError(schema, d, `number is less than minimum of ${schema.minimum}`); + return new NumberValidationErrorMinimum(schema, d); } } if (schema.exclusiveMinimum) { if (d <= schema.exclusiveMinimum) { - return new NumberValidationError( - schema, - d, - `number is less than or equal to exclusive minimum of ${schema.exclusiveMinimum}` - ); + return new NumberValidationErrorExclusiveMinimum(schema, d); } } if (schema.const) { if (d !== schema.const) { - return new NumberValidationError(schema, d, `must be: ${schema.const}`); + return new NumberValidationErrorConst(schema, d); } } if (schema.enum) { if (schema.enum.indexOf(d) === -1) { - return new NumberValidationError(schema, d, `must be one of: ${schema.enum}`); + return new NumberValidationErrorEnum(schema, d); } } diff --git a/src/base-validators/object.test.ts b/src/base-validators/object.test.ts index 90a4089..d13b89f 100644 --- a/src/base-validators/object.test.ts +++ b/src/base-validators/object.test.ts @@ -1,18 +1,23 @@ import { JSONSchemaObject } from "@json-schema-tools/meta-schema"; -import validator, { ObjectValidationError } from "./object"; +import validator, { ObjectValidationError, ObjectValidationErrorBadType, ObjectValidationErrorDependentRequired, ObjectValidationErrorMaxProperties, ObjectValidationErrorMinProperties, ObjectValidationErrorRequired } from "./object"; describe("validator", () => { + it("general ObjectValidationError", () => { + expect(validator({ type: "object" }, "123")) + .toBeInstanceOf(ObjectValidationError); + }); + it("handles the basics", () => { expect(validator({ type: "object" }, {})).toBe(true); expect(validator({ type: "object" }, { foo: "123" })).toBe(true); - expect(validator({ type: "object" }, undefined)).toBeInstanceOf(ObjectValidationError); - expect(validator({ type: "object" }, null)).toBeInstanceOf(ObjectValidationError); - expect(validator({ type: "object" }, 123)).toBeInstanceOf(ObjectValidationError); - expect(validator({ type: "object" }, [])).toBeInstanceOf(ObjectValidationError); + expect(validator({ type: "object" }, undefined)).toBeInstanceOf(ObjectValidationErrorBadType); + expect(validator({ type: "object" }, null)).toBeInstanceOf(ObjectValidationErrorBadType); + expect(validator({ type: "object" }, 123)).toBeInstanceOf(ObjectValidationErrorBadType); + expect(validator({ type: "object" }, [])).toBeInstanceOf(ObjectValidationErrorBadType); class Foo { } - expect(validator({ type: "object" }, Foo)).toBeInstanceOf(ObjectValidationError); - expect(validator({ type: "object" }, new Foo())).toBeInstanceOf(ObjectValidationError); + expect(validator({ type: "object" }, Foo)).toBeInstanceOf(ObjectValidationErrorBadType); + expect(validator({ type: "object" }, new Foo())).toBeInstanceOf(ObjectValidationErrorBadType); }); it("dependentRequired", () => { @@ -28,8 +33,8 @@ describe("validator", () => { expect(validator(testSchema, { foo: 123, bar: 123, baz: 123 })).toBe(true); expect(validator(testSchema, { bar: 123, baz: 123 })).toBe(true); - expect(validator(testSchema, { foo: 123 })).toBeInstanceOf(ObjectValidationError); - expect(validator(testSchema, { foo: 123, bar: 123 })).toBeInstanceOf(ObjectValidationError); + expect(validator(testSchema, { foo: 123 })).toBeInstanceOf(ObjectValidationErrorDependentRequired); + expect(validator(testSchema, { foo: 123, bar: 123 })).toBeInstanceOf(ObjectValidationErrorDependentRequired); }); it("handles required properties", () => { @@ -42,9 +47,9 @@ describe("validator", () => { } as JSONSchemaObject; expect(validator(testSchema, { foo: 123 })).toBe(true); expect(validator(testSchema, { foo: 123, bar: 123 })).toBe(true); - expect(validator(testSchema, {})).toBeInstanceOf(ObjectValidationError); - expect(validator(testSchema, { bar: 123 })).toBeInstanceOf(ObjectValidationError); - expect(validator(testSchema, { bar: 123 })).toBeInstanceOf(ObjectValidationError); + expect(validator(testSchema, {})).toBeInstanceOf(ObjectValidationErrorRequired); + expect(validator(testSchema, { bar: 123 })).toBeInstanceOf(ObjectValidationErrorRequired); + expect(validator(testSchema, { bar: 123 })).toBeInstanceOf(ObjectValidationErrorRequired); }); it("handles min and maxProperties", () => { @@ -55,8 +60,8 @@ describe("validator", () => { } as JSONSchemaObject; expect(validator(testSchema0, { foo: 123 })).toBe(true); expect(validator(testSchema0, { foo: 123, bar: 123, baz: 123 })).toBe(true); - expect(validator(testSchema0, { foo: 123, bar: 123, baz: 123, boom: 123 })).toBeInstanceOf(ObjectValidationError); - expect(validator(testSchema0, {})).toBeInstanceOf(ObjectValidationError); + expect(validator(testSchema0, { foo: 123, bar: 123, baz: 123, boom: 123 })).toBeInstanceOf(ObjectValidationErrorMaxProperties); + expect(validator(testSchema0, {})).toBeInstanceOf(ObjectValidationErrorMinProperties); }); // it("handles propertyNames", () => {// will fail until decide on subschema validation pattern diff --git a/src/base-validators/object.ts b/src/base-validators/object.ts index e0a3cdb..937e4fb 100644 --- a/src/base-validators/object.ts +++ b/src/base-validators/object.ts @@ -1,57 +1,129 @@ import { JSONSchemaObject } from "@json-schema-tools/meta-schema"; +import ValidationError from "../validation-error"; -export class ObjectValidationError implements Error { +export class ObjectValidationError extends ValidationError { public name = "ObjectValidationError"; - public message: string; - constructor(schema: JSONSchemaObject, data: any, reason: string) { - this.message = [ - "invalid object", - `reason: ${reason}`, - ].join("\n"); + constructor(schema: JSONSchemaObject, data: any, public message: string) { + const msg = [ + "Invalid object value", + message, + ]; + super(schema, data, msg.join("\n")); } } -export default (schema: JSONSchemaObject, d: any): true | ObjectValidationError => { - if (d === null) { - return new ObjectValidationError(schema, d, "null is not a valid object"); +export class ObjectValidationErrorBadType extends ObjectValidationError { + public name = "ObjectValidationErrorBadType"; + + constructor(schema: JSONSchemaObject, data: any) { + const msg = [ + "Value must be of type 'object'", + ]; + + if (data === null) { + msg.push("'null' is not valid as an object type"); + } else if (data instanceof Array) { + msg.push("Arrays are not valid objects"); + } else if (typeof data !== "object") { + msg.push(`Received type: ${typeof data}`); + } else { + msg.push(`${data.constructor.name} is not an object`); + } + + super(schema, data, msg.join('\n')); + } +} + +export class ObjectValidationErrorRequired extends ObjectValidationError { + public name = "ObjectValidationErrorRequired"; + + constructor(schema: JSONSchemaObject, data: any, missing: string[]) { + super(schema, data, [ + "Value must have a property for every item in the field 'required`", + `Missing properties: ${missing.join(", ")}`, + ].join('\n')); + } +} + +export class ObjectValidationErrorMaxProperties extends ObjectValidationError { + public name = "ObjectValidationErrorMaxProperties"; + + constructor(schema: JSONSchemaObject, data: any, numProps: number) { + const max = schema.maxProperties as number; + super(schema, data, [ + `Value must have less than or equal to ${max} properties`, + `Received an object with ${numProps} properties`, + `difference: ${numProps} - ${max} = ${numProps - max}`, + ].join('\n')); } +} + +export class ObjectValidationErrorMinProperties extends ObjectValidationError { + public name = "ObjectValidationErrorMinProperties"; - if (typeof d !== "object") { - return new ObjectValidationError(schema, d, "not an object type"); + constructor(schema: JSONSchemaObject, data: any, numProps: number) { + const min = schema.minProperties as number; + + super(schema, data, [ + `Value must have greater than or equal to ${min} properties`, + `Received an object with ${numProps} properties`, + `difference: ${numProps} - ${min} = ${numProps - min}`, + ].join('\n')); } +} + +export class ObjectValidationErrorDependentRequired extends ObjectValidationError { + public name = "ObjectValidationErrorRequired"; - if (d instanceof Array) { - return new ObjectValidationError(schema, d, "array is not a valid object"); + constructor(schema: JSONSchemaObject, data: any, missing: string[][]) { + const msg = [ + "Missing dependent required properties:", + ]; + + missing.forEach((mDep) => { + msg.push(`the property '${mDep[1]}' depends on the missing property '${mDep[0]}`); + }); + + super(schema, data, msg.join('\n')); } +} + - if (d.constructor.name !== "Object") { - return new ObjectValidationError(schema, d, `${d.constructor.name} is not an object`); +export default (schema: JSONSchemaObject, d: any): true | ObjectValidationError => { + if ( + d === null || + typeof d !== "object" || + d instanceof Array || + d.constructor.name !== "Object" + ) { + return new ObjectValidationErrorBadType(schema, d); } const dKeys = Object.keys(d); if (schema.required) { - const missingKeys = schema.required.filter((reqStr) => dKeys.indexOf(reqStr) === -1); + const missingKeys = schema.required.filter( + (reqStr) => dKeys.indexOf(reqStr) === -1 + ); if (missingKeys.length > 0) { - const errorMsg = `missing required properties on object: ${missingKeys.join(", ")}`; - return new ObjectValidationError(schema, d, errorMsg); + return new ObjectValidationErrorRequired(schema, d, missingKeys); } } if (schema.maxProperties !== undefined) { if (dKeys.length > schema.maxProperties) { - return new ObjectValidationError(schema, d, `too many properties. maxProperties is ${schema.maxProperties}`) + return new ObjectValidationErrorMaxProperties(schema, d, dKeys.length); } } if (schema.minProperties !== undefined) { if (dKeys.length < schema.minProperties) { - return new ObjectValidationError(schema, d, `not enough properties. minProperties is ${schema.minProperties}`) + return new ObjectValidationErrorMinProperties(schema, d, dKeys.length); } } if (schema.dependentRequired) { - const missingDepMap: any = {}; + const missingDepMap: { [k: string]: string[] } = {}; Object.entries(schema.dependentRequired).forEach(([ifKey, reqKeys]: [string, any]) => { if (dKeys.indexOf(ifKey) !== -1) { @@ -68,13 +140,8 @@ export default (schema: JSONSchemaObject, d: any): true | ObjectValidationError const missingDeps = Object.keys(missingDepMap); if (missingDeps.length > 0) { - const errorMsg = [ - "Missing dependent required properties:", - ]; - missingDeps.forEach((mDep) => { - errorMsg.push(`the property ${mDep} is required in the presence of ${missingDepMap[mDep].join(", ")}`) - }); - return new ObjectValidationError(schema, d, errorMsg.join("\n")); + const missing = missingDeps.map((mDep) => [mDep, missingDepMap[mDep].join(", ")]); + return new ObjectValidationErrorDependentRequired(schema, d, missing); } } diff --git a/src/base-validators/string.test.ts b/src/base-validators/string.test.ts index 6404273..12911bc 100644 --- a/src/base-validators/string.test.ts +++ b/src/base-validators/string.test.ts @@ -1,42 +1,55 @@ -import validator, { StringValidationError } from "./string"; +import validator, { + StringValidationError, + StringValidationErrorBadType, + StringValidationErrorConst, + StringValidationErrorEnum, + StringValidationErrorMaxLength, + StringValidationErrorMinLength, + StringValidationErrorPattern +} from "./string"; describe("validator", () => { - it("can validate and invalidate a string", () => { - expect(validator({ type: "string" }, "im a string")).toBe(true); + it("general StringValidationError", () => { expect(validator({ type: "string" }, 123)).toBeInstanceOf(StringValidationError); - expect(validator({ type: "string" }, {})).toBeInstanceOf(StringValidationError); - expect(validator({ type: "string" }, true)).toBeInstanceOf(StringValidationError); - expect(validator({ type: "string" }, false)).toBeInstanceOf(StringValidationError); - expect(validator({ type: "string" }, null)).toBeInstanceOf(StringValidationError); + }); + + it("returns StringValidationBadType errors", () => { + expect(validator({ type: "string" }, "im a string")).toBe(true); + expect(validator({ type: "string" }, 123)).toBeInstanceOf(StringValidationErrorBadType); + expect(validator({ type: "string" }, 123)).toBeInstanceOf(StringValidationErrorBadType); + expect(validator({ type: "string" }, {})).toBeInstanceOf(StringValidationErrorBadType); + expect(validator({ type: "string" }, true)).toBeInstanceOf(StringValidationErrorBadType); + expect(validator({ type: "string" }, false)).toBeInstanceOf(StringValidationErrorBadType); + expect(validator({ type: "string" }, null)).toBeInstanceOf(StringValidationErrorBadType); }); it("maxLength", () => { - expect(validator({ type: "string", maxLength: 10 }, "12345678910")).toBeInstanceOf(StringValidationError); + expect(validator({ type: "string", maxLength: 10 }, "12345678910")).toBeInstanceOf(StringValidationErrorMaxLength); expect(validator({ type: "string", maxLength: 10 }, "123456789")).toBe(true); }); it("minLength", () => { expect(validator({ type: "string", minLength: 10 }, "12345678910")).toBe(true); - expect(validator({ type: "string", minLength: 10 }, "123456789")).toBeInstanceOf(StringValidationError); + expect(validator({ type: "string", minLength: 10 }, "123456789")).toBeInstanceOf(StringValidationErrorMinLength); }); it("pattern", () => { expect(validator({ type: "string", pattern: "foo" }, "foo")).toBe(true); - expect(validator({ type: "string", pattern: "bar" }, "foo")).toBeInstanceOf(StringValidationError); + expect(validator({ type: "string", pattern: "bar" }, "foo")).toBeInstanceOf(StringValidationErrorPattern); }); it("const", () => { expect(validator({ type: "string", const: "foo" }, "foo")).toBe(true); - expect(validator({ type: "string", const: "bar" }, "foo")).toBeInstanceOf(StringValidationError); + expect(validator({ type: "string", const: "bar" }, "foo")).toBeInstanceOf(StringValidationErrorConst); }); it("enum", () => { expect(validator({ type: "string", enum: ["foo"] }, "foo")).toBe(true); - expect(validator({ type: "string", enum: ["bar"] }, "foo")).toBeInstanceOf(StringValidationError); + expect(validator({ type: "string", enum: ["bar"] }, "foo")).toBeInstanceOf(StringValidationErrorEnum); expect(validator({ type: "string", enum: ["foo", "bar"] }, "foo")).toBe(true); - expect(validator({ type: "string", enum: ["foo", "bar"] }, "baz")).toBeInstanceOf(StringValidationError); + expect(validator({ type: "string", enum: ["foo", "bar"] }, "baz")).toBeInstanceOf(StringValidationErrorEnum); - expect(validator({ type: "string", enum: ["foo", 123, "bar"] }, "123")).toBeInstanceOf(StringValidationError); + expect(validator({ type: "string", enum: ["foo", 123, "bar"] }, "123")).toBeInstanceOf(StringValidationErrorEnum); }); }); diff --git a/src/base-validators/string.ts b/src/base-validators/string.ts index 18de740..964fc6d 100644 --- a/src/base-validators/string.ts +++ b/src/base-validators/string.ts @@ -1,50 +1,112 @@ import { JSONSchemaObject } from "@json-schema-tools/meta-schema"; +import ValidationError from "../validation-error"; -export class StringValidationError implements Error { +export class StringValidationError extends ValidationError { public name = "StringValidationError"; - public message: string; - constructor(schema: JSONSchemaObject, data: any, reason: string) { - this.message = [ - "invalid data provided is not a valid string", - `reason: ${reason}`, - ].join("\n"); + constructor(schema: JSONSchemaObject, data: any, message: string) { + const msg = [ + `Invalid string value`, + message, + ]; + super(schema, data, msg.join("\n")); + } +} + +export class StringValidationErrorBadType extends StringValidationError { + public name = "StringValidationErrorBadType"; + + constructor(schema: JSONSchemaObject, data: any) { + super(schema, data, [ + "Value must be of type 'string'", + `Received type: ${typeof data}`, + ].join('\n')); + } +} + +export class StringValidationErrorMaxLength extends StringValidationError { + public name = "StringValidationErrorMaxLength"; + + constructor(schema: JSONSchemaObject, data: string) { + super(schema, data, [ + `Value must have a length less than or equal to ${schema.maxLength}`, + `Received values length: ${data.length}`, + ].join('\n')); + } +} + +export class StringValidationErrorMinLength extends StringValidationError { + public name = "StringValidationErrorMinLength"; + + constructor(schema: JSONSchemaObject, data: string) { + super(schema, data, [ + `Value must have a length greater than or equal to ${schema.minLength}`, + `Received values length: ${data.length}`, + ].join('\n')); + } +} + +export class StringValidationErrorPattern extends StringValidationError { + public name = "StringValidationErrorPattern"; + + constructor(schema: JSONSchemaObject, data: string) { + super(schema, data, `Value must match pattern: ${schema.pattern}`); + } +} + +export class StringValidationErrorConst extends StringValidationError { + public name = "StringValidationErrorConst"; + + constructor(schema: JSONSchemaObject, data: string) { + super(schema, data, `Value must be equal to: ${schema.const}`); + } +} + +export class StringValidationErrorEnum extends StringValidationError { + public name = "StringValidationErrorEnum"; + + constructor(schema: JSONSchemaObject, data: string) { + super( + schema, + data, + `Value must be equal to one of: ${(schema as any).enum.join(", ")}` + ); } } export default (schema: JSONSchemaObject, d: any): true | StringValidationError => { if (typeof d !== "string") { - return new StringValidationError(schema, d, "not a string type"); + return new StringValidationErrorBadType(schema, d); } if (schema.maxLength) { if (d.length > schema.maxLength) { - return new StringValidationError(schema, d, `cannot be longer than maxLength: ${schema.maxLength}`); + return new StringValidationErrorMaxLength(schema, d); } } if (schema.minLength) { if (d.length <= schema.minLength) { - return new StringValidationError(schema, d, `cannot be shorter than minLength: ${schema.minLength}`); + return new StringValidationErrorMinLength(schema, d); } } if (schema.pattern) { const reg = new RegExp(schema.pattern); if (reg.test(d) === false) { - return new StringValidationError(schema, d, `must match pattern: ${schema.pattern}`); + return new StringValidationErrorPattern(schema, d) } } if (schema.const) { if (d !== schema.const) { - return new StringValidationError(schema, d, `must be: ${schema.const}`); + return new StringValidationErrorConst(schema, d); } } if (schema.enum) { if (schema.enum.indexOf(d) === -1) { - return new StringValidationError(schema, d, `must be one of: ${schema.enum}`); + return new StringValidationErrorEnum(schema, d); } } diff --git a/src/index.test.ts b/src/index.test.ts index 09d7857..46ad1b2 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,7 +1,8 @@ -import validator, { ValidationErrors } from "./"; +import validator from "./"; import { JSONSchema } from "@json-schema-tools/meta-schema"; import { ObjectValidationError } from "./base-validators/object"; import { NumberValidationError } from "./base-validators/number"; +import { ValidationErrors } from "./validation-error"; describe("validator", () => { it("is a function", () => { expect(typeof validator).toBe("function"); }); diff --git a/src/index.ts b/src/index.ts index 632d70e..9c804c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,36 +1,20 @@ import { JSONSchema, JSONSchemaObject } from "@json-schema-tools/meta-schema"; -import StringValidator, { StringValidationError } from "./base-validators/string"; -import BooleanValidator, { BooleanValidationError } from "./base-validators/boolean"; -import NumberValidator, { NumberValidationError } from "./base-validators/number"; -import IntegerValidator, { IntegerValidationError } from "./base-validators/integer"; -import ObjectValidator, { ObjectValidationError } from "./base-validators/object"; -import ArrayValidator, { ArrayValidationError } from "./base-validators/array"; - +import StringValidator from "./base-validators/string"; +import BooleanValidator from "./base-validators/boolean"; +import NumberValidator from "./base-validators/number"; +import IntegerValidator from "./base-validators/integer"; +import ObjectValidator from "./base-validators/object"; +import ValidationError, { tValidationError, ValidationErrors } from "./validation-error"; import traverse from "@json-schema-tools/traverse"; import jsonpath from "jsonpath"; -// import all the different validation errors -type ValidationError = - StringValidationError | - BooleanValidationError | - IntegerValidationError | - NumberValidationError | - ObjectValidationError | - ArrayValidationError; - -export class ValidationErrors implements Error { - public name = "ValidationErrors"; - public message: string; - - constructor(public errors: ValidationError[]) { - this.message = ""; - } -} - -const validateItem = (schema: JSONSchema, data: any): true | ValidationError[] => { +const validateItem = (schema: JSONSchema, data: any): true | tValidationError[] => { const errors: ValidationError[] = []; if (typeof schema === "boolean") { + if (schema === false) { + errors.push(new ValidationError(schema, data, "Boolean schema 'false' is always invalid")); + } const valid = BooleanValidator(schema, data); if (valid !== true) { errors.push(valid); @@ -69,6 +53,46 @@ const validateItem = (schema: JSONSchema, data: any): true | ValidationError[] = return errors; }; +// $.properties.foo -> $.foo +// $.items[0].properties.foo -> $[0].foo +// $.properties.foo.items[0].properties.bar -> $.foo[0].bar +const schemaPathToRegularPath = (path: string) => { + const tokens = path.split("."); + const regularPath: string[] = []; + + let pushNext = false; + tokens.forEach((t, i) => { + if (pushNext) { + regularPath[regularPath.length - 1] += `['${t}']`; + pushNext = false; + return; + } + + if (t === "$") { + return regularPath.push(t); + } + + if (t.startsWith("items")) { + const l = regularPath.length - 1; + let toAppend = ""; + if (t.includes("[") && t.includes("]")) { + toAppend = t.replace("items", ""); + } else { + toAppend = "[*]"; + } + + regularPath[l] += toAppend + return; + } + + if (t === "properties") { + pushNext = true; + return; + } + }); + return regularPath.join("."); +}; + /** * A validator is a function is passed a schema and some data to validate against the it. * Errors if your schema contains $refs. Use the json-schema-tools/dereferencer beforehand. @@ -79,51 +103,14 @@ const validateItem = (schema: JSONSchema, data: any): true | ValidationError[] = * */ const validator = (schema: JSONSchema, data: any): true | ValidationErrors => { - // $.properties.foo -> $.foo - // $.items[0].properties.foo -> $[0].foo - // $.properties.foo.items[0].properties.bar -> $.foo[0].bar - const schemaPathToRegularPath = (path: string) => { - const tokens = path.split("."); - const regularPath: string[] = []; - - let pushNext = false; - tokens.forEach((t, i) => { - if (pushNext) { - regularPath.push(t); - pushNext = false; - return; - } - - if (t === "$") { - return regularPath.push(t); - } - - if (t.startsWith("items")) { - const l = regularPath.length - 1; - let toAppend = ""; - if (t.includes("[") && t.includes("]")) { - toAppend = t.replace("items", ""); - } else { - toAppend = "[*]"; - } - - regularPath[l] += toAppend - return; - } - - if (t === "properties") { - pushNext = true; - return; - } - }); - return regularPath.join("."); - }; let errors: ValidationError[] = []; + let errorMap: { [path: string]: ValidationError[] } = {}; traverse(schema, (ss, isCycle, path, parent: JSONSchema) => { const regularPath = schemaPathToRegularPath(path); const [reffed] = jsonpath.query(data, regularPath); + // console.log(path, regularPath, reffed); let result: boolean | ValidationError[] = true; if (reffed === undefined) { @@ -134,7 +121,6 @@ const validator = (schema: JSONSchema, data: any): true | ValidationErrors => { const tokens = path.split("."); const key = tokens[tokens.length - 1]; if (required.includes(key)) { - console.error(new Error("boom")); result = validateItem(ss, reffed); } } @@ -145,11 +131,19 @@ const validator = (schema: JSONSchema, data: any): true | ValidationErrors => { if (result !== true) { errors = errors.concat(result); + + if (errorMap[path] === undefined) { + errorMap[path] = result; + } else { + errorMap[path] = errorMap[path].concat(result); + } } return ss; }); + // console.log(JSON.stringify(errorMap, null, '\t')); + if (errors.length !== 0) { return new ValidationErrors(errors); } @@ -158,3 +152,7 @@ const validator = (schema: JSONSchema, data: any): true | ValidationErrors => { }; export default validator; + +function ArrayValidator(schema: JSONSchemaObject, data: any) { + throw new Error("Function not implemented."); +} diff --git a/src/json-schema-test-suite.test.ts b/src/json-schema-test-suite.test.ts new file mode 100644 index 0000000..2865885 --- /dev/null +++ b/src/json-schema-test-suite.test.ts @@ -0,0 +1,37 @@ +import validator from "./"; +import dereferencer from "@json-schema-tools/dereferencer"; +import { ValidationErrors } from "./validation-error"; +const testSuite = require('@json-schema-org/tests'); + +describe("validating against test cases from JSON-Schema-Test-Suite", () => { + const tss = testSuite.loadSync(); + + beforeAll(async () => { + for (const testSuite of tss) { + for (const testCase of testSuite) { + const dereffer = new dereferencer(testCase.schema); + testCase.schema = await dereffer.resolve(); + } + } + }); + + tss.forEach((ts: any) => { + describe(ts.name, () => { + ts.schemas.forEach((t: any) => { + describe(t.description, () => { + t.tests.forEach((testCase: any) => { + it(`testCase.description ${JSON.stringify(t.schema)} vs. ${JSON.stringify(testCase.data)}`, () => { + const result: any = validator(t.schema, testCase.data); + if (testCase.valid) { + expect(result).toBe(true); + } else { + expect(result).not.toBe(true); + expect(result).toBeInstanceOf(ValidationErrors); + } + }); + }); + }); + }); + }); + }); +}); diff --git a/src/meta-schema.test.ts b/src/meta-schema.test.ts new file mode 100644 index 0000000..39b2981 --- /dev/null +++ b/src/meta-schema.test.ts @@ -0,0 +1,59 @@ +import validator from "./"; +import dereferencer from "@json-schema-tools/dereferencer"; +import schema, { JSONSchema } from "@json-schema-tools/meta-schema"; +import ValidationError, { ValidationErrors } from "./validation-error"; +import { StringValidationError, StringValidationErrorBadType } from "./base-validators/string"; +import { ObjectValidationError, ObjectValidationErrorBadType } from "./base-validators/object"; + +describe("validating JSONSchema with meta-schema", () => { + const passingTests = []; + const failingTests: [any, any[]][] = []; + + passingTests.push({ title: "normalString", type: "string" }); + passingTests.push({ title: "fancyString", type: "string", minLength: 3 }); + + failingTests.push([ + { title: 123, type: {} }, + [ + StringValidationError, + StringValidationErrorBadType + ] + ]); + + failingTests.push([ + { title: 'badProps', type: 'object', properties: 123 }, + [ + ObjectValidationError, + ObjectValidationErrorBadType + ] + ]); + + let s: JSONSchema; + beforeAll(async () => { + const dereffer = new dereferencer(schema); + s = await dereffer.resolve(); + }); + + passingTests.forEach((test) => { + it(`returns true for the valid JSONSchema titled: '${(test as any).title}'`, () => { + const result = validator(s, test); + expect(result).not.toBeInstanceOf(ValidationErrors); + expect(result).toBe(true); + }); + }); + + failingTests.forEach((test) => { + const [testSchema, expectedErrors] = test; + const errNames = expectedErrors.map((e) => e.name).join(", "); + it(`returns ${errNames} for the invalid JSONSchema titled: '${testSchema.title}'`, () => { + const result: any = validator(s, testSchema); + + expectedErrors.forEach((expectedErr) => { + const found = result.errors.filter((err: ValidationError) => err instanceof expectedErr); + expect(found.length).toBeGreaterThan(0); + }); + + expect(result).toBeInstanceOf(ValidationErrors); + }); + }); +}); diff --git a/src/validation-error.ts b/src/validation-error.ts index 90d1a98..44aa92b 100644 --- a/src/validation-error.ts +++ b/src/validation-error.ts @@ -1,13 +1,53 @@ -import { JSONSchemaObject } from "@json-schema-tools/meta-schema"; +import { JSONSchema } from "@json-schema-tools/meta-schema"; +import { StringValidationError } from "./base-validators/string"; +import { BooleanValidationError } from "./base-validators/boolean"; +import { NumberValidationError } from "./base-validators/number"; +import { IntegerValidationError } from "./base-validators/integer"; +import { ObjectValidationError } from "./base-validators/object"; + +export type tValidationError = StringValidationError | BooleanValidationError | NumberValidationError | IntegerValidationError | ObjectValidationError; + +export class ValidationErrors implements Error { + public name = "ValidationErrors"; + public message: string; + + constructor(public errors: ValidationError[]) { + const msg = [ + "JSONSChema Validation Errors" + ]; + + errors.forEach((err) => { + msg.push(""); + msg.push("__________________________"); + msg.push(`**${err.name}**`); + msg.push(""); + msg.push(`${err.message}`); + msg.push("__________________________"); + }); + this.message = msg.join("\n"); + } +} export default class ValidationError implements Error { public name = "ValidationError"; public message: string; - constructor(schema: JSONSchemaObject, data: any, reason: string) { - this.message = [ - `Invalid data provided for the schema: ${(schema.title) ? schema.title : "no title set"}`, - `Reason: ${reason}`, - ].join("\n"); + constructor(schema: JSONSchema, data: any, message: string) { + let msg = []; + + if (schema === true || schema === false) { + if (schema === false) { + msg.push("Boolean schema 'false' is always invalid"); + } + } else { + if (schema.title) { + msg.push(`Validation Error for schema titled: "${schema.title}"`); + } else { + msg.push(`Validation Error`); + } + } + msg.push(message); + msg.push(`Received value: ${data}`); + this.message = msg.join("\n"); } }