Last active
January 19, 2019 23:02
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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