Skip to content

Latest commit

 

History

History
1259 lines (962 loc) · 32.2 KB

README.md

File metadata and controls

1259 lines (962 loc) · 32.2 KB

garn-validator: Create validations with ease

Features

  • Supports checking primitives or objects with schemas
  • Apply default value if validation fails.
  • Easy to use and learn but powerful
  • It's totally composable
  • Fast and without dependencies
  • Six behaviors:
    • mustBe returns the value evaluated or it throws. (default export)
    • isValid returns true or false, never throws
    • isValidOrLog returns true or false and log error, never throws
    • hasErrors returns null or Array of errors, never throws
    • mustBeOrThrowAll returns the value evaluated or throws AggregateError
    • isValidOrLogAll returns true or false and log all errors, never throws
  • Works with ESModules or CommonJS from Node 10.x or Deno
  • Works in all modern browsers
  • Works in all frontend frameworks: React, Angular, Vue, etc...

npm version npm downloads Node Tests CI Deno Tests CI

DEPRECATION WARNING: isValidOrThrow behavior has been deprecated in favor of mustBe. Learn more at mustBe

Example

import mustBe, { isValid, isValidOrLog } from "garn-validator";

const isValidPassword = isValid(
  String, //  must be String
  (str) => str.length >= 8, // and length >= 8
  /[a-z]/, // and have at least one lowercase
  /[A-Z]/, // and have at least one uppercase
  /[0-9]/, // and have at least one digit
  /[-_/!¡?¿$%&/()]/ // and have at least one especial character
);

isValidPassword("12345Aa?"); // returns true

const isValidName = mustBe(String, (name) => name.length > 3).or("anonymous"); // will auto correct to 'anonymous' if fails


const isValidAge = isValidOrLog(
  Number,
  (age) => age > 18,
  (age) => age < 40
);

isValidAge(15); // false, and logs 15 do not match validator (age) => age > 18

// Composition
const isValidUser = mustBe({
  name: isValidName,
  age: isValidAge,
  password: isValidPassword,
  country: ["ES", "UK"], // 'ES' or 'UK'
});

const newUser = isValidUser({
  name: "", // will be fixed
  age: 38,
  password: "12345zZ?",
  country: "ES",
}); // returns { name: 'anonymous', age: 38, password: '12345zZ?', country: 'ES' }

const anotherUser = isValidUser({
  name: "garn",
  age: 38,
  password: "1234", // incorrect
  country: "ES",
}); // it throws --> TypeValidationError: At path /password "1234" do not match validator (str) => str.length >= 8

Contents

Get started

Node

npm install garn-validator

Import with ES Modules

// default export is mustBe
import mustBe from "garn-validator";
// or use named export
import { mustBe } from "garn-validator";

Require with CommonJs

const { mustBe } = require("garn-validator/commonjs");
// or use de default export
const mustBe = 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 mustBe from "https://deno.land/x/garn_validator/src/index.js";

To have type definitions you can do:

import * as garnValidator from "https://deno.land/x/garn_validator/src/index.js";
import * as ValidatorTypes from "https://deno.land/x/garn_validator/src/index.d.ts";
garnValidator as typeof ValidatorTypes;

const { mustBe } = garnValidator;

Basic Usage

import mustBe from "garn-validator"; // default export is mustBe

const isValidUser = mustBe({ name: String, age: Number });

isValidUser({ name: "garn", age: 38 }); // returns { name: "garn", age: 38 }
isValidUser({ name: "garn", age: "38" }); // it throws

Check against constructor

mustBe(Number)(2); // returns 2
mustBe(String)(2); // it throws
mustBe(Array)([1, 2]); // returns [1, 2]
mustBe(Object)([1, 2]); // it throws

Learn more in depth at Constructors

Check against primitive

mustBe("a")("a"); // returns "a"
mustBe(true)(false); // it throws

Learn more in depth at Primitives

Check string against regex

mustBe(/a*/)("a"); // returns "a"
mustBe(/a/)("b"); // it throws

Learn more in depth at RegExp

Check against custom function

mustBe((value) => value > 0)(33); // returns 33
mustBe((value) => value > 0)(-1); // wil throw
mustBe(Number.isNaN)(NaN); // returns NaN
mustBe(Number.isInteger)(1.1); // wil throw

Learn more in depth at Custom function

Check against enums (OR operator)

mustBe(["a", "b"])("a"); // returns "a"
mustBe(["a", "b"])("c"); // it throws
mustBe([Number, String])("18"); // returns "18"
mustBe([null, undefined, false, 0, ""])(18); // it throws

Learn more in depth at Enums

Check multiple validations (AND operator)

mustBe(Array, (array) => array.length === 2)([1, 2]); // returns [1, 2]
mustBe(
  (v) => v > 0,
  (v) => v < 50
)(100); // it throws

Learn more in depth at Validations in serie (AND operator)

Check object against an schema

const schema = { a: Number, b: Number }; // a and b are required
const obj = { a: 1, b: 2 };
mustBe(schema)(obj); // returns obj

mustBe({ a: 1 })({ a: 1, b: 2 }); // returns { a: 1, b: 2 },  because b is not checked
mustBe({ c: Number })({ a: 1, b: 2 }); // it throws (c is missing)

// Optional keys
mustBe({ x$: String })({}); // returns {}

Learn more in depth at Schema

Behaviors

All behaviors run the same algorithm but differs in what returns and how behaves.

There are six behaviors that can be divided in two categories:

  • It stops in first error (bail):

    • mustBe returns the value evaluated or it throws. (default export)
    • isValid returns true or false, never throws
    • isValidOrLog returns true or false and log error, never throws
  • It collects all Errors:

    • hasErrors returns null or Array of errors, never throws
    • mustBeOrThrowAll returns the value evaluated or throws AggregateError
    • isValidOrLogAll returns true or false and log all errors, never throws

mustBe

mustBe returns the value evaluated or it throws.

let input = "Garn";
let userName = mustBe(String)(input); //  return 'Garn'
let input = "Garn";
let userName;
let isValidName = mustBe(String, (val) => val.length > 4);
try {
  userName = isValidName(input); //  it throws
} catch (err) {
  userName = "anonymous";
}

mustBe may have attached an .or() to apply a default value if the validation fail.

let input = "Garn";
let isValidName = mustBe(String, (val) => val.length > 4).or("anonymous");
let userName = isValidName(input); //  returns 'anonymous'

The .or() can receive a function to apply a transformation to the original value.

let input = "42";
let asNumber = mustBe(Number).or((value /* "42" */) => Number(value));
let number = asNumber(input); //  returns 42

If you need to apply a function as default, you can use a function the returns a function.

let input = "i am not a function";
let noop = () => {};
let mustBeFunction = mustBe(Function).or(() => noop);
let fn = mustBeFunction(input); //  returns () => {}

It can work nested in a schema

let input = { name: "Garn" };
let isValidName = mustBe(String, (val) => val.length > 4).or("anonymous");

let user = mustBe({ name: isValidName })(input); // { name:'anonymous' }

If the .or() fails the whole validation will fail

let input = "not a valid number";
let transformToNumberIfPosible = (maybeNumber) => {
  let number = Number(maybeNumber);
  if (number == maybeNumber) return number;
  else throw new TypeError("not valid number");
};
let asNumber = mustBe(Number).or(transformToNumberIfPosible);
let number = asNumber(input); //  it throws CastError (aggregateError) with TypeError: not valid number within its array of errors

isValid, isValidOrLog and isValidOrLogAll

isValid returns true or false never throws, so if it fails for any reason you should know it won't tell you anything but false.

import { isValid } from "garn-validator";

// stops in first Error
isValid(/[a-z]/)("g"); // returns true
isValid(/[a-z]/)("G"); // returns false, doesn't throws

isValidOrLog is the same as isValid but log first error found and stops validating.

isValidOrLogAll returns true or false and log all errors, never 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

hasErrors

hasErrors returns null or Array with all errors found, never throws.

It's very useful to show the user all errors that need to be fixed.

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"); // [TypeValidationError, TypeValidationError]

mustBe vs mustBeOrThrowAll

mustBe returns the value evaluated or it throws the first error found.

try {
  mustBe({ a: Number, b: String })({ a: null, b: null });
} catch (error) {
  error instanceof TypeValidationError; // true
  error.message; // At path /a null do not match constructor Number
}

mustBeOrThrowAll returns the value evaluated or it throws an AggregateError with all errors found.

try {
  mustBeOrThrowAll({ a: Number, b: String })({ a: null, b: null });
} catch (error) {
  error instanceof AggregateError; // true
  error instanceof SchemaValidationError; // true
  console.log(error);
  /*
    SchemaValidationError: value {"a":null,"b":null} do not match schema {"a":Number,"b":String}
  */
  console.log(error.errors);
  /*
    [
      TypeValidationError: At path /a null do not match constructor Number ,
      TypeValidationError: At path /b null do not match constructor String ,
    ]
  */
}

But if it finds only one error, it will throw TypeValidationError, no AggregateError

try {
  mustBeOrThrowAll({ a: Number, b: String })({ a: null, b: "str" });
} catch (error) {
  console.log(error);
  /*
    TypeValidationError: At path /a null do not match constructor Number ,
  */
  error instanceof TypeValidationError; // true
  error instanceof SchemaValidationError; // false
}

Learn more at Errors

Utils

The library has a bunch of pre-made validations (and growing), which makes easier validations of stings, numbers, objects or dates.

Learn more at utils test

import {
  mustBe,
  arrayOf,
  and,
  Integer,
  Numeric,
  Positive,
  Lowercase,
  before,
} from "garn-validator";

let Schema = {
  name: Lowercase,
  birthday: before(new Date()),
  height: and(Number, Positive, Integer),
  creditCard: Numeric,
  books_id: arrayOf(String),
};

let data = {
  name: "garn",
  birthday: "1982-03-16",
  height: 170,
  creditCard: "42424242424242",
  books_id: ["123", "321"],
};

let user = mustBe(Schema).or(null)(data);

Logical utils

or(...validations) Is just a shortcut to an enum.

Not to be confused with mustBe().or()

import { mustBe, or } from "garn-validator";

mustBe(or(Number, String));
// same as:
mustBe([Number, String]);

and(...validations) is a shortcut to mustBe(...args), but semantically useful.

import { mustBe, and } from "garn-validator";

mustBe({ age: and(Number, (num) => num > 18) });
// same as:
mustBe({ age: mustBe(Number, (num) => num > 18) });

not(...validations) it negates the validations passed

import { mustBe, not } from "garn-validator";

// anything but Number
mustBe(not(Number))("qwerty"); // valid, return 'qwerty'

Object utils

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 mustBe from "garn-validator";

mustBe(Array, { [/\d/]: Number })([1, 2, 3]); // returns [1, 2, 3]
mustBe(Array, { [/\d/]: Number })([1, 2, "3"]); // throws

In order 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 mustBe, { arrayOf } from "garn-validator";

mustBe(arrayOf(Number))([1, 2, 3]); // returns [1, 2, 3]
mustBe(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 mustBe, { objectOf } from "garn-validator";

mustBe(objectOf(Number))({ a: 1, b: 2 }); // returns { a: 1, b: 2 }
mustBe(objectOf(Number))({ a: 1, b: "2" }); // throws

In depth

Types of validations

There are six 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

mustBe(1)(1); // returns 1 --> 1 === 1

mustBe("1")(1); // throws,  '1' !== 1

mustBe(1n)(1); // throws,  1n !== 1

mustBe(undefined)(null); // throws,  undefined !== null

// keep in mind than a symbol is only equal to itself
let s = Symbol();
mustBe(s)(s); // returns s
mustBe(s)(Symbol()); // throws

Constructors

Checking against a constructor means to know if the value evaluated has been created from that constructor

mustBe(Number)(2); // (2).constructor === Number  --> true
mustBe(Symbol)(Symbol()); // ok

A valid constructor is a class or any built-in constructor.

class Car {}
let honda = new Car();
mustBe(Car)(honda); // honda.constructor === Car  --> true

You can't use a normal function used as constructor from the old JS times.

function Car(name) {
  this.name = name;
}
let honda = new Car("honda");
mustBe(Car)(honda); // throws.  Car is detected as custom validator function

All built in Constructors are supported

Proxy detection

NOT YET WORKING IN DENO

In order to detect any Object (or Array) is a Proxy we intercept the creation of Proxies.

To have that functionality you must import "garn-validator/src/proxyDetection.js" before any creation of Proxies you need to detect;

import "garn-validator/src/proxyDetection.js";

const target = { a: 1 };
const proxy = new Proxy(target, {
  get: () => 33,
});

mustBe(Proxy)(proxy); // returns proxy

without running garn-validator/src/proxyDetection.js

// NO IMPORT
const target = { a: 1 };
const proxy = new Proxy(target, {
  get: () => 33,
});

mustBe(Proxy)(proxy); // fails

RegExp

The perfect validator to check strings. It does what you expect:

let isLowerCased = mustBe(/^[a-z]+$/);

isLowerCased("honda"); // /^[a-z]+$/.test('honda') --> true
// or building a regexp with the constructor RexExp;
mustBe(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 in order to pass the validation.

mustBe((val) => val >= 0)(10); // returns 10
mustBe((val) => val >= 0)(-10); // throws

mustBe(() => "I am truthy")(10); // returns 10
mustBe(() => [])(10); // returns 10

To fail a validation may return a falsy value or throw an error.

If it returns a falsy value, the default error will be thrown: TypeValidationError

If it throws an error, that error will be thrown.

mustBe(() => false)(10); // throws TypeValidationError
mustBe(() => 0)(10); // throws TypeValidationError

mustBe(() => {
  throw new RangeError("ups");
})(10); // throws RangeError

mustBe(() => {
  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"];
mustBe(cities)("valencia"); // returns "valencia"
mustBe(cities)("madrid"); // throws

But it's much more powerful than checking against primitives. It can contain any type of validator.

It checks every item until one passes.

let isNumberOrBigInt = [Number, BigInt]; // must be Number or BigInt
mustBe(isNumberOrBigInt)(1n); // returns 1n
mustBe(isNumberOrBigInt)(1); // returns 1

let isFalsy = [0, "", null, undefined, false];
mustBe(isFalsy)(""); // returns ""

let isNumberAlike = [Number, (val) => val === Number(val)];
mustBe(isNumberAlike)(1n); // returns 1n
mustBe(isNumberAlike)(1); // returns 1
mustBe(isNumberAlike)("1"); // returns "1"

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 protected]",
  },
};
mustBe(schema)(obj); // returns obj

Only the keys in the schema will be checked. Any key not present in the schema won't be checked (under consideration to be changed)

mustBe({})({ a: 1 }); // returns { a: 1 } , a is not in the schema

Optional Keys

And optional key must be undefined , null, or pass the validation

mustBe({ x$: Number })({ x: 1 }); // returns { x: 1 }, x is present and is Number
mustBe({ x$: String })({ x: 1 }); // it throws, x is present but is not String

mustBe({ x$: String })({}); // returns {}, x is undefined
mustBe({ x$: String })({ x: undefined }); // returns { x: undefined }, x is undefined
mustBe({ x$: String })({ x: null }); // returns { x: null }, x is null

You can use key$ or 'key?'. It would be nicer to have key? without quotes but is not valid JS

mustBe({ "x?": String })({}); // returns {}

Regexp keys

You can validate multiple keys at once using a regexp key

mustBe({
  [/./]: String,
})({
  a: "a",
  b: "b",
}); // ok

// or write it as plain string
mustBe({
  "/./": String,
})({
  a: "a",
  b: 1, // fails
}); // throws

// only checks the keys that matches regex
mustBe({
  [/^[a-z]+$/]: Number,
})({
  x: 1,
  y: 2,
  z: 3,
  CONSTANT: "foo", // not checked
}); // ok, all lowercased keys are numbers

The required keys and optional won't be check against a regexp key

mustBe({
  [/./]: Number,
  x: String, //  this required key has priority against regex key
})({
  x: "x", // not checked as Number, checked as String
}); // ok,  x is String

mustBe({
  [/./]: Number,
  x: String,
})({
  x: "x", // not checked as Number, checked as String
  y: "y", // checked as Number, fails
}); // throw

mustBe({
  [/./]: Number,
  $x: String,
  y: String,
})({
  x: "x", // not checked as Number, checked as String
  y: "y", // checked as String
  z: "z", // checked as Number, fails
}); // throw

This feature is perfect to note that any key not specified in schema is not allowed

mustBe({
  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
mustBe({
  max: (val, root, keyName) => val > root.min,
  min: (val, root, keyName) => val < root.max,
})({
  max: 1,
  min: -1,
}); // ok

mustBe({
  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
mustBe({
  [/./]: (val, root, keyName) => keyName.length > 3,
})({
  max: 1, // key too short
  longKey: 1, // valid key
}); // it throws, max key is too short

Validations in serie (AND operator)

The validator constructor can receive as many validations as needed.

All will be checked until one fails

const isArrayOfLength2 = mustBe(Array, (array) => array.length === 2);
isArrayOfLength2([1, 2]); // returns [1, 2]

mustBe(
  (v) => v > 0,
  (v) => v < 50 // will fail
)(100); // it throws
const isValidPassword = mustBe(
  String, // must be an String
  (str) => str.length >= 8, // and its length must be at least 8
  /[a-z]/, // and must have at least one lowercase
  /[A-Z]/, // and must have at least one uppercase
  /[0-9]/, // and must have at least one number
  /[-_/!·$%&/()]/ // and must have at least one especial character
);

isValidPassword("12345wW-"); // returns "12345wW-"
isValidPassword("12345"); // fails

Errors

If a validation fails it will throw new TypeValidationError(meaningfulMessage) which inherits from TypeError. It can be imported.

If it throws an error from a custom validator, that error will be thrown.

import { mustBe, TypeValidationError } from "garn-validator";

try {
  mustBe(Boolean)(33);
} catch (error) {
  error instanceof TypeValidationError; // true
  error instanceof TypeError; // true
}

try {
  mustBe(() => {
    throw "ups";
  })(33);
} catch (error) {
  error === "ups"; // true
}

try {
  mustBe(() => {
    throw new RangeError("out of range");
  })(33);
} catch (error) {
  error instanceof RangeError; // true
  error instanceof TypeError; // false
}

AggregateError

There are 3 types a AggregateError that can be thrown:

  • SchemaValidationError: thrown when more than one key fails checking an schema
  • EnumValidationError: thrown when all validations fails checking an enum
  • SerieValidationError: thrown when more than one validation fails checking an Serie

All of them inherits from AggregateError and has a property errors with an array of all errors collected

try {
  mustBeOrThrowAll(Number, String)(null);
} catch (error) {
  error instanceof AggregateError; // true
  console.log(error.errors);
  /*
    [
      TypeValidationError: value null do not match constructor Number ,
      TypeValidationError: value null do not match constructor String ,
    ]
  */
}

SchemaValidationError

If using mustBeOrThrowAll 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 mustBe only the first Error will be thrown.

// more than 2 keys fails
try {
  mustBeOrThrowAll({ 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 {
  mustBeOrThrowAll({ a: 1 })({});
} catch (error) {
  console.log(error instanceof TypeError); // true
  console.log(error instanceof SchemaValidationError); // false
}

EnumValidationError

If all validations of an enum fails, 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 {
  mustBe([Boolean, String])(1);
} catch (error) {
  console.log(error instanceof EnumValidationError); // true
  console.log(error instanceof AggregateError); // true
}

try {
  mustBe([Boolean])(1);
} catch (error) {
  console.log(error instanceof EnumValidationError); // false
  console.log(error instanceof TypeError); // true
}

SerieValidationError

If using mustBeOrThrowAll fails all validations of a serie , it will throw a SerieValidationError with all Errors aggregated in error.errors

But if the length of the enum is 1. it will throw only this error.

SerieValidationError inherits from AggregateError.

try {
  mustBeOrThrowAll(Boolean, String)(1);
} catch (error) {
  console.log(error instanceof SerieValidationError); // true
  console.log(error instanceof AggregateError); // true
}

try {
  mustBeOrThrowAll(Boolean)(1);
} catch (error) {
  console.log(error instanceof SerieValidationError); // false
  console.log(error instanceof TypeError); // true
}

hasErrors

hasErrors will flatMap all errors found. No AggregateError will be in the array returned.

hasErrors(/[a-z]/)("g"); // null
hasErrors(/[a-z]/, Number)("G");
/*
[
  TypeValidationError: value "G" do not match regex /[a-z]/,
  TypeValidationError: value "G" do not match constructor Number ,
]
*/

hasErrors({ a: Number, b: String })({ a: null, b: null });
/*
[
  TypeValidationError: At path /a null do not match constructor Number,
  TypeValidationError: At path /b null do not match constructor String
]
*/

Raw Error data

All errors the library throws has the raw data collected in a property called raw.

try {
  mustBe({ a: Number })({ a: null });
} catch (error) {
  console.log(error.raw);
}
/*
{

  //  the validation failing
  type: [Function: Number],

  //  the value evaluated
  value: null,

  // the root object
  root: { a: null },

  // the key failing
  keyName: 'a',

  // the whole path where the evaluation happens as an array
  path: [ 'a' ],

  // the error message
  message: 'At path /a null do not match constructor Number',

   // the error constructor
  '$Error': [class TypeValidationError extends TypeError],

  // the behavior applied
  behavior: {
    collectAllErrors: false,
    onValid: [Function: onValidDefault],
    onInvalid: [Function: onInvalidDefault]
  },
}
 */

Composition in depth

You can create your own validators and use them as custom validation creating new ones.

const isPositive = mustBe((v) => v > 0);
const isNotBig = mustBe((v) => v < 100);
const isNumber = mustBe([Number, String], (num) => num == Number(num));

mustBe(isNumber, isPositive, isNotBig)("10"); // returns "10"
mustBe(isNumber, isPositive, isNotBig)(200); // it throws

When used inside another kind of behavior, it will inherit the behavior from where it has been used.

const isNotBig = isValidOrLog((v) => v < 100);
// its normal behavior
isNotBig(200); // false, logs '200 do not match validator (v) => v < 100'

isValid(isNotBig)(200); // false , and won't log
mustBe(isNotBig)(200); // fails , and won't log
hasErrors(isNotBig)(200); // array,  won't log
/*
[
  new TypeValidationError('200 do not match validator (v) => v < 100')
]
 */

Actually, it's not treated as a custom validation function. No matter is your are using hasErrors which return null when nothing fails, and it's just works.

const isBigNumber = hasErrors(
  [Number, String],
  (num) => num == Number(num),
  (num) => num > 1000
);

// its normal behavior
isBigNumber("a12");
/* [
  new TypeValidationError(""a12" do not match validator (num) => num == Number(num)"),
  new TypeValidationError(""a12" do not match validator num => num > 1000"),
];
 */

// inherit behavior
isValidOrLog(isBigNumber)("a12"); // false, and log only one error value "a10" do not match validator (num) => num == Number(num)

Especial cases

AsyncFunction & GeneratorFunction

AsyncFunction and GeneratorFunction constructors are not in the global scope of any of the three JS environments (node, browser or deno). 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 mustBe, { AsyncFunction, GeneratorFunction } from "garn-validator";

mustBe(AsyncFunction)(async () => {}); // ok
mustBe(GeneratorFunction)(function* () {}); // ok

mustBe(Function)(function* () {}); // throws
mustBe(Function)(async function () {}); // 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
  • Support for browser
  • Behavior applyDefaultsOnError. (syntax mustBe(Number).or(0))
  • Async validation support
  • More built-in utils functions