Skip to content

Instantly share code, notes, and snippets.

@akirattii
Last active January 19, 2019 23:02
Show Gist options
  • Save akirattii/4968f08a30cafd10cd95db3b47669b47 to your computer and use it in GitHub Desktop.
Save akirattii/4968f08a30cafd10cd95db3b47669b47 to your computer and use it in GitHub Desktop.
NodeJS: A validator base class to create your own validator to validate posted data from browser.
const test = require('tape');
const ValidatorBase = require("./ValidatorBase.js");
const inst = new ValidatorBase({ debug: true });
const className = inst.constructor.name;
//
// checkNum
//
test(`Testing ${className}#checkNum`, function(t) {
const funcName = "checkNum"; // testing method
// OKs:
const paramsOK = [
{ item: { name: 100 }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: -100 }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: "100" }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: "-100" }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: 0 }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: "0" }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: 99 }, key: "name", min: 99, max: 100, type: "int", errors: [] },
{ item: { name: "99" }, key: "name", min: 99, max: 100, type: "int", errors: [] },
{ item: { name: "-1" }, key: "name", min: -1, max: 0, type: "int", errors: [] },
{ item: { name: "-0.1" }, key: "name", min: -1, max: 0, type: "decimal", errors: [] },
{ item: { name: "-0.09" }, key: "name", min: -0.1, max: 0, type: "decimal", errors: [] },
{ item: { name: "-0.09" }, key: "name", min: -0.1, max: 0, type: "decimal", allowEmpty: true, errors: [] },
{ item: { name: "" }, key: "name", allowEmpty: true, errors: [] },
{ item: { name: null }, key: "name", allowEmpty: true, errors: [] },
{ item: { name: undefined }, key: "name", allowEmpty: true, errors: [] },
];
// NGs:
const paramsNG = [
{ item: { name: 101 }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: -101 }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: "101" }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: "-101" }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: 0.1 }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: "0.1" }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: "99.9" }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: null }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: NaN }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: undefined }, key: "name", min: -100, max: 100, type: "int", errors: [] },
{ item: { name: "-0.1" }, key: "name", min: 0, max: 1, type: "int", errors: [] },
{ item: { name: "-0.11" }, key: "name", min: -0.1, max: 1, type: "decimal", errors: [] },
];
t.plan(paramsOK.length + paramsNG.length);
testloop(t, inst, funcName, paramsOK, paramsNG); // test loop
});
//
// checkPositiveInt
//
test(`Testing ${className}#checkPositiveInt`, function(t) {
const funcName = "checkPositiveInt"; // testing method
// OKs:
const paramsOK = [
{ item: { name: 0 }, key: "name", min: 0, errors: [] },
{ item: { name: 1 }, key: "name", min: 1, errors: [] },
{ item: { name: 100 }, key: "name", min: 99, max: 100, errors: [] },
{ item: { name: "100" }, key: "name", min: 99, max: 100, errors: [] },
];
// NGs:
const paramsNG = [
{ item: { name: -1 }, key: "name", max: 100, errors: [] },
{ item: { name: -100 }, key: "name", max: 100, errors: [] },
{ item: { name: 101 }, key: "name", max: 100, errors: [] },
{ item: { name: "101" }, key: "name", max: 100, errors: [] },
];
t.plan(paramsOK.length + paramsNG.length);
testloop(t, inst, funcName, paramsOK, paramsNG); // test loop
});
//
// checkByRegex
//
test(`Testing ${className}#checkByRegex`, function(t) {
const funcName = "checkByRegex"; // testing method
// OKs:
const paramsOK = [
{ item: { name: "000-111" }, key: "name", regex: "^(\\d+)\-(\\d+)$", errors: [] },
];
// NGs:
const paramsNG = [
{ item: { name: "000111" }, key: "name", regex: "^(\\d+)\-(\\d+)$", errors: [] },
{ item: { name: 1234 }, key: "name", regex: "^(\\d+)\-(\\d+)$", errors: [] },
];
t.plan(paramsOK.length + paramsNG.length);
testloop(t, inst, funcName, paramsOK, paramsNG); // test loop
});
//
// checkLen
//
test(`Testing ${className}#checkLen`, function(t) {
const funcName = "checkLen"; // testing method
// OKs:
const paramsOK = [
{ item: { name: "abc" }, key: "name", min: 1, max: 3, errors: [] },
{ item: { name: 123 }, key: "name", min: 1, max: 3, errors: [] },
];
// NGs:
const paramsNG = [
{ item: { name: "abcd" }, key: "name", min: 1, max: 3, errors: [] },
{ item: { name: "" }, key: "name", min: 1, max: 3, errors: [] },
{ item: { name: 1234 }, key: "name", min: 1, max: 3, errors: [] },
];
t.plan(paramsOK.length + paramsNG.length);
testloop(t, inst, funcName, paramsOK, paramsNG); // test loop
});
//
// checkBool
//
test(`Testing ${className}#checkBool`, function(t) {
const funcName = "checkBool"; // testing method
// OKs:
const paramsOK = [
{ item: { name: "true" }, key: "name", errors: [] },
{ item: { name: true }, key: "name", errors: [] },
{ item: { name: "false" }, key: "name", errors: [] },
{ item: { name: false }, key: "name", errors: [] },
];
// NGs:
const paramsNG = [
{ item: { name: 1 }, key: "name", errors: [] },
{ item: { name: 0 }, key: "name", errors: [] },
{ item: { name: "TRUE" }, key: "name", errors: [] },
{ item: { name: "FALSE" }, key: "name", errors: [] },
];
t.plan(paramsOK.length + paramsNG.length);
testloop(t, inst, funcName, paramsOK, paramsNG); // test loop
});
//
// checkEmail
//
test(`Testing ${className}#checkEmail`, function(t) {
const funcName = "checkEmail"; // testing method
// OKs:
const paramsOK = [
{ item: { name: "[email protected]" }, key: "name", errors: [] },
{ item: { name: "[email protected]" }, key: "name", errors: [] },
];
// NGs:
const paramsNG = [
{ item: { name: "hoge.foo.com" }, key: "name", errors: [] },
{ item: { name: "123456789@123com" }, key: "name", errors: [] },
];
t.plan(paramsOK.length + paramsNG.length);
testloop(t, inst, funcName, paramsOK, paramsNG); // test loop
});
//
// checkURL
//
test(`Testing ${className}#checkURL`, function(t) {
const funcName = "checkURL"; // testing method
// OKs:
const paramsOK = [
{ item: { name: "https://www.example.com" }, key: "name", errors: [] },
{ item: { name: "http://123.com" }, key: "name", errors: [] },
{ item: { name: "http://123.com/hoge/1234%#?a=%#$#$#" }, key: "name", errors: [] },
];
// NGs:
const paramsNG = [
{ item: { name: "ftp://hoge.foo.com" }, key: "name", errors: [] },
{ item: { name: "hoge.foo.com" }, key: "name", errors: [] },
{ item: { name: "hoge.foo.com" }, key: "name", errors: [] },
{ item: { name: "123456789@123com" }, key: "name", errors: [] },
];
t.plan(paramsOK.length + paramsNG.length);
testloop(t, inst, funcName, paramsOK, paramsNG); // test loop
});
//
// checkUnixtime
//
test(`Testing ${className}#checkUnixtime`, function(t) {
const funcName = "checkUnixtime"; // testing method
// OKs:
const paramsOK = [
{ item: { name: "1234567890" }, key: "name", allowEmpty: true, errors: [] },
{ item: { name: "01234567890" }, key: "name", allowEmpty: true, errors: [] },
{ item: { name: 0 }, key: "name", allowEmpty: true, errors: [] },
{ item: { name: "" }, key: "name", allowEmpty: true, errors: [] },
{ item: { name: null }, key: "name", allowEmpty: true, errors: [] },
{ item: { name: 123456789 }, key: "name", min: 123456788, max: 1234567890, errors: [] },
{ item: { name: "123456789" }, key: "name", min: 123456788, max: 1234567890, errors: [] },
];
// NGs:
const paramsNG = [
{ item: { name: "-1234567890" }, key: "name", allowEmpty: true, errors: [] },
{ item: { name: -1234567890 }, key: "name", allowEmpty: true, errors: [] },
{ item: { name: "" }, key: "name", allowEmpty: false, errors: [] },
{ item: { name: null }, key: "name", allowEmpty: false, errors: [] },
];
t.plan(paramsOK.length + paramsNG.length);
testloop(t, inst, funcName, paramsOK, paramsNG); // test loop
});
//
// checkContained
//
test(`Testing ${className}#checkContained`, function(t) {
const funcName = "checkContained"; // testing method
// OKs:
const paramsOK = [
{ item: { name: "" }, key: "name", list: [1, 2, 3], allowEmpty: true, errors: [] },
{ item: { name: "1" }, key: "name", list: [1, 2, 3], allowEmpty: true, errors: [] },
{ item: { name: 2 }, key: "name", list: [1, 2, 3], allowEmpty: true, errors: [] },
{ item: { name: "3" }, key: "name", list: [1, 2, 3], allowEmpty: true, errors: [] },
{ item: { name: "3" }, key: "name", list: ["1", "2", "3"], allowEmpty: true, errors: [] },
{ item: { name: 3 }, key: "name", list: ["1", "2", "3"], allowEmpty: true, errors: [] },
{ item: { name: null }, key: "name", list: [null, 1], allowEmpty: false, errors: [] },
{ item: { name: null }, key: "name", list: ["null", "1"], allowEmpty: false, errors: [] },
{ item: { name: "null" }, key: "name", list: [null, 1], allowEmpty: false, errors: [] },
{ item: { name: "undefined" }, key: "name", list: [undefined, 1], allowEmpty: false, errors: [] },
{ item: { name: undefined }, key: "name", list: [undefined, 1], allowEmpty: false, errors: [] },
];
// NGs:
const paramsNG = [
{ item: { name: null }, key: "name", list: [1, 2, 3], allowEmpty: false, errors: [] },
{ item: { name: -2 }, key: "name", list: [1, 2, 3], allowEmpty: true, errors: [] },
{ item: { name: "0" }, key: "name", list: [1, 2, 3], allowEmpty: true, errors: [] },
{ item: { name: "4" }, key: "name", list: [1, 2, 3], allowEmpty: true, errors: [] },
];
t.plan(paramsOK.length + paramsNG.length);
testloop(t, inst, funcName, paramsOK, paramsNG); // test loop
});
function testloop(t, inst, fn, paramsOK, paramsNG) {
// OKs loop test:
for (let len = paramsOK.length, i = 0; i < len; i++) {
const p = paramsOK[i];
t.equal(inst[fn](p) === null, true); //OK
}
// NGs loop test:
for (let len = paramsNG.length, i = 0; i < len; i++) {
const p = paramsNG[i];
t.equal(inst[fn](p) === null, false); //NG
}
}
const Big = require("big.js");
/**
* Validator base class.
* Create your validator subclass derived from this class.
* Assuming that it's used to validate a browser's posting data on server-side.
*
* ## NOTE: `check*()` methods:
*
* If validation failed in a checking method named `check*()`,
* some errors are set to `errors` which is passed as a parameter, and also returned as a method's result.
*
* ## NOTE: Error object
*
* See code in `_setError()` method of this class which returns Error object.
*
*/
/*
# Usage
```
// First, extend ValidatorBase to create your own validator class:
const ValidatorBase = require("./ValidatorBase.js");
// Create your own validator class:
class FooValidator extends ValidatorBase {
constructor(p) {
super(p);
}
// @override
validate(item, errors = []) {
if (!item) {
return errors.push("data not found.");
}
super.checkLen({ item, key: "name", min: 1, max: 16, errors });
super.checkPositiveInt({ item, key: "mynumber", min: 10, max: 10, allowEmpty: false, errors });
return errors;
}
};
// Create your own validator's instance:
const validator = new FooValidator();
// For debugging, you can set `debug` parameter. If set true,
// `val` which is a value you inputted is added to each error object of `errors`:
// const validator = new FooValidator({ debug: true });
// Test data to validate:
const data = { name: "abc", mynumber: "1234567890" };
// Validate! If validation is ok, the returned `errors` is [] (empty array).
const errors = validator.validate(data);
// Also, you can set `errors` that already created:
// const errors = [];
// validator.vaidator(data, errors); // if there are any validation errors, they are appended to `errors`.
```
*/
module.exports = class ValidatorBase {
/**
* Parameter:
* @param debug {Boolean} - includes `val` keyerty to check the actual data.
*/
constructor(p) {
this.debug = (p && p.debug === true) ? true : false;
}
/**
* TODO: OVERRIDE!
* @param {Object} - Typically, this object is a data posted from client-side. So their keyerty types must be all <string>.
* @param {Array} - errors array into which error objects appended.
*
* errors' format: [{
* key: "<keyerty_name>",
* msg: "<error_msg_of_the_keyerty>",
* (val: <actual_value_if_debug_true>,)
* }]
*/
validate(item, errors = []) {
console.warn("TODO: DO NOT CALL THIS DIRECTLY! Please override `validate()` method from a subclass you derived!");
// TODO: Override from derived class!
return errors;
}
//
// Basic check functions
//
/**
* Numeric check.
* If validation failed, errors are set to `errors` parameter and also returned as a result.
*/
// NOTE: if want to pass all length of numeric string, set `null` to either `min` and `max`,
// otherwise set Number.MIN_VALUE to `min` and Number.MAX_VALUE to `max`
checkNum({
key,
item,
errors,
min = Number.MIN_VALUE,
max = Number.MAX_VALUE,
type = "int", // "int" or "decimal"
allowEmpty = false,
}) {
this._checkBasicParam({ key, item, errors });
const val = item[key];
if (allowEmpty === true && !val) {
return null;
}
if (type) type = type.toLowerCase();
if (type === "int" &&
/^(\+|\-)?(\d+)$/.test(val) &&
this.withinRangeNum({ val, min, max })) {
return null; // OK, valid integer
} else if (type === "decimal" &&
this.isNumeric(val) &&
this.withinRangeNum({ val, min, max })) {
return null; // OK, valid decimal
} else {
return this._setError({ key, item, errors, msg: `must be a ${type} number within ${min} to ${max}` });
}
}
/** Checks whether it's a positive integer. */
checkPositiveInt({
key,
item,
errors,
min = 0,
max = Number.MAX_VALUE,
allowEmpty = false,
}) {
return this.checkNum({ key, item, errors, min, max, type: "int", allowEmpty });
}
/** Checks whether it fits to the regular expression. */
checkByRegex({
key,
item,
errors,
regex, // RegExp string. e.g. "^(\\d+){3}$".
allowEmpty = false,
}) {
this._checkBasicParam({ key, item, errors });
if (!regex) throw Error("invalid parameter: 'regex' as RegExp string must be set.");
const val = item[key];
if (allowEmpty === true && !val) {
return null;
}
const reg = new RegExp(regex);
if (reg.test(val)) {
return null; // OK
} else {
return this._setError({ key, item, errors, msg: `must be a valid format: ${regex}` });
}
}
/** Checks whether it's within the range of the string length. */
checkLen({
key,
item,
min = 1,
max = Number.MAX_VALUE,
errors,
allowEmpty = false,
}) {
this._checkBasicParam({ key, item, errors });
const val = item[key];
if (allowEmpty === true && !val) {
return null;
}
if (!this.withinRangeLen({ val, min, max })) {
return this._setError({ key, item, errors, msg: `must have length within ${min} to ${max}` });
}
return null;
}
/** Checks whether it's a boolean (or boolean string). */
checkBool({
item,
key,
errors,
allowEmpty = false,
}) {
this._checkBasicParam({ key, item, errors });
const val = item[key];
if (allowEmpty === true && !val) {
return null;
}
if (!this.isBool(val)) {
return this._setError({ key, item, errors, msg: `must be a boolean` });
}
return null;
}
/** Checks whether it's an email address. */
checkEmail({
item,
key,
errors,
allowEmpty = false,
}) {
this._checkBasicParam({ key, item, errors });
const val = item[key];
if (allowEmpty === true && !val) {
return null;
}
if (!this.isEmail(val)) {
return this._setError({ key, item, errors, msg: `must be an email address` });
}
return null;
}
/** Checks whether it's an URL. */
checkURL({
item,
key,
errors,
allowEmpty = false,
}) {
this._checkBasicParam({ key, item, errors });
const val = item[key];
if (allowEmpty === true && !val) {
return null;
}
if (!this.isURL(val)) {
return this._setError({ key, item, errors, msg: `must be an URL` });
}
return null;
}
/** Checks whether it's an unixtime. */
checkUnixtime({
item,
key,
errors,
min = 0,
max = Number.MAX_VALUE,
allowEmpty = false
}) {
this._checkBasicParam({ key, item, errors });
const val = item[key];
if (allowEmpty === true && !val) {
return null;
}
return this.checkPositiveInt({ key, item, errors, min, max });
}
/**
* Checks whether item[key] is contained in the `list`
*
* 例: list が [0, 10, 20, null] の場合, item[key] が次の値**以外**の場合にバリデーションエラーを返します:
* 0, 10, 20, "0", "10", "20"
*/
checkContained({
item,
key,
errors,
list = [], // e.g. [0, 10, 20, 99], ["abc", "def", "ghi"], or ["aaa", 999, null, undefined]
allowEmpty = false
}) {
this._checkBasicParam({ key, item, errors });
let val = item[key];
if (allowEmpty === true && !val) {
return null;
}
// リスト内の値と item[key] をどっちも文字列に統一して比較する
val = "" + val;
const midx = list.findIndex(x => {
x = "" + x; // 数値も文字列にキャスト
return x === val;
});
if (midx < 0) {
return this._setError({ key, item, errors, msg: `must choose one of these: ${list.join(", ")}` });
}
return null;
}
_checkBasicParam({ key, item, errors }) {
if (!key || !item || !Array.isArray(errors)) {
throw Error("invalid parameter: either 'key', 'item', and 'errors' must be set.");
}
}
_setError({ key, item, errors, msg }) {
const err = {
key,
msg,
};
if (this.debug) err.val = item[key];
errors.push(err);
return errors;
}
//
// generic functions
//
isNumeric(num) {
if (num == null) return false;
return !isNaN(num)
}
isBool(val) {
if (val === true || val === false) return true;
if (val === "true" || val === "false") return true;
return false;
}
withinRangeNum({ val, min = Number.MIN_VALUE, max = Number.MAX_VALUE }) {
if (min === null && max === null) return true; // if either min and max are null, returns true.
if (!this.isNumeric(val)) return false;
const b = new Big(val);
return (b.gte(min) && b.lte(max));
}
withinRangeLen({ val, min = 1, max = Number.MAX_VALUE }) {
val = "" + val;
return (val.length >= min && val.length <= max);
}
isPositiveInt({ val, allowZero = true }) {
val = "" + val; // cast to string
if (val === "0" && allowZero === false) return false;
return /^\+?\d+$/.test(val);
}
isEmail(email) {
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
isURL(s) {
return /https?\:\/\/(\S+)$/.test(s);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment