Skip to main content

garn-validator

Ultra fast runtime type validator without dependencies.

npm version

Tests CI

Features

  • Supports checking primitives or objects with schemas
  • Ultra light and fast with 0 dependencies
  • Easy to use and simple to learn but powerful
  • 5 behaviors (isValid, isValidOrThrow, isValidOrLogAllErrors, isValidOrLog and hasErrors)
  • Works with ESModules or CommonJS from Node 10.x or Deno
  • Works in all frontend frameworks under babel (React, Angular, Vue, etc…)

Get started

Install

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 built-in constructors

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 5 behaviors you can import:

  • isValidOrThrow (returns true of throw the first error found)
  • hasErrors (return null or array of errors, never throws)
  • isValid (returns true or false, never throws)
  • isValidOrLog (returns true or false and log first error, never throws)
  • isValidOrLogAllErrors (returns true or false and log all errors, never throws)
  • isValidOrThrowAllErrors (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 isValidOrThrowAllErrors

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 {
  isValidOrThrowAllErrors({ a: 1, b: 2 })({});
} catch (error) {
  console.log(error instanceof SchemaValidationError); // true
  console.log(error instanceof AggregateError); // true
  console.log(error.errors.length); // 2
}
try {
  isValidOrThrow({ a: 1, b: 2 })({});
} catch (error) {
  console.log(error instanceof SchemaValidationError); // false
  console.log(error instanceof TypeError); // true
}

// only 1 key fails
try {
  isValidOrThrowAllErrors({ 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 isValidOrThrowAllErrors

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 {
  isValidOrThrowAllErrors(Boolean, String)(1);
} catch (error) {
  console.log(error instanceof SeriesValidationError); // true
  console.log(error instanceof AggregateError); // true
}

try {
  isValidOrThrowAllErrors(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 => isValidOrThrow(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 => isValidOrThrow(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

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

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

errors.test.js

import {
  isValidOrThrow,
  hasErrors,
  isValidOrLogAllErrors,
  isValidOrThrowAllErrors,
  SchemaValidationError,
  EnumValidationError,
  SeriesValidationError,
} from "garn-validator";

describe("AggregateError", () => {
  test.each([AggregateError, SchemaValidationError, Error])(
    "if schema fails with more than 2 errors should throw %p",
    (ErrorType) => {
      expect(() => {
        isValidOrThrowAllErrors({ a: Number, b: String })({});
      }).toThrow(ErrorType);
    }
  );
  test.each([AggregateError, EnumValidationError, Error])(
    "if enums fails with more than 2 errors should throw %p",
    (ErrorType) => {
      expect(() => {
        isValidOrThrowAllErrors([Number, String])(true);
      }).toThrow(ErrorType);
    }
  );
  test.each([AggregateError, SeriesValidationError, Error])(
    "if Series fails with more than 2 errors should throw %p",
    (ErrorType) => {
      expect(() => {
        isValidOrThrowAllErrors(Number, String)(true);
      }).toThrow(ErrorType);
    }
  );
  test("checking schema should throw SchemaValidationError or TypeError", () => {
    try {
      isValidOrThrowAllErrors({ 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 TypeError).toBe(true);
    }

    // only 1 key fails
    try {
      isValidOrThrowAllErrors({ a: 1 })({});
    } catch (error) {
      expect(error instanceof TypeError).toBe(true);
      expect(error instanceof SchemaValidationError).toBe(false);
    }
  });
  test("checking enum should throw EnumValidationError or TypeError", () => {
    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 TypeError).toBe(true);
    }
  });
  test("checking series should throw SeriesValidationError or TypeError ", () => {
    try {
      isValidOrThrowAllErrors(Boolean, String)(1);
    } catch (error) {
      expect(error instanceof SeriesValidationError).toBe(true);
      expect(error instanceof AggregateError).toBe(true);
    }

    try {
      isValidOrThrowAllErrors(Boolean)(1);
    } catch (error) {
      expect(error instanceof SeriesValidationError).toBe(false);
      expect(error instanceof TypeError).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 TypeError", () => {
    expect(() => {
      isValidOrThrow(Boolean)(33);
    }).toThrow(TypeError);
  });
  test("Should throw meaningfully message", () => {
    expect(() => {
      isValidOrThrow(1)(33);
    }).toThrow("value 33 do not match type 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 {
      isValidOrThrowAllErrors(
        () => {
          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 type {"a":Number}');
  });
  test("should format the value", () => {
    expect(() => {
      isValidOrThrow({ a: Number })({ b: 33 });
    }).toThrow("on path /a value undefined do not match type 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 type 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).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 TypeError('on path /num value "2" do not match type 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 TypeError("value 2 do not match type v=>v>100")],
      ],
      [
        String,
        (v) => v > 100,
        2,
        [
          new TypeError("value 2 do not match type String"),
          new TypeError("value 2 do not match type 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 TypeError('on path /num value "2" do not match type Number')],
      ],
      [
        { num: Number, str: String },
        { num: "2", str: null },
        [
          new TypeError('on path /num value "2" do not match type Number'),
          new TypeError("on path /str value null do not match type String"),
        ],
        // [
        //   new AggregateError(
        //     [
        //       new TypeError('on path /num value "2" do not match type Number'),
        //       new TypeError("on path /str value null do not match type String"),
        //     ],
        //     'value {"num":"2","str":null} do not match type {"num":Number,"str":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 TypeError('on path /obj/num value "2" do not match type Number')],
      ],
      [
        {
          thr: () => {
            throw new RangeError("ups");
          },
        },
        { thr: 1 },
        [new RangeError("ups")],
      ],
      [
        { obj: { num: Number, str: String } },
        { obj: { num: "2", str: null } },
        [
          new TypeError('on path /obj/num value "2" do not match type Number'),
          new TypeError("on path /obj/str value null do not match type 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 TypeError(
          "on path /noValidKey value 1 do not match type ()=>false"
        ),
        new TypeError(
          'on path /name value "Garn" do not match type /^[a-z]{3,}$/'
        ),
        new TypeError("on path /age value 18 do not match type age=>age>18"),
        new EvalError("unexpected key"),
        new TypeError(
          'on path /car/brand value "Honda" do not match type "honda"'
        ),
        new TypeError(
          'on path /car/brand value "Honda" do not match type "toyota"'
        ),
        new TypeError(
          'on path /car/date value "1982-01-01" do not match type Date'
        ),
        new TypeError(
          "on path /car/country/name value undefined do not match type String"
        ),

        new TypeError("on path /optional value false do not match type 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 TypeError("on path /x value true do not match type Number"),
        new TypeError("on path /y value 1 do not match type Boolean"),
        new TypeError("on path /z value undefined do not match type Function"),
      ]);
    });
  });
});

describe("isValidOrThrowAllErrors ", () => {
  jest.spyOn(globalThis.console, "error");

  test("should throw AggregateError with all errors", () => {
    try {
      isValidOrThrowAllErrors(Number, String)(true);
      throw "ups";
    } catch (error) {
      expect(error).toBeInstanceOf(AggregateError);
    }
    try {
      isValidOrThrowAllErrors(Number, String)(true);

      throw "ups";
    } catch (error) {
      expect(error).not.toBeInstanceOf(TypeError);
    }
  });
  test("should throw 2 errors", () => {
    try {
      isValidOrThrowAllErrors(Number, Boolean, String)(true);
    } catch (error) {
      expect(error.errors.length).toBe(2);
    }
  });
});
describe("isValidOrLogAllErrors", () => {
  test("should return true or false", () => {
    jest.spyOn(globalThis.console, "error");
    expect(isValidOrLogAllErrors(Number, String)(true)).toBe(false);

    expect(isValidOrLogAllErrors(Boolean, true)(true)).toBe(true);
  });
  test("should log 2 errors", () => {
    jest.spyOn(globalThis.console, "error");

    isValidOrLogAllErrors(Number, Boolean, String)(true);
    expect(globalThis.console.error).toHaveBeenCalledTimes(2);
  });
  test("should log meaningful errors", () => {
    jest.spyOn(globalThis.console, "error");
    isValidOrLogAllErrors(Number, Boolean, String)(true);

    expect(globalThis.console.error).toHaveBeenCalledWith(
      new TypeError("value true do not match type Number")
    );
    expect(globalThis.console.error).toHaveBeenCalledWith(
      new TypeError("value true do not match type String")
    );
  });
  test("should log meaningful errors in schemas", () => {
    jest.spyOn(globalThis.console, "error");
    isValidOrLogAllErrors(
      { x: Number },
      { y: Boolean },
      { z: String }
    )({ x: 1, y: 2, z: 3 });

    expect(globalThis.console.error).toHaveBeenCalledWith(
      new TypeError("on path /y value 2 do not match type Boolean")
    );
    expect(globalThis.console.error).toHaveBeenCalledWith(
      new TypeError("on path /z value 3 do not match type String")
    );
  });
});

// describe("Composable errors", () => {
//   test("should ", () => {
//     const isNumber = isValidOrThrowAllErrors(
//       Number,
//       BigInt,
//       (num) => num === Number(num),
//     );
//     throw hasErrors(isNumber)("a");

//   });
// });