garn-validator
Ultra fast runtime type validator without dependencies.
Features
- Supports checking primitives or objects with schemas
- Ultra light and fast with 0 dependencies
- Easy to use and simple to learn but powerful
- 6 behaviors:
hasErrors
,isValid
,isValidOrThrow
,isValidOrLog
,isValidOrLogAll
andisValidOrThrowAll
. - Works with ESModules or CommonJS from Node 10.x or Deno
- Works in all frontend frameworks under babel (React, Angular, Vue, etc…)
Contents
- Get started
- In depth
- Roadmap
- Examples - schema.test.js - custom-validator.test.js - errors.test.js
Get started
Node
npm install garn-validator
Import with ES Modules
// default export is isValidOrThrow
import isValidOrThrow from "garn-validator";
// or use named exports
import { isValidOrThrow } from "garn-validator";
Require with CommonJs
const { isValidOrThrow } = require("garn-validator/commonjs");
// or use de default export
const isValidOrThrow = require("garn-validator/commonjs").default;
Deno
The library can be used as is in typescript
Import from deno third party modules: deno.land/x/garn_validator
// mod.ts
import is from "https://deno.land/x/garn_validator/src/index.js";
Basic Usage
import is from "garn-validator"; // default export is isValidOrThrow
const isValidUser = is({ name: String, age: Number });
isValidUser({ name: "garn", age: 38 }); // true
isValidUser({ name: "garn", age: "38" }); // it throws
Check against constructor
is(Number)(2); // true
is(String)(2); // it throws
is(Array)([1, 2]); // true
is(Object)([1, 2]); // it throws
Check against primitive
is("a")("a"); // true
is(true)(false); // it throws
Check string against regex
is(/a*/)("a"); // true
is(/a/)("b"); // it throws
Check against custom function
is((value) => value > 0)(33); // true
is((value) => value > 0)(-1); // wil throw
is(Number.isNaN)(NaN); // true
is(Number.isInteger)(1.1); // wil throw
Check against enums (OR operator)
is(["a", "b"])("a"); // true
is(["a", "b"])("c"); // it throws
is([Number, String])("18"); // true
is([null, undefined, false, 0, ""])(18); // it throws
Check multiple validations (AND operator)
is(Array, (array) => array.length === 2)([1, 2]); // true
is(
(v) => v > 0,
(v) => v < 50
)(100); // it throws
Check object against an schema
const schema = { a: Number, b: Number }; // a and b are required
const obj = { a: 1, b: 2 };
is(schema)(obj); // true
is({ a: 1 })({ a: 1, b: 2 }); // true, a must be 1
is({ c: Number })({ a: 1, b: 2 }); // it throws (c is missing)
// Optional keys
is({ x$: String })({}); // true
Composable
// Simple example
const isPositive = is((v) => v > 0);
const isNotBig = is((v) => v < 100);
isPositive(-2); // it throws
is(isPositive, isNotBig)(200); // it throws
// Real example
const isValidPassword = is(
String,
(str) => str.length >= 8,
/[a-z]/,
/[A-Z]/,
/[0-9]/,
/[-_/!·$%&/()]/
);
const isValidName = is(String, (name) => name.length >= 3);
const isValidAge = is(
Number,
(age) => age > 18,
(age) => age < 40
);
const isValidUser = is({
name: isValidName,
age: isValidAge,
password: isValidPassword,
country: ["ES", "UK"],
});
isValidUser({
name: "garn",
age: 38,
password: "1234", // incorrect
country: "ES",
}); // it throws
Behaviors
There are 6 behaviors that can be divided in 2 categories:
- Stop in first Error:
isValidOrThrow
(returns true of throw the first error found)isValid
(returns true or false, never throws)isValidOrLog
(returns true or false and log first error, never throws)
- Collect all Errors:
hasErrors
(return null or array of errors, never throws)isValidOrLogAll
(returns true or false and log all errors, never throws)isValidOrThrowAll
(returns true or throw AggregateError with all errors found)
The default export is isValidOrThrow
Learn more at errors.test.js
import { isValid } from "garn-validator";
// stops in first Error
isValid(/[a-z]/)("g"); // returns true
isValid(/[a-z]/)("G"); // returns false, doesn't throws
import { isValidOrLog } from "garn-validator";
// stops in first Error
isValidOrLog(/[a-z]/)("g"); // do nothing (but also returns true)
isValidOrLog(/[a-z]/)("G"); // logs error and return false
import { hasErrors } from "garn-validator";
// return null or array or errors
// checks until the end
hasErrors(/[a-z]/)("g"); // null
hasErrors(/[a-z]/, Number)("G"); // [TypeError, TypeError]
In depth
Types of validations
There are 6 types of validations: Primitives, Constructors, RegExp, Enums, Schemas and Custom functions
Primitives
Checking a primitive is a === comparison
Anything that is not and object in JS is a primitive: Number
, String
, undefined
, null
and Symbol
is(1)(1); // true 1 === 1
is("1")(1); // throws '1' !== 1
is(1n)(1); // throws 1n !== 1
is(undefined)(null); // throws undefined !== null
// keep in mind than a symbol is only equal to itself
let s = Symbol();
is(s)(s); // true
is(s)(Symbol()); // throws
Constructors
Checking against a constructor means to know if the value evaluated has been created from that constructor
is(Number)(2); // (2).constructor === Number --> true
is(Symbol)(Symbol()); // true
A valid constructor is a class
or any built-in constructors.
class Car {}
let honda = new Car();
is(Car)(honda); // honda.constructor === Car --> true
You cannot use a normal function used as constructor from the old JS times.
function Car(name) { this.name = name}
let honda = new Car('honda');
is(Car)(honda); // throws. Car is detected as custom validator function
All built in Constructors are supported
RegExp
The perfect validator to check strings. It does what you expect:
let isLowerCased = is(/^[a-z]+$/);
isLowerCased("honda"); // /^[a-z]+$/.test('honda') --> true
// or building a regexp with the constructor RexExp;
is(new RegExp(/^[a-z]+$/))("honda"); // true
Custom function
Any function that is not a constructor is treated as custom validator
It must return any truthy value to pass the validation.
is((val) => val >= 0)(10); // true
is((val) => val >= 0)(-10); // throws
is(() => "I am truthy")(10); // true
is(() => [])(10); // true
To fail a validation may return falsy or throw an error.
if it returns a falsy value the default error will be thrown: TypeError
if it throws an error that error will be thrown.
is( () => false ) (10); // throws TypeError
is( () => 0 ) (10); // throws TypeError
is( () => {
throw new RangeError('ups);
} ) (10); // throws RangeError
is( () => {
throw 'ups';
} ) (10); // throws 'ups'
Enums
Enums works as OR operator. Must be an array which represent all options.
let cities = ["valencia", "new york", "salzburg"];
is(cities)("valencia"); // true
is(cities)("madrid"); // throws
But is much more powerful than checking against primitives. It can contain any type of validator (Primitives, Constructors, RegExp, Enums, Schemas and Custom functions).
It check every item until one passes.
let isNumberOrBigInt = [Number, BigInt];
is(isNumberOrBigInt)(1n); // true
is(isNumberOrBigInt)(1); // true
let isFalsy = [0, "", null, undefined, false];
is(isFalsy)(""); // true
let isNumberAlike = [Number, (val) => val === Number(val)];
is(isNumberAlike)(1n); // true
is(isNumberAlike)(1); // true
is(isNumberAlike)("1"); // true
Schema
An Schema is just a plain object telling in each key which validation must pass.
let schema = {
name: String, // required and be a Number
age: (age) => age > 18, // required and be a greater than 18
tel: [Number, String], // required and be Number or String
role: ["admin", "user"], // required and be 'admin' or 'user'
credentials: {
// must be and object and will be validate with this "subSchema"
pass: String,
email: String,
},
};
let obj = {
name: "garn",
age: 20,
tel: "+34617819234",
role: "admin",
credentials: {
pass: "1234",
email: "email@example.com",
},
};
is(schema)(obj); // true
Only the keys in the schema will be checked. Any key not present in the schema won’t be checked
is({})({a:1}); // true , a is not in the schema
Optional Keys
And optional key must be undefined
, null
, or pass the validation
is({ x$: Number })({ x: 1 }); // true, x is present and is Number
is({ x$: String })({ x: 1 }); // it throws, x is present but is not String
is({ x$: String })({}); // true, x is undefined
is({ x$: String })({ x: undefined}); // true, x is undefined
is({ x$: String })({ x: null }); // true, x is null
You can use key$
or 'key?'
. It would be nicer to have key?
without quotes but is not valid JS
is({ "x?": String })({}); // true
Regexp keys
You can validate multiple keys at once using a regexp key
is({
[/./]: String
})({
a: 'a',
b: 'b',
}); // true
// or write it as plain string
is({
'/./': String
})({
a: 'a',
b: 1, // fails
}); // throws
// only checks the keys that matches regex
is({
[/^[a-z]+$/]: Number,
})({
x: 1,
y: 2,
z: 3,
CONSTANT: "foo", // not checked
}); // true, all lowercased keys are numbers
The required keys and optional won’t be check against a regexp key
is({
[/./]: Number,
x: String, // this required key has priority against regex key
})({
x:'x' // not checked as Number, checked as String
}); // true, x is String
is({
[/./]: Number,
x: String,
})({
x:'x', // not checked as Number, checked as String
y:'y', // checked as Number, fails
}); // throw
is({
[/./]: Number,
$x: String,
y: String,
})({
x:'x', // not checked as Number, checked as String
y:'y', // checked as String
z:1 // checked as Number, fails
}); // throw
This feature is perfect to note that any key not specified in schema is not allowed
is({
x: String,
[/./]: () => false,
})({
x:'x',
y: 'y', // fails
});
Custom validation used in schemas
When using a custom validator inside an schema will be run with 3 arguments: (value, root, keyName) => {}
- value: the value present in that key from the object
- root: the whole object, not matter how deep the validation occurs
- keyName: the name of the key to be checked.
// against root obj
is({
max: (val, root, keyName) => val > root.min,
min: (val, root, keyName) => val < root.max,
})({
max: 1,
min: -1,
}); // true
is({
max: (val, root, keyName) => val > root.min,
min: (val, root, keyName) => val < root.max,
})({
max: 10,
min: 50,
}); // it throws
// all key must be at least 3 characters
is({
[/./]: (val, root, keyName) => keyName.length > 3,
})({
max: 1, // key too short
longKey: 1, // valid key
}); // it throws, max key is too short
Errors
If a validation fails by default it will throw new TypeError(meaningfulMessage)
;
If using a custom validator throws an error , that error will be thrown.
SchemaValidationError using isValidOrThrowAll
If more than one key fails checking an Schema , it will throw a SchemaValidationError with all Errors aggregated in error.errors.
If only one key fail it will throw only that Error (not an AggregateError)
SchemaValidationError inherits from AggregateError,
But if using
isValidOrThrow
only the first Error will be thrown.
// more than 2 keys fails
try {
isValidOrThrowAll({ a: 1, b: 2 })({});
} catch (error) {
console.log(error instanceof SchemaValidationError); // true
console.log(error instanceof AggregateError); // true
console.log(error.errors.length); // 2
}
// only 1 key fails
try {
isValidOrThrowAll({ a: 1 })({});
} catch (error) {
console.log(error instanceof TypeError); // true
console.log(error instanceof SchemaValidationError); // false
}
EnumValidationError
If fails all items of an enum, it will throw a EnumValidationError with all Errors aggregated in error.errors
But if the length of the enum is 1, it will throw only that error.
EnumValidationError inherits from AggregateError.
try {
isValidOrThrow([Boolean, String])(1);
} catch (error) {
console.log(error instanceof EnumValidationError); // true
console.log(error instanceof AggregateError); // true
}
try {
isValidOrThrow([Boolean])(1);
} catch (error) {
console.log(error instanceof EnumValidationError); // false
console.log(error instanceof TypeError); // true
}
SeriesValidationError using isValidOrThrowAll
If fails all items of a serie of validations, it will throw a SeriesValidationError with all Errors aggregated in error.errors
But if the length of the enum is 1. it will throw only this error.
SeriesValidationError inherits from AggregateError.
try {
isValidOrThrowAll(Boolean, String)(1);
} catch (error) {
console.log(error instanceof SeriesValidationError); // true
console.log(error instanceof AggregateError); // true
}
try {
isValidOrThrowAll(Boolean)(1);
} catch (error) {
console.log(error instanceof SeriesValidationError); // false
console.log(error instanceof TypeError); // true
}
hasErrors
hasError will flatMap all errors found. No AggregateError will be in the array returned.
Especial cases
AsyncFunction & GeneratorFunction
AsyncFunction
and GeneratorFunction
constructors are not in the global scope of any of the 3 JS environments (node, browser or node). If you need to check an async function or a generator you can import them from garn-validator.
Note: Async functions and generators are not normal function, so it will fail against Function constructor
import is, { AsyncFunction, GeneratorFunction } from "garn-validator";
is(AsyncFunction)(async () => {}); // true
is(GeneratorFunction)(function* () {}); // true
is(Function)(function* () {}); // throws
is(Function)(async function () {}); // throws
arrayOf
As we use the array []
as enum, if you need to check the items of an array you should treat it as an object and check against an schema.
import is from "garn-validator";
is(Array, { [/\d/]: Number })([1, 2, 3]); // true
is(Array, { [/\d/]: Number })([1, 2, "3"]); // throws
To not be so ugly you can import arrayOf
from garn-validator as a shortcut to:
export const arrayOf = type => isValid(Array, {[/^\d$/]: type})
import is, { arrayOf } from "garn-validator";
is(arrayOf(Number))([1, 2, 3]); // true
is(arrayOf(Number))([1, 2, "3"]); // throws
objectOf
You can import objectOf
from garn-validator as a shortcut to:
export const objectOf = type => isValid(Object, {[/./]: type})
import is, { objectOf } from "garn-validator";
is(objectOf(Number))({ a: 1, b: 2 }); // true
is(objectOf(Number))({ a: 1, b: "2" }); // throws
Roadmap
- Check value by constructor
- Enum type
- Shape type
- Custom validation with a function (value, root, keyName)
- Check RegEx
- Match object key by RegEx
- Multiples behaviors
- ArrayOf & objectOf
- Multiples validations
isValid(String, val => val.length > 3, /^[a-z]+$/ )('foo')
- Schema with optionals key
{ 'optionalKey?': Number }
or{ optionalKey$: Number }
- Setting for check all keys (no matter if it fails) and return (or throw) an array of errors
- Support for deno
- behavior applyDefaultsOnError
- Async validation support
- More built-in utils functions (containsText, startWith, endsWith, min, max, isLowercase, isUppercase, …)
Examples
Watch folder tests to learn more.
schema.test.js
import isValidOrThrow, { isValid, objectOf, arrayOf } from "garn-validator";
describe("check schema", () => {
test("check with constructor", () => {
expect(() => {
isValidOrThrow({ a: Number })({
a: 1,
b: 2,
}); // not throw, all ok
}).not.toThrow();
expect(() => {
isValidOrThrow({ a: Number, c: Number })({
a: 1,
b: 2,
});
}).toThrow();
expect(() => {
isValidOrThrow({ a: Number, c: undefined })({
a: 1,
b: 2,
});
}).not.toThrow();
});
test("keys on the schema are required", () => {
expect(() => {
isValidOrThrow({ a: 1 })({ a: 1, b: 2 });
}).not.toThrow();
expect(() => {
isValidOrThrow({ c: 1 })({ a: 1, b: 2 });
}).toThrow();
});
test("check with primitives", () => {
expect(() => {
isValidOrThrow({ a: 2 })({
a: 1,
b: 2,
});
}).toThrow();
expect(() => {
isValidOrThrow({ a: 1 })({
a: 1,
b: 2,
});
}).not.toThrow();
});
test("check with custom function", () => {
expect(() => {
isValidOrThrow({ a: (val) => val < 0 })({
a: 1,
b: 2,
});
}).toThrow();
expect(() => {
isValidOrThrow({ a: (val) => val > 0 })({
a: 1,
b: 2,
});
}).not.toThrow();
});
test("check with custom function", () => {
let obj = { x: "x", y: "x" };
expect(() => {
isValidOrThrow({ x: (val, rootObject) => rootObject.y === val })(obj);
}).not.toThrow();
expect(() => {
isValidOrThrow({
max: (val, rootObject) => val > rootObject.min,
min: (val, rootObject) => val < rootObject.max,
})({
max: 1,
min: -1,
});
}).not.toThrow();
expect(() => {
isValidOrThrow({
max: (val, rootObject) => val > rootObject.min,
min: (val, rootObject) => val < rootObject.max,
})({
max: 1,
min: 10,
});
}).toThrow();
expect(() => {
isValidOrThrow({
"/./": (val, _, keyName) => keyName === val,
})({
x: "x",
y: "y",
});
}).not.toThrow();
expect(() => {
isValidOrThrow({
"/./": (val, _, keyName) => keyName === val,
})({
x: "x",
y: "x",
});
}).toThrow();
});
test("match key with regex", () => {
expect(() => {
isValidOrThrow({ [/[a-z]/]: Number })({
a: 1,
b: 2,
});
}).not.toThrow();
expect(() => {
isValidOrThrow({ [/[a-z]/]: 0 })({
a: 1,
b: 2,
});
}).toThrow();
expect(() => {
// only throws if the key is matched
isValidOrThrow({ [/[A-Z]/]: Number })({
a: 1,
b: 2,
});
}).not.toThrow();
expect(() => {
isValidOrThrow({ [/[a-z]/]: Number, a: 1 })({
a: 1,
b: 2,
}); // not throw, all lowercase keys are numbers
}).not.toThrow();
expect(() => {
isValidOrThrow({ [/[a-z]/]: Number, a: 2 })({
a: 1,
b: 2,
}); // will throw (a is not 2)
}).toThrow();
});
});
describe("check objects recursively", () => {
const obj = {
a: 1,
deep: {
x: "x",
deeper: {
y: "y",
method: (v) => console.log(v),
user: {
name: "garn",
city: {
name: "narnia",
cp: 46001,
country: "ESP",
},
},
},
},
};
const schema = {
a: Number,
deep: {
x: (val, root, key) => key === val,
deeper: {
y: "y",
method: Function,
user: {
name: String,
city: {
name: (v) => v.length > 3,
cp: [Number, String],
country: ["ESP", "UK"],
},
},
},
},
};
test("should work", () => {
expect(() => {
isValidOrThrow(schema)(obj); // not throw, all ok
}).not.toThrow();
});
test("should throw", () => {
expect(() => {
isValidOrThrow({ ...schema, a: String })(obj);
}).toThrow();
});
});
describe("optional keys", () => {
test("if the key exists should be check", () => {
expect(isValid({ a$: Number })({ a: 1 })).toBe(true);
expect(isValid({ a$: String })({ a: 1 })).toBe(false);
});
test("if the key doesn't exists should be valid", () => {
expect(isValid({ a$: Number })({})).toBe(true);
});
test("if the key is null or undefined should be valid", () => {
expect(isValid({ a$: Number })({a:undefined})).toBe(true);
expect(isValid({ a$: Number })({a:null})).toBe(true);
});
test("should work ending with $ or ?", () => {
expect(isValid({ "a?": Number })({ a: 1 })).toBe(true);
expect(isValid({ "a?": String })({ a: 1 })).toBe(false);
});
test("complex example should work", () => {
expect(
isValid({
a$: Number,
b: 2,
c$: (v, r, key) => key === "c",
d$: String,
})({
a: 1,
b: 2,
c: true,
})
).toBe(true);
});
test("complex example should fail", () => {
expect(
isValid({
a$: Number,
b: 2,
c$: (v, r, key) => key === "c",
d$: String,
})({
a: true,
b: 2,
c: true,
})
).toBe(false);
});
});
describe("special cases", () => {
test("required keys are more important than optional", () => {
expect(() => {
isValidOrThrow({
a: String,
a$: Number,
})({
a: "2",
});
}).not.toThrow();
});
test("required regExp keys do not check optional or required", () => {
expect(() => {
isValidOrThrow({
a: String,
a$: Number,
[/a/]: Boolean,
})({
a: "2",
});
}).not.toThrow();
expect(() => {
isValidOrThrow({
a: String,
a$: Number,
[/a/]: Boolean,
})({
a: "2",
aa: 12,
});
}).toThrow();
});
});
describe('check Array against an schema', () => {
test("should check an Array as an object", () => {
expect(() => {
isValidOrThrow({
0: Number,
1: Number,
})([1, 2]);
}).not.toThrow();
expect(() => {
isValidOrThrow({
"/d/": Number,
})([1, 2]);
}).not.toThrow();
expect(() => {
isValidOrThrow({
0: String,
})([1, 2]);
}).toThrow();
});
});
describe("check String against an schema", () => {
test("should check an string as an object", () => {
expect(() => {
isValidOrThrow({
0: /[lL]/,
1: (char) => char === "o",
})("Lorem");
}).not.toThrow();
expect(() => {
isValidOrThrow({
0: /[lL]/,
1: (char) => char === "o",
2: "R",
})("Lorem");
}).toThrow();
expect(() => {
isValidOrThrow({
99: "a",
})("Lorem");
}).toThrow();
});
});
describe("check a function against an schema", () => {
test("should check an function as an object", () => {
let fn = function () {};
expect(() => {
isValidOrThrow({
toString: Function,
})(fn);
}).not.toThrow();
expect(() => {
isValidOrThrow({
toString: Boolean,
})(fn);
}).toThrow();
});
});
describe("arrayOf", () => {
test("should work", () => {
expect(() => {
isValidOrThrow(arrayOf(Number))([1, 2, 3]);
}).not.toThrow();
expect(() => {
isValidOrThrow(arrayOf((n) => n > 0))([1, 2, 3]);
}).not.toThrow();
});
test("should throw", () => {
expect(() => {
isValidOrThrow(arrayOf(Number))([1, 2, "3"]);
}).toThrow();
expect(() => {
isValidOrThrow(arrayOf((n) => n > 0))([1, 2, -3]);
}).toThrow();
expect(() => {
isValidOrThrow(arrayOf(Number))({ 0: 1, 1: 2 });
}).toThrow();
});
});
describe("objectOf", () => {
test("should work", () => {
expect(() => {
isValidOrThrow(objectOf(Number))({ a: 1, b: 2 });
}).not.toThrow();
expect(() => {
isValidOrThrow(objectOf((n) => n > 0))({ a: 1, b: 2 });
}).not.toThrow();
});
test("should throw", () => {
expect(() => {
isValidOrThrow(objectOf(Number))({ a: 1, b: "2" });
}).toThrow();
expect(() => {
isValidOrThrow(objectOf((n) => n > 0))({ a: 1, b: -2 });
}).toThrow();
});
});
describe("should check instances", () => {
class MyClass {
constructor() {
this.date = new Date();
this.name = "Garn";
this.valid = false;
}
}
test("should work", () => {
expect(() => {
isValidOrThrow({
date: Date,
name: String,
valid: Boolean,
})(new MyClass());
}).not.toThrow();
});
test("should throw", () => {
expect(() => {
isValidOrThrow({
date: Date,
name: String,
valid: Number,
})(new MyClass());
}).toThrow();
expect(() => {
isValidOrThrow(Object, {
date: Date,
name: String,
valid: Boolean,
})(new MyClass());
}).toThrow();
});
});
custom-validator.test.js
import isValidOrThrow from "garn-validator";
describe("check with custom validator", () => {
test("you can return true or false", () => {
expect(() => {
isValidOrThrow(() => true)(33);
}).not.toThrow();
expect(() => {
isValidOrThrow(() => false)(33);
}).toThrow();
});
test("you can throw a custom message", () => {
try {
isValidOrThrow(() => {
throw "ups";
})(33);
throw 'mec'
} catch (error) {
expect(error).toBe('ups');
}
});
test("by default throws TypeError", () => {
try {
isValidOrThrow(Boolean)(33);
throw 'mec'
} catch (error) {
expect(error).toBeInstanceOf(TypeError)
}
});
test("you can throw a custom type of error", () => {
try {
isValidOrThrow((v) => {
if (v > 10) throw new RangeError("ups");
})(33);
throw 'mec'
} catch (error) {
expect(error).toBeInstanceOf(RangeError)
expect(error).not.toBeInstanceOf(TypeError)
}
});
});
errors.test.js
import {
isValidOrThrow,
hasErrors,
isValidOrLogAll,
isValidOrThrowAll,
SchemaValidationError,
EnumValidationError,
SeriesValidationError,
TypeValidationError
} from "garn-validator";
describe("AggregateError", () => {
test.each([AggregateError, SchemaValidationError, Error])(
"if schema fails with more than 2 errors should throw %p",
(ErrorType) => {
expect(() => {
isValidOrThrowAll({ a: Number, b: String })({});
}).toThrow(ErrorType);
}
);
test.each([AggregateError, EnumValidationError, Error])(
"if enums fails with more than 2 errors should throw %p",
(ErrorType) => {
expect(() => {
isValidOrThrowAll([Number, String])(true);
}).toThrow(ErrorType);
}
);
test.each([AggregateError, SeriesValidationError, Error])(
"if Series fails with more than 2 errors should throw %p",
(ErrorType) => {
expect(() => {
isValidOrThrowAll(Number, String)(true);
}).toThrow(ErrorType);
}
);
test("checking schema should throw SchemaValidationError or TypeValidationError", () => {
try {
isValidOrThrowAll({ a: 1, b: 2 })({});
} catch (error) {
expect(error instanceof SchemaValidationError).toBe(true);
expect(error instanceof AggregateError).toBe(true);
expect(error.errors.length).toBe(2);
}
try {
isValidOrThrow({ a: 1, b: 2 })({});
} catch (error) {
expect(error instanceof SchemaValidationError).toBe(false);
expect(error instanceof TypeValidationError).toBe(true);
}
// only 1 key fails
try {
isValidOrThrowAll({ a: 1 })({});
} catch (error) {
expect(error instanceof TypeValidationError).toBe(true);
expect(error instanceof SchemaValidationError).toBe(false);
}
});
test("checking enum should throw EnumValidationError or TypeValidationError", () => {
try {
isValidOrThrow([Boolean, String])(1);
} catch (error) {
expect(error instanceof EnumValidationError).toBe(true);
expect(error instanceof AggregateError).toBe(true);
}
try {
isValidOrThrow([Boolean])(1);
} catch (error) {
expect(error instanceof EnumValidationError).toBe(false);
expect(error instanceof TypeValidationError).toBe(true);
}
});
test("checking series should throw SeriesValidationError or TypeValidationError ", () => {
try {
isValidOrThrowAll(Boolean, String)(1);
} catch (error) {
expect(error instanceof SeriesValidationError).toBe(true);
expect(error instanceof AggregateError).toBe(true);
}
try {
isValidOrThrowAll(Boolean)(1);
} catch (error) {
expect(error instanceof SeriesValidationError).toBe(false);
expect(error instanceof TypeValidationError).toBe(true);
}
});
});
describe("check with invalid validator", () => {
test("should detect async functions", () => {
try {
isValidOrThrow(async () => false)(1);
throw "mec";
} catch (error) {
expect(error).toBeInstanceOf(SyntaxError);
}
});
test("should detect generators", () => {
try {
isValidOrThrow(function* () {})(1);
throw "mec";
} catch (error) {
expect(error).toBeInstanceOf(SyntaxError);
}
});
});
describe("check errors", () => {
test("by default throws TypeValidationError", () => {
expect(() => {
isValidOrThrow(Boolean)(33);
}).toThrow(TypeValidationError);
});
test("Should throw meaningfully message", () => {
expect(() => {
isValidOrThrow(1)(33);
}).toThrow("value 33 do not match primitive 1");
});
test("should throw a custom type of error", () => {
expect(() => {
isValidOrThrow((v) => {
if (v > 10) throw new RangeError("ups");
})(33);
}).toThrow(RangeError);
});
test("should throw a custom type of error", () => {
expect(() => {
isValidOrThrow((v) => {
if (v > 10) throw new RangeError("ups");
})(33);
}).toThrow("ups");
});
test("should throw anything", () => {
try {
isValidOrThrow((v) => {
if (v > 10) throw "ups";
})(33);
} catch (error) {
expect(error).toBe("ups");
}
});
test("should throw anything", () => {
try {
isValidOrThrowAll(
() => {
throw 1;
},
() => {
throw 2;
}
)(33);
} catch (error) {
// error is AggregateError
expect(error).toBeInstanceOf(AggregateError);
expect(error.errors).toEqual([1, 2]);
}
});
test("should format the schema", () => {
expect(() => {
isValidOrThrow({ a: Number })(33);
}).toThrow('value 33 do not match schema {"a":Number}');
});
test("should format the value", () => {
expect(() => {
isValidOrThrow({ a: Number })({ b: 33 });
}).toThrow("on path /a value undefined do not match constructor Number");
});
});
describe("check errors in serie", () => {
test("should throw the error message related to the check failed", () => {
expect(() => {
isValidOrThrow(Number, String)(2);
}).toThrow("value 2 do not match constructor String");
});
test("should throw the error message related to the check failed", () => {
expect(() => {
isValidOrThrow(() => {
throw new Error();
}, String)(2);
}).toThrow(Error);
});
test("should check only until the first check fails", () => {
jest.spyOn(globalThis.console, "log");
try {
isValidOrThrow(
() => {
throw new Error();
},
() => console.log("I run?")
)(2);
} catch (err) {}
expect(globalThis.console.log).not.toHaveBeenCalled();
});
});
describe("checking enums", () => {
test("should throw AggregateError (EnumValidationError) if none pass", () => {
try {
isValidOrThrow([
() => {
throw "ups";
},
String,
])(1);
throw "mec";
} catch (error) {
expect(error).toBeInstanceOf(EnumValidationError);
expect(error.message).toMatch('enum');
expect(error).toBeInstanceOf(AggregateError);
}
});
});
describe("hasErrors", () => {
test("should return null", () => {
expect(
hasErrors({ num: Number, str: String })({ num: 2, str: "str" })
).toBe(null);
});
test("should return array of errors", () => {
expect(
hasErrors({ num: Number, str: String })({ num: "2", str: "str" })
).toEqual([
new TypeValidationError('on path /num value "2" do not match constructor Number'),
]);
});
test("should flat all aggregated Errors", () => {
expect(hasErrors(Number, { x: 1 }, () => false)(true).length).toBe(3);
});
test("should flat all aggregated Errors", () => {
expect(hasErrors(Number, { x: 1, y: 2 }, [1, 2])({}).length).toBe(5);
});
describe("in serie", () => {
test.each([
[Number, (v) => v > 0, 2, null],
[
Number,
(v) => v > 100,
2,
[new TypeValidationError("value 2 do not match validator v=>v>100")],
],
[
String,
(v) => v > 100,
2,
[
new TypeValidationError("value 2 do not match constructor String"),
new TypeValidationError("value 2 do not match validator v=>v>100"),
],
],
])("hasErrors(%p,%p)(%p) === %p", (a, b, input, expected) => {
expect(hasErrors(a, b)(input)).toEqual(expected);
});
});
describe("in schema", () => {
test.each([
[{ num: Number }, { num: 2 }, null],
[{ num: Number, str: String }, { num: 2, str: "str" }, null],
])(
"should return null : hasErrors(%p)(%p) === %p",
(schema, obj, expected) => {
expect(hasErrors(schema)(obj)).toEqual(expected);
}
);
test.each([
[
{ num: Number, str: String },
{ num: "2", str: "str" },
[new TypeValidationError('on path /num value "2" do not match constructor Number')],
],
[
{ num: Number, str: String },
{ num: "2", str: null },
[
new TypeValidationError('on path /num value "2" do not match constructor Number'),
new TypeValidationError("on path /str value null do not match constructor String"),
],
],
])(
"should return array of errors hasErrors(%p)(%p) === %p",
(schema, obj, expected) => {
expect(hasErrors(schema)(obj)).toEqual(expected);
}
);
});
describe("in recursive schema", () => {
test.each([
[{ obj: { num: Number } }, { obj: { num: 2 } }],
[{ obj: { num: Number, str: String } }, { obj: { num: 2, str: "str" } }],
])("should return null : hasErrors(%p)(%p) === %p", (schema, obj) => {
expect(hasErrors(schema)(obj)).toEqual(null);
});
test.each([
[
{ obj: { num: Number, str: String } },
{ obj: { num: "2", str: "str" } },
[new TypeValidationError('on path /obj/num value "2" do not match constructor Number')],
],
[
{
thr: () => {
throw new RangeError("ups");
},
},
{ thr: 1 },
[new RangeError("ups")],
],
[
{ obj: { num: Number, str: String } },
{ obj: { num: "2", str: null } },
[
new TypeValidationError('on path /obj/num value "2" do not match constructor Number'),
new TypeValidationError("on path /obj/str value null do not match constructor String"),
],
],
])(
"should return array of errors hasErrors(%p)(%p) === %p",
(schema, obj, expected) => {
expect(hasErrors(schema)(obj)).toEqual(expected);
}
);
});
describe("complex schema", () => {
const schema = {
name: /^[a-z]{3,}$/,
age: (age) => age > 18,
car: {
brand: ["honda", "toyota"],
date: Date,
country: {
name: String,
},
[/./]: () => {
throw new EvalError("unexpected key");
},
},
optional$: true,
[/./]: () => false,
};
test("should return null ", () => {
const obj = {
name: "garn",
age: 19,
optional: true,
car: {
brand: "honda",
date: new Date("1982-01-01"),
country: {
name: "Japan",
},
},
};
expect(hasErrors(schema)(obj)).toEqual(null);
});
test("should return errors", () => {
const obj = {
name: "Garn",
age: 18,
optional: false,
car: {
brand: "Honda",
date: "1982-01-01",
country: {
NAME: "Japan",
},
evalError: null,
},
noValidKey: 1,
};
expect(hasErrors(schema)(obj)).toEqual([
new TypeValidationError(
"on path /noValidKey value 1 do not match validator ()=>false"
),
new TypeValidationError(
'on path /name value "Garn" do not match regex /^[a-z]{3,}$/'
),
new TypeValidationError("on path /age value 18 do not match validator age=>age>18"),
new EvalError("unexpected key"),
new TypeValidationError(
'on path /car/brand value "Honda" do not match primitive "honda"'
),
new TypeValidationError(
'on path /car/brand value "Honda" do not match primitive "toyota"'
),
new TypeValidationError(
'on path /car/date value "1982-01-01" do not match constructor Date'
),
new TypeValidationError(
"on path /car/country/name value undefined do not match constructor String"
),
new TypeValidationError("on path /optional value false do not match primitive true"),
]);
});
});
describe("multiples schemas in series", () => {
test("should return errors", () => {
const schema1 = {
x: Number,
};
const schema2 = {
y: Boolean,
z: Function,
};
const obj = {
x: true,
y: 1,
};
expect(hasErrors(schema1, schema2)(obj)).toEqual([
new TypeValidationError("on path /x value true do not match constructor Number"),
new TypeValidationError("on path /y value 1 do not match constructor Boolean"),
new TypeValidationError("on path /z value undefined do not match constructor Function"),
]);
});
});
});
describe("isValidOrThrowAll ", () => {
jest.spyOn(globalThis.console, "error");
test("should throw AggregateError with all errors", () => {
try {
isValidOrThrowAll(Number, String)(true);
throw "ups";
} catch (error) {
expect(error).toBeInstanceOf(AggregateError);
}
try {
isValidOrThrowAll(Number, String)(true);
throw "ups";
} catch (error) {
expect(error).not.toBeInstanceOf(TypeValidationError);
}
});
test("should throw 2 errors", () => {
try {
isValidOrThrowAll(Number, Boolean, String)(true);
} catch (error) {
expect(error.errors.length).toBe(2);
}
});
});
describe("isValidOrLogAll", () => {
test("should return true or false", () => {
jest.spyOn(globalThis.console, "error");
expect(isValidOrLogAll(Number, String)(true)).toBe(false);
expect(isValidOrLogAll(Boolean, true)(true)).toBe(true);
});
test("should log 2 errors", () => {
jest.spyOn(globalThis.console, "error");
isValidOrLogAll(Number, Boolean, String)(true);
expect(globalThis.console.error).toHaveBeenCalledTimes(2);
});
test("should log meaningful errors", () => {
jest.spyOn(globalThis.console, "error");
isValidOrLogAll(Number, Boolean, String)(true);
expect(globalThis.console.error).toHaveBeenCalledWith(
new TypeValidationError("value true do not match constructor Number")
);
expect(globalThis.console.error).toHaveBeenCalledWith(
new TypeValidationError("value true do not match constructor String")
);
});
test("should log meaningful errors in schemas", () => {
jest.spyOn(globalThis.console, "error");
isValidOrLogAll(
{ x: Number },
{ y: Boolean },
{ z: String }
)({ x: 1, y: 2, z: 3 });
expect(globalThis.console.error).toHaveBeenCalledWith(
new TypeValidationError("on path /y value 2 do not match constructor Boolean")
);
expect(globalThis.console.error).toHaveBeenCalledWith(
new TypeValidationError("on path /z value 3 do not match constructor String")
);
});
});
// describe("Composable errors", () => {
// test("should ", () => {
// const isNumber = isValidOrThrowAll(
// Number,
// BigInt,
// (num) => num === Number(num),
// );
// throw hasErrors(isNumber)("a");
// });
// });