valia
Version:
A runtime data validator in TypeScript with advanced type inference, built-in validation functions, and seamless integration for server and client environments.
1,560 lines (1,519 loc) • 54.1 kB
JavaScript
class Issue extends Error {
constructor(context, message, plugin) {
super(message);
const red = "\x1b[31m", cyan = "\x1b[36m", gray = "\x1b[90m", reset = "\x1b[0m";
const emitter = "Valia" + (plugin ? ":" + plugin : "");
const timestamp = new Date().toISOString();
this.message =
`\n${red}[Error]${reset} ${cyan}[${emitter}]${reset} ${gray}${timestamp}${reset}` +
`\nContext: ${context}` +
`\nMessage: ${message}`;
}
}
class FormatsManager {
constructor() {
this.store = new Map();
}
add(formats) {
for (const format of formats) {
this.store.set(format.type, format);
}
}
get(type) {
const format = this.store.get(type);
if (!format)
throw new Issue("Formats Manager", "The format of type '" + type + "' is unknown.");
return (format);
}
}
class EventsManager {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
emit(event, ...args) {
const callbacks = this.listeners.get(event);
if (!callbacks)
return;
for (const callback of callbacks) {
callback(...args);
}
}
off(event, callback) {
const listeners = this.listeners.get(event);
if (!listeners)
return;
const index = listeners.indexOf(callback);
if (index !== -1)
listeners.splice(index, 1);
}
}
const nodeSymbol = Symbol("node");
function hasNodeSymbol(obj) {
return (typeof obj === "object" && Reflect.has(obj, nodeSymbol));
}
class MountingStack {
constructor(rootNode) {
this.tasks = [];
this.tasks.push({
node: rootNode,
partPaths: { explicit: [], implicit: [] },
fullPaths: { explicit: [], implicit: [] }
});
}
pushChunk(sourceTask, chunk) {
const { fullPaths } = sourceTask;
for (let i = 0; i < chunk.length; i++) {
const { node, partPaths } = chunk[i];
this.tasks.push({
node,
partPaths,
fullPaths: {
explicit: fullPaths.explicit.concat(partPaths.explicit),
implicit: fullPaths.implicit.concat(partPaths.implicit)
}
});
}
}
}
function mounter(managers, rootNode) {
const { formats, events } = managers;
const stack = new MountingStack(rootNode);
while (stack.tasks.length) {
const currentTask = stack.tasks.pop();
const { node, partPaths, fullPaths } = currentTask;
if (hasNodeSymbol(node)) {
node[nodeSymbol] = {
...node[nodeSymbol],
partPaths
};
}
else {
const format = formats.get(node.type);
const chunk = [];
format.mount?.(chunk, node);
Object.assign(node, {
...node,
[nodeSymbol]: {
childNodes: chunk.map((task) => task.node),
partPaths
}
});
Object.freeze(node);
if (chunk.length)
stack.pushChunk(currentTask, chunk);
events.emit("NODE_MOUNTED", node, fullPaths);
}
}
events.emit("TREE_MOUNTED", rootNode);
return rootNode;
}
function createReject(task, code) {
return ({
code,
path: task.fullPaths,
type: task.node.type,
label: task.node.label,
message: task.node.message
});
}
class CheckingStack {
constructor(rootNode, rootData) {
this.tasks = [];
this.tasks.push({
data: rootData,
node: rootNode,
fullPaths: { explicit: [], implicit: [] }
});
}
pushChunk(sourceTask, chunk) {
for (let i = 0; i < chunk.length; i++) {
const currentTask = chunk[i];
const partPaths = currentTask.node[nodeSymbol].partPaths;
let stackHooks = sourceTask.stackHooks;
if (currentTask.hooks) {
const hooks = {
owner: sourceTask,
index: {
chunk: this.tasks.length - i,
branch: this.tasks.length
},
...currentTask.hooks
};
stackHooks = stackHooks ? stackHooks.concat(hooks) : [hooks];
}
this.tasks.push({
data: currentTask.data,
node: currentTask.node,
fullPaths: {
explicit: sourceTask.fullPaths.explicit.concat(partPaths.explicit),
implicit: sourceTask.fullPaths.implicit.concat(partPaths.implicit)
},
stackHooks
});
}
}
callHooks(currentTask, reject) {
const stackHooks = currentTask.stackHooks;
if (!stackHooks)
return (null);
const lastHooks = stackHooks[stackHooks.length - 1];
if (!reject && lastHooks.index.branch !== this.tasks.length) {
return (null);
}
for (let i = stackHooks.length - 1; i >= 0; i--) {
const hooks = stackHooks[i];
const claim = reject ? hooks.onReject(reject) : hooks.onAccept();
switch (claim.action) {
case "DEFAULT":
this.tasks.length = hooks.index.branch;
if (!reject)
return (null);
continue;
case "REJECT":
this.tasks.length = hooks.index.branch;
reject = createReject(hooks.owner, claim.code);
continue;
case "IGNORE":
if (claim?.target === "CHUNK") {
this.tasks.length = hooks.index.chunk;
}
else {
this.tasks.length = hooks.index.branch;
}
return (null);
}
}
return (reject);
}
}
function checker(managers, rootNode, rootData) {
const { formats, events } = managers;
const stack = new CheckingStack(rootNode, rootData);
let reject = null;
while (stack.tasks.length) {
const currentTask = stack.tasks.pop();
const { data, node, stackHooks } = currentTask;
const chunk = [];
let code = null;
if (!(node.nullish && data == null)) {
const format = formats.get(node.type);
code = format.check(chunk, node, data);
}
if (code)
reject = createReject(currentTask, code);
else if (chunk.length)
stack.pushChunk(currentTask, chunk);
if (stackHooks)
reject = stack.callHooks(currentTask, reject);
if (reject)
break;
}
events.emit("DATA_CHECKED", rootNode, rootData, reject);
return (reject);
}
function getInternalTag(target) {
return (Object.prototype.toString.call(target).slice(8, -1));
}
function convertBase16ToBase64(input, base64, padding) {
const totalChunksLength = Math.floor(input.length / 6) * 6;
let output = "";
let i = 0;
while (i < totalChunksLength) {
const dec = parseInt(input.slice(i, i + 6), 16);
output += (base64[((dec >> 18) & 63)]
+ base64[((dec >> 12) & 63)]
+ base64[((dec >> 6) & 63)]
+ base64[(dec & 63)]);
i += 6;
}
if (i < input.length) {
const restChunk = input.slice(i, i + 6);
// 143016576 = 00100 01000 01100 10000 10100 00000 = 4 8 12 16 20 0
const leftShift = (143016576 >> (restChunk.length * 5)) & 31;
const dec = parseInt(restChunk, 16) << leftShift;
output += base64[((dec >> 18) & 63)]
+ base64[((dec >> 12) & 63)];
if (leftShift < 12)
output += base64[((dec >> 6) & 63)];
if (leftShift < 8)
output += base64[(dec & 63)];
}
while (padding && output.length % 4 !== 0) {
output += '=';
}
return (output);
}
function convertBase16ToBase32(input, base32, padding = true) {
const totalChunksLength = Math.floor(input.length / 10) * 10;
let output = "";
let i = 0;
while (i < totalChunksLength) {
const decHigh = parseInt(input.slice(i, i + 5), 16);
const decLow = parseInt(input.slice(i + 5, i + 10), 16);
output += base32[((decHigh >> 15) & 31)]
+ base32[((decHigh >> 10) & 31)]
+ base32[((decHigh >> 5) & 31)]
+ base32[(decHigh & 31)]
+ base32[((decLow >> 15) & 31)]
+ base32[((decLow >> 10) & 31)]
+ base32[((decLow >> 5) & 31)]
+ base32[(decLow & 31)];
i += 10;
}
if (i < input.length) {
const restChunk = input.slice(i, i + 5);
// 4469248 = 00100 01000 01100 10000 00000 = 4 8 12 16 0
const leftShift = (4469248 >> (restChunk.length * 5)) & 31;
const decHigh = parseInt(restChunk, 16) << leftShift;
output += base32[((decHigh >> 15) & 31)]
+ base32[((decHigh >> 10) & 31)];
if (leftShift < 12) {
output += base32[((decHigh >> 5) & 31)]
+ base32[(decHigh & 31)];
}
}
if (i + 5 < input.length) {
const restChunk = input.slice(i + 5, i + 10);
// 4469248 = 00100 01000 01100 10000 00000 = 4 8 12 16 0
const leftShift = (4469248 >> (restChunk.length * 5)) & 31;
const decLow = parseInt(restChunk, 16) << leftShift;
output += base32[((decLow >> 15) & 31)]
+ base32[((decLow >> 10) & 31)];
if (leftShift < 12)
output += base32[((decLow >> 5) & 31)];
if (leftShift < 8)
output += base32[(decLow & 31)];
}
while (padding && output.length % 8 !== 0) {
output += '=';
}
return (output);
}
function convertBase64ToBase16(input, base64) {
if (input.endsWith("="))
input = input.slice(0, input.indexOf("="));
const totalChunksLength = Math.floor(input.length / 4) * 4;
const base16 = "0123456789ABCDEF";
let output = "";
let i = 0;
while (i < totalChunksLength) {
const dec = (base64.indexOf(input[i]) << 18)
| (base64.indexOf(input[i + 1]) << 12)
| (base64.indexOf(input[i + 2]) << 6)
| base64.indexOf(input[i + 3]);
output += base16[((dec >> 20) & 15)]
+ base16[((dec >> 16) & 15)]
+ base16[((dec >> 12) & 15)]
+ base16[((dec >> 8) & 15)]
+ base16[((dec >> 4) & 15)]
+ base16[(dec & 15)];
i += 4;
}
if (i < input.length) {
const rest = input.slice(i);
const restLength = rest.length;
const dec = ((base64.indexOf(rest[0]) << 18)
| (rest[1] ? base64.indexOf(rest[1]) << 12 : 0)
| (rest[2] ? base64.indexOf(rest[2]) << 6 : 0)
| (rest[3] ? base64.indexOf(rest[3]) : 0));
output += base16[((dec >> 20) & 15)]
+ base16[((dec >> 16) & 15)];
if (restLength > 2) {
output += base16[((dec >> 12) & 15)]
+ base16[((dec >> 8) & 15)];
}
if (restLength > 3) {
output += base16[((dec >> 4) & 15)]
+ base16[(dec & 15)];
}
}
return (output);
}
function convertBase32ToBase16(input, base32) {
if (input.endsWith("="))
input = input.slice(0, input.indexOf("="));
const totalChunksLength = Math.floor(input.length / 8) * 8;
const base16 = "0123456789ABCDEF";
let output = "";
let i = 0;
while (i < totalChunksLength) {
const dec = (base32.indexOf(input[i]) << 15)
| (base32.indexOf(input[i + 1]) << 10)
| (base32.indexOf(input[i + 2]) << 5)
| base32.indexOf(input[i + 3]);
output += base16[((dec >> 16) & 15)]
+ base16[((dec >> 12) & 15)]
+ base16[((dec >> 8) & 15)]
+ base16[((dec >> 4) & 15)]
+ base16[(dec & 15)];
i += 4;
}
if (i < input.length) {
const rest = input.slice(i);
const restLength = rest.length;
const dec = ((base32.indexOf(rest[0]) << 15)
| (rest[1] ? base32.indexOf(rest[1]) << 10 : 0)
| (rest[2] ? base32.indexOf(rest[2]) << 5 : 0)
| (rest[3] ? base32.indexOf(rest[3]) : 0));
output += base16[((dec >> 16) & 15)]
+ base16[((dec >> 12) & 15)];
if (restLength > 1) {
output += base16[((dec >> 8) & 15)]
+ base16[((dec >> 4) & 15)];
}
if (restLength > 3) {
output += base16[(dec & 15)];
if (i + 5 >= input.length) {
output += base16[0];
}
}
}
if (i + 5 < input.length) {
const rest = input.slice(i + 5);
const restLength = rest.length;
const dec = ((base32.indexOf(rest[0]) << 15)
| (rest[1] ? base32.indexOf(rest[1]) << 10 : 0)
| (rest[2] ? base32.indexOf(rest[2]) << 5 : 0)
| (rest[3] ? base32.indexOf(rest[3]) : 0));
output += base16[((dec >> 16) & 15)]
+ base16[((dec >> 12) & 15)]
+ base16[((dec >> 8) & 15)];
if (restLength > 2) {
output += base16[((dec >> 4) & 15)]
+ base16[(dec & 15)];
}
}
return (output);
}
function base16ToBase64(input, to = "B64", padding = true) {
if (to === "B64") {
const base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
return (convertBase16ToBase64(input, base64, padding));
}
else if (to === "B64URL") {
const base64Url = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
return (convertBase16ToBase64(input, base64Url, padding));
}
else {
throw new Issue("Parameters", "The base64 type of the parameter 'to' is unknown.");
}
}
function base16ToBase32(input, to = "B16", padding = true) {
if (to === "B16") {
const base32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
return (convertBase16ToBase32(input, base32, padding));
}
else if (to === "B16HEX") {
const base32Hex = "0123456789ABCDEFGHIJKLMNOPQRSTUV";
return (convertBase16ToBase32(input, base32Hex, padding));
}
else {
throw new Issue("Parameters", "The base32 type of the parameter 'to' is unknown.");
}
}
function base64ToBase16(input, from = "B64") {
if (from === "B64") {
const base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
return (convertBase64ToBase16(input, base64));
}
else if (from === "B64URL") {
const base64Url = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
return (convertBase64ToBase16(input, base64Url));
}
else {
throw new Issue("Parameters", "The base64 type of the parameter 'from' is unknown.");
}
}
function base32ToBase16(input, from = "B16") {
if (from === "B16") {
const base32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
return (convertBase32ToBase16(input, base32));
}
else if (from === "B16HEX") {
const base32Hex = "0123456789ABCDEFGHIJKLMNOPQRSTUV";
return (convertBase32ToBase16(input, base32Hex));
}
else {
throw new Issue("Parameters", "The base32 type of the parameter 'from' is unknown.");
}
}
// OBJECT
function isObject(x) {
return (typeof x === "object");
}
/**
* A plain object is considered as follows:
* - It must be an object.
* - It must have a prototype of `Object.prototype` or `null`.
*/
function isPlainObject(x) {
if (x === null || typeof x !== "object")
return (false);
const prototype = Object.getPrototypeOf(x);
if (prototype !== Object.prototype && prototype !== null) {
return (false);
}
return (true);
}
// ARRAY
function isArray(x) {
return (Array.isArray(x));
}
function isTypedArray(x) {
return (ArrayBuffer.isView(x) && !(x instanceof DataView));
}
// FUNCTION
function isFunction(x) {
return (typeof x === "function");
}
/**
* A basic function is considered as follows:
* - It must be an function.
* - It must not be an `async`, `generator` or `async generator` function.
*/
function isBasicFunction(x) {
return (getInternalTag(x) === "Function");
}
function isAsyncFunction(x) {
return (getInternalTag(x) === "AsyncFunction");
}
function isGeneratorFunction(x) {
return (getInternalTag(x) === "GeneratorFunction");
}
function isAsyncGeneratorFunction(x) {
return (getInternalTag(x) === "AsyncGeneratorFunction");
}
var objectTesters = /*#__PURE__*/Object.freeze({
__proto__: null,
isArray: isArray,
isAsyncFunction: isAsyncFunction,
isAsyncGeneratorFunction: isAsyncGeneratorFunction,
isBasicFunction: isBasicFunction,
isFunction: isFunction,
isGeneratorFunction: isGeneratorFunction,
isObject: isObject,
isPlainObject: isPlainObject,
isTypedArray: isTypedArray
});
/**
* Check if all characters of the string are in the ASCII table (%d0-%d127).
*
* If you enable `onlyPrintable` valid characters will be limited to
* printable characters from the ASCII table (%32-%d126).
*
* Empty returns `false`.
*/
function isAscii(str, config) {
if (config?.onlyPrintable)
return (RegExp("^[\\x20-\\x7E]+$").test(str));
return (RegExp("^[\\x00-\\x7F]+$").test(str));
}
/*
Composition :
DIGIT = %x30-39
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
hexOctet = HEXDIG HEXDIG
uuid = 4*4hexOctet "-"
2*2hexOctet "-"
2*2hexOctet "-"
2*2hexOctet "-"
6*6hexOctet
Sources :
RFC 9562 Section 4 : DIGIT
HEXDIG
hexOctet
UUID -> uuid
Links :
https://datatracker.ietf.org/doc/html/rfc9562#section-4
*/
const extractUuidVersionRegex = new RegExp("^[0-9A-F]{8}-[0-9A-F]{4}-([1-7])[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", "i");
/**
* **Standard :** RFC 9562
*
* @version 1.0.0
*/
function isUuid(str, params) {
const extracted = extractUuidVersionRegex.exec(str);
if (!extracted || !extracted[1])
return (false);
if (!params?.version || (extracted[1].codePointAt(0) - 48) === params?.version)
return (true);
return (false);
}
function weak(callback) {
let ref = null;
return (() => {
if (!ref) {
const obj = callback();
ref = new WeakRef(obj);
return (obj);
}
const value = ref.deref();
if (!value) {
const obj = callback();
ref = new WeakRef(obj);
return (obj);
}
return (value);
});
}
/**
# IPV4
Composition :
dec-octet = 1*3DIGIT ; Representing a decimal integer value in the range 0 through 255
prefix = 1*2DIGIT ; Representing a decimal integer value in the range 0 through 32.
IPv4 = dec-octet 3("." dec-octet) ["/" prefix]
# IPV6
Composition :
HEXDIG = DIGIT / A-F / a-f
IPv6-full = 1*4HEXDIG 7(":" 1*4HEXDIG)
IPv6-comp = [1*4HEXDIG *5(":" 1*4HEXDIG)] "::" [1*4HEXDIG *5(":" 1*4HEXDIG)]
IPv6v4-full = 1*4HEXDIG 5(":" 1*4HEXDIG) ":" IPv4
IPv6v4-comp = [1*4HEXDIG *3(":" 1*4HEXDIG)] "::" [1*4HEXDIG *3(":" 1*4HEXDIG) ":"] IPv4
prefix = 1*3DIGIT ; Representing a decimal integer value in the range 0 through 128.
IPv6 = (IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp) ["/" prefix]
*/
const ipV4Seg = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])";
const ipV4Pattern = `(?:${ipV4Seg}\\.){3}${ipV4Seg}`;
const ipV4SimpleRegex = new RegExp(`^${ipV4Pattern}$`);
const ipV4PrefixRegex = weak(() => new RegExp(`^${ipV4Pattern}/(3[0-2]|[12]?[0-9])$`));
const ipV6Seg = "(?:[0-9a-fA-F]{1,4})";
const ipV6Pattern = "(?:" +
`(?:${ipV6Seg}:){7}(?:${ipV6Seg}|:)|` +
`(?:${ipV6Seg}:){6}(?:${ipV4Pattern}|:${ipV6Seg}|:)|` +
`(?:${ipV6Seg}:){5}(?::${ipV4Pattern}|(?::${ipV6Seg}){1,2}|:)|` +
`(?:${ipV6Seg}:){4}(?:(?::${ipV6Seg}){0,1}:${ipV4Pattern}|(?::${ipV6Seg}){1,3}|:)|` +
`(?:${ipV6Seg}:){3}(?:(?::${ipV6Seg}){0,2}:${ipV4Pattern}|(?::${ipV6Seg}){1,4}|:)|` +
`(?:${ipV6Seg}:){2}(?:(?::${ipV6Seg}){0,3}:${ipV4Pattern}|(?::${ipV6Seg}){1,5}|:)|` +
`(?:${ipV6Seg}:){1}(?:(?::${ipV6Seg}){0,4}:${ipV4Pattern}|(?::${ipV6Seg}){1,6}|:)|` +
`(?::(?:(?::${ipV6Seg}){0,5}:${ipV4Pattern}|(?::${ipV6Seg}){1,7}|:)))`;
const ipV6SimpleRegex = new RegExp(`^${ipV6Pattern}$`);
const ipV6PrefixRegex = weak(() => new RegExp(`^${ipV6Pattern}/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$`));
/**
* **Standard:** No standard
*
* @version 1.0.0
*/
function isIp(str, params) {
if (!params?.allowPrefix && ipV4SimpleRegex.test(str))
return (true);
else if (params?.allowPrefix && ipV4PrefixRegex().test(str))
return (true);
if (!params?.allowPrefix && ipV6SimpleRegex.test(str))
return (true);
else if (params?.allowPrefix && ipV6PrefixRegex().test(str))
return (true);
return (false);
}
/**
* **Standard:** No standard
*
* @version 1.0.0
*/
function isIpV4(str, params) {
if (!params?.allowPrefix && ipV4SimpleRegex.test(str))
return (true);
else if (params?.allowPrefix && ipV4PrefixRegex().test(str))
return (true);
return (false);
}
/**
* **Standard:** No standard
*
* @version 1.0.0
*/
function isIpV6(str, params) {
if (!params?.allowPrefix && ipV4SimpleRegex.test(str))
return (true);
else if (params?.allowPrefix && ipV4PrefixRegex().test(str))
return (true);
return (false);
}
/*
Composition :
letter = %d65-%d90 / %d97-%d122; A-Z / a-z
digit = %x30-39; 0-9
label = letter [*(digit / letter / "-") digit / letter]
domain = label *("." label)
Links :
https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.1
*/
const domainRegex = new RegExp("^[A-Za-z](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\\.[A-Za-z](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*$");
/**
* **Standard :** RFC 1035
*
* @version 1.0.0
*/
function isDomain(str, params) {
return (domainRegex.test(str));
}
/*
Composition :
atom = 1*atext
dot-local = atom *("." atom)
quoted-local = DQUOTE *QcontentSMTP DQUOTE
ip-address = IPv4-address-literal / IPv6-address-literal
general-address = General-address-literal
local = dot-local / quote-local
domain = Domain
address = ip-address / general-address
mailbox = local "@" (domain / address)
Sources :
RFC 5234 Appendix B.1 : DQUOTE
RFC 5322 Section 3.2.3 : atext
RFC 5321 Section 4.1.3 : IPv4-address-literal
IPv6-address-literal
General-address-literal
RFC 5321 Section 4.1.2 : QcontentSMTP
Domain
Links :
https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1
https://datatracker.ietf.org/doc/html/rfc5322#section-3.2.3
https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.3
https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2
*/
const dotStringPattern = "(?:[-!=?A-B\\x23-\\x27\\x2A-\\x2B\\x2F-\\x39\\x5E-\\x7E]+(?:\\.[-!=?A-B\\x23-\\x27\\x2A-\\x2B\\x2F-\\x39\\x5E-\\x7E]+)*)";
const quotedStringPattern = "(?:\"(?:[\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]|\\\\[\\x20-\\x7E])*\")";
const dotLocalRegex = new RegExp(`^${dotStringPattern}$`);
const dotOrQuoteLocalRegex = weak(() => new RegExp(`^(?:${dotStringPattern}|${quotedStringPattern})$`));
const ipAddressRegex = weak(() => new RegExp(`^\\[(?:IPv6:${ipV6Pattern}|${ipV4Pattern})\\]$`));
const generalAddressRegex = weak(() => new RegExp(`(?:[a-zA-Z0-9-]*[a-zA-Z0-9]+:[\\x21-\\x5A\\x5E-\\x7E]+)`));
function parseEmail(str) {
const length = str.length;
let i = 0;
// EXTRACT LOCAL
const localStart = i;
if (str[localStart] === "\"") {
while (++i < length) {
if (str[i] === "\\")
i++;
else if (str[i] === "\"") {
i++;
break;
}
}
}
else {
while (i < length && str[i] !== "@")
i++;
}
if (i === localStart || str[i] !== "@")
return (null);
const localEnd = i;
// EXTRACT DOMAIN
const domainStart = ++i;
const domainEnd = length;
if (domainStart === domainEnd)
return (null);
return ({
local: str.slice(localStart, localEnd),
domain: str.slice(domainStart, domainEnd)
});
}
function isValidLocal(str, params) {
if (dotLocalRegex.test(str))
return (true);
if (params?.allowQuotedString
&& dotOrQuoteLocalRegex().test(str))
return (true);
return (false);
}
function isValidDomain(str, params) {
if (isDomain(str))
return (true);
if (params?.allowIpAddress
&& ipAddressRegex().test(str))
return (true);
if (params?.allowGeneralAddress
&& generalAddressRegex().test(str))
return (true);
return (false);
}
/**
* **Standard :** RFC 5321
*
* @version 2.0.0
*/
function isEmail(str, params) {
const email = parseEmail(str);
if (!email)
return (false);
// CHECK LOCAL
if (!isValidLocal(email.local, params))
return (false);
// CHECK DOMAIN
if (!isValidDomain(email.domain, params))
return (false);
// RFC 5321 4.5.3.1.2 : Length restriction
if (!email.domain.length || email.domain.length > 255)
return (false);
return (true);
}
/*
Composition :
data = pchar
value = value
token = restricted-name
mediatype = [token "/" token] *(";" token "=" value)
dataurl = "data:" [mediatype] [";base64"] "," data
Sources :
RFC 3986 Section 3.3 : pchar
RFC 2045 Section 5.1 : value
RFC 6838 Section 4.2 : restricted-name
Links :
https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
https://datatracker.ietf.org/doc/html/rfc6838#section-4.2
https://datatracker.ietf.org/doc/html/rfc2397#section-3
*/
const paramTokenPattern = "[a-zA-Z0-9!#$%&'*+.^_`{|}~-]+";
const paramTokenQuotePattern = "\"[a-zA-Z0-9!#$%&'()*+,./:;<=>?@\[\\\]^_`{|}~-]+\"";
const valueRegex = new RegExp(`^(?:${paramTokenPattern}|${paramTokenQuotePattern})$`);
const tokenRegex = new RegExp(`^[a-zA-Z0-9](?:[a-zA-Z0-9!#$&^/_.+-]{0,125}[a-zA-Z0-9!#$&^/_.-])?$`);
const dataRegex = new RegExp(`^(?:[a-zA-Z0-9._~!$&'()*+,;=:@-]|%[a-zA-Z0-9]{2})*$`);
function parseDataUrl(str) {
const result = {
data: "",
type: "",
subtype: "",
parameters: [],
isBase64: false
};
let i = 0;
if (!str.startsWith("data:"))
return (null);
i += 5;
if (str[i] !== ";" && str[i] !== ",") {
// EXTRACT TYPE
const typeStart = i;
while (str[i] && str[i] !== "/")
i++;
if (!str[i] || typeStart === i)
return (null);
const typeEnd = i;
// EXTRACT SUBTYPE
const subtypeStart = ++i;
while (str[i] && str[i] !== ";" && str[i] !== ",")
i++;
if (!str[i] || subtypeStart === i)
return (null);
const subtypeEnd = i;
result.type = str.slice(typeStart, typeEnd);
result.subtype = str.slice(subtypeStart, subtypeEnd);
}
// EXTRACT PARAMETERS
while (str[i] && str[i] === ";") {
if (str.startsWith(";base64,", i)) {
result.isBase64 = true;
i += 7;
break;
}
const nameStart = ++i;
while (str[i] && str[i] !== "=")
i++;
if (!str[i] || nameStart === i)
return (null);
const nameEnd = i;
const valueStart = ++i;
if (str[valueStart] === "\"") {
while (str[i] && !(str[i - 1] === "\"" && (str[i] === ";" || str[i] === ",")))
i++;
}
else {
while (str[i] && str[i] !== ";" && str[i] !== ",")
i++;
}
if (!str[i] || valueStart === i)
return (null);
const valueEnd = i;
result.parameters.push({
name: str.slice(nameStart, nameEnd),
value: str.slice(valueStart, valueEnd)
});
}
if (str[i] !== ",")
return (null);
i += 1;
// EXTRACT DATA
if (str[i])
result.data = str.slice(i);
return (result);
}
/**
* **Standard :** RFC 2397 (RFC 2045, RFC 6838, RFC 3986)
*
* @version 2.0.0
*/
function isDataUrl(str, params) {
const dataUrl = parseDataUrl(str);
if (!dataUrl)
return (false);
if (dataUrl.type || dataUrl.subtype) {
// CHECK TYPE
if (!tokenRegex.test(dataUrl.type))
return (false);
// RFC 6838 4.2: Length restriction
if (dataUrl.type.length > 127)
return (false);
// CHECK SUBTYPE
if (!tokenRegex.test(dataUrl.subtype))
return (false);
// RFC 6838 4.2: Length restriction
if (dataUrl.subtype.length > 127)
return (false);
}
// CHECK PARAMETERS
for (let i = 0; i < dataUrl.parameters.length; i++) {
const parameter = dataUrl.parameters[i];
if (!tokenRegex.test(parameter.name))
return (false);
if (!valueRegex.test(parameter.value))
return (false);
// RFC 6838 4.3: Identical name restriction and case insensitive
if (dataUrl.parameters.some(({ name }, j) => j !== i && name.toLowerCase() === name.toLowerCase()))
return (false);
}
// CHECK DATA
if (!dataRegex.test(dataUrl.data))
return (false);
if (params?.type) {
const hasValidType = params.type.some(type => type.toLowerCase() === dataUrl.type.toLowerCase());
if (!hasValidType)
return (false);
}
if (params?.subtype) {
const hasValidSubtype = params.subtype.some(subtype => subtype.toLowerCase() === dataUrl.subtype.toLowerCase());
if (!hasValidSubtype)
return (false);
}
return (true);
}
const base16Regex = new RegExp("^(?:[A-F0-9]{2})*$");
const base32Regex = new RegExp("^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}[=]{6}|[A-Z2-7]{4}[=]{4}|[A-Z2-7]{5}[=]{3}|[A-Z2-7]{6}[=]{2}|[A-Z2-7]{7}[=]{1})?$");
const base32HexRegex = weak(() => new RegExp("^(?:[0-9A-V]{8})*(?:[0-9A-V]{2}[=]{6}|[0-9A-V]{4}[=]{4}|[0-9A-V]{5}[=]{3}|[0-9A-V]{6}[=]{2}|[0-9A-V]{7}[=]{1})?$"));
const base64Regex = new RegExp("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}[=]{2}|[A-Za-z0-9+/]{3}[=]{1})?$");
const base64UrlRegex = weak(() => new RegExp("^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}[=]{2}|[A-Za-z0-9_-]{3}[=]{1})?$"));
/**
* **Standard :** RFC 4648
*
* @see https://datatracker.ietf.org/doc/html/rfc4648#section-4
*
* @version 1.0.0
*/
function isBase64(str, params) {
if (typeof str !== "string")
new Issue("Parameters", "'str' must be of type string.");
return (str.length % 4 == 0 && base64Regex.test(str));
}
/**
* **Standard :** RFC 4648
*
* @see https://datatracker.ietf.org/doc/html/rfc4648#section-5
*
* @version 1.0.0
*/
function isBase64Url(str, params) {
if (typeof str !== "string")
new Issue("Parameters", "'str' must be of type string.");
return (str.length % 4 === 0 && base64UrlRegex().test(str));
}
/**
* **Standard :** RFC 4648
*
* @see https://datatracker.ietf.org/doc/html/rfc4648#section-6
*
* @version 1.0.0
*/
function isBase32(str, params) {
if (typeof str !== "string")
new Issue("Parameters", "'str' must be of type string.");
return (str.length % 8 === 0 && base32Regex.test(str));
}
/**
* **Standard :** RFC 4648
*
* @see https://datatracker.ietf.org/doc/html/rfc4648#section-7
*
* @version 1.0.0
*/
function isBase32Hex(str, params) {
if (typeof str !== "string")
new Issue("Parameters", "'str' must be of type string.");
return (str.length % 8 === 0 && base32HexRegex().test(str));
}
/**
* **Standard :** RFC 4648
*
* @see https://datatracker.ietf.org/doc/html/rfc4648#section-8
*
* @version 1.0.0
*/
function isBase16(str, params) {
if (typeof str !== "string")
new Issue("Parameters", "'str' must be of type string.");
return (str.length % 2 === 0 && base16Regex.test(str));
}
var stringTesters = /*#__PURE__*/Object.freeze({
__proto__: null,
isAscii: isAscii,
isBase16: isBase16,
isBase32: isBase32,
isBase32Hex: isBase32Hex,
isBase64: isBase64,
isBase64Url: isBase64Url,
isDataUrl: isDataUrl,
isDomain: isDomain,
isEmail: isEmail,
isIp: isIp,
isIpV4: isIpV4,
isIpV6: isIpV6,
isUuid: isUuid
});
const testers = {
object: objectTesters,
string: stringTesters
};
/**
* Clones the object starting from the root and stops traversing a branch
* when a mounted criteria node is encountered. In such cases, the mounted
* object encountered see its internal properties copied to a new reference
* so that the junction is a unique reference in the tree.
*
* @param src Source object of the clone
* @returns Clone of the source object
*/
function cloner(rootSrc) {
let rootCpy = {};
let stack = [{
src: rootSrc,
cpy: rootCpy
}];
while (stack.length > 0) {
let { src, cpy } = stack.pop();
if (isPlainObject(src)) {
if (hasNodeSymbol(src)) {
cpy = { ...src };
}
else {
const keys = Reflect.ownKeys(src);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (isPlainObject(src[key])) {
if (hasNodeSymbol(src[key])) {
cpy[key] = { ...src[key] };
}
else {
cpy[key] = {};
stack.push({
src: src[key],
cpy: cpy[key]
});
}
}
else if (isArray(src[key])) {
cpy[key] = [];
stack.push({
src: src[key],
cpy: cpy[key]
});
}
else {
cpy[key] = src[key];
}
}
}
}
else if (isArray(src)) {
for (let i = 0; i < src.length; i++) {
const index = i;
if (isPlainObject(src[index])) {
if (hasNodeSymbol(src[index])) {
cpy[i] = { ...src[index] };
}
else {
cpy[i] = {};
stack.push({
src: src[index],
cpy: cpy[index]
});
}
}
else if (isArray(src[index])) {
cpy[index] = [];
stack.push({
src: src[index],
cpy: cpy[index]
});
}
else {
cpy[index] = src[index];
}
}
}
else {
cpy = src;
}
}
return rootCpy;
}
const BooleanFormat = {
type: "boolean",
check(chunk, criteria, value) {
if (typeof value !== "boolean") {
return ("TYPE.BOOLEAN.NOT_SATISFIED");
}
return (null);
},
};
const SymbolFormat = {
type: "symbol",
check(chunk, criteria, value) {
if (typeof value !== "symbol") {
return "TYPE.SYMBOL.NOT_SATISFIED";
}
else if (criteria.symbol && value !== criteria.symbol) {
return "SYMBOL.NOT_ALLOWED";
}
return (null);
}
};
const NumberFormat = {
type: "number",
mount(chunk, criteria) {
Object.assign(criteria, {
empty: criteria.empty ?? true
});
},
check(chunk, criteria, value) {
if (typeof value !== "number") {
return ("TYPE.NUMBER.NOT_SATISFIED");
}
else if (value === 0) {
return (criteria.empty ? null : "EMPTY.NOT_ALLOWED");
}
else if (criteria.min != null && value < criteria.min) {
return ("MIN.NOT_SATISFIED");
}
else if (criteria.max != null && value > criteria.max) {
return ("MAX.NOT_SATISFIED");
}
else if (criteria.enum != null) {
if (isPlainObject(criteria.enum) && !Object.values(criteria.enum).includes(value)) {
return ("ENUM.NOT_SATISFIED");
}
else if (isArray(criteria.enum) && !criteria.enum.includes(value)) {
return ("ENUM.NOT_SATISFIED");
}
}
else if (criteria.custom && !criteria.custom(value)) {
return ("CUSTOM.NOT_SATISFIED");
}
return (null);
}
};
const StringFormat = {
type: "string",
mount(chunk, criteria) {
Object.assign(criteria, {
empty: criteria.empty ?? true
});
},
check(chunk, criteria, value) {
if (typeof value !== "string") {
return ("TYPE.STRING.NOT_SATISFIED");
}
const valueLength = value.length;
if (!valueLength) {
return (criteria.empty ? null : "EMPTY.NOT_ALLOWED");
}
else if (criteria.min != null && valueLength < criteria.min) {
return ("MIN.LENGTH.NOT_SATISFIED");
}
else if (criteria.max != null && valueLength > criteria.max) {
return ("MAX.LENGTH.NOT_SATISFIED");
}
else if (criteria.enum != null) {
if (isArray(criteria.enum) && !criteria.enum.includes(value)) {
return ("ENUM.NOT_SATISFIED");
}
else if (!Object.values(criteria.enum).includes(value)) {
return ("ENUM.NOT_SATISFIED");
}
}
else if (criteria.regex != null && !criteria.regex.test(value)) {
return ("REGEX.NOT_SATISFIED");
}
else if (criteria.testers) {
for (const key of Object.keys(criteria.testers)) {
if (!(testers.string[key](value, criteria.testers[key]))) {
return ("TESTER.NOT_SATISFIED");
}
}
}
else if (criteria.custom && !criteria.custom(value)) {
return ("CUSTOM.NOT_SATISFIED");
}
return (null);
}
};
const SimpleFormat = {
type: "simple",
bitflags: {
null: 1 << 0,
undefined: 1 << 1,
nullish: 1 << 2,
unknown: 1 << 3
},
mount(chunk, criteria) {
Object.assign(criteria, {
bitcode: this.bitflags[criteria.simple]
});
},
check(chunk, criteria, value) {
const { bitflags } = this, { bitcode } = criteria;
if (bitcode & bitflags.unknown) {
return (null);
}
if (bitcode & bitflags.nullish && value != null) {
return ("TYPE.NULLISH.NOT_SATISFIED");
}
else if (bitcode & bitflags.null && value !== null) {
return ("TYPE.NULL.NOT_SATISFIED");
}
else if ((bitcode & bitflags.undefined) && value !== undefined) {
return ("TYPE.UNDEFINED.NOT_SATISFIED");
}
return (null);
}
};
const RecordFormat = {
type: "record",
mount(chunk, criteria) {
Object.assign(criteria, {
empty: criteria.empty ?? true
});
chunk.push({
node: criteria.key,
partPaths: {
explicit: ["key"],
implicit: []
}
});
chunk.push({
node: criteria.value,
partPaths: {
explicit: ["value"],
implicit: ["%", "string", "symbol"]
}
});
},
check(chunk, criteria, data) {
if (!isPlainObject(data)) {
return ("TYPE.PLAIN_OBJECT.NOT_SATISFIED");
}
const keys = Reflect.ownKeys(data);
const keysLength = keys.length;
if (keysLength === 0) {
return (criteria.empty ? null : "EMPTY.NOT_ALLOWED");
}
else if (criteria.min != null && keysLength < criteria.min) {
return ("MIN.KEYS.NOT_SATISFIED");
}
else if (criteria.max != null && keysLength > criteria.max) {
return ("MAX.KEYS.NOT_SATISFIED");
}
for (let i = 0; i < keysLength; i++) {
const key = keys[i];
chunk.push({
data: key,
node: criteria.key
}, {
data: data[key],
node: criteria.value
});
}
return (null);
}
};
function getRequiredKeys(optional, acceptedKeys) {
if (optional === true)
return ([]);
if (optional === false)
return ([...acceptedKeys]);
return (acceptedKeys.filter(key => !optional.includes(key)));
}
function isShorthandStruct(obj) {
return (isPlainObject(obj) && typeof obj?.type !== "string");
}
const StructFormat = {
type: "struct",
mount(chunk, criteria) {
const optional = criteria.optional ?? false;
const additional = criteria.additional ?? false;
const acceptedKeys = Reflect.ownKeys(criteria.struct);
const requiredKeys = getRequiredKeys(optional, acceptedKeys);
Object.assign(criteria, {
optional: optional,
additional: additional,
acceptedKeys: new Set(acceptedKeys),
requiredKeys: new Set(requiredKeys)
});
for (const key of acceptedKeys) {
let value = criteria.struct[key];
if (isShorthandStruct(value)) {
value = {
type: "struct",
struct: value
};
criteria.struct[key] = value;
}
chunk.push({
node: value,
partPaths: {
explicit: ["struct", key],
implicit: ["&", key]
}
});
}
if (typeof additional !== "boolean") {
chunk.push({
node: additional,
partPaths: {
explicit: ["additional"],
implicit: []
}
});
}
},
check(chunk, criteria, data) {
if (!isPlainObject(data)) {
return ("TYPE.PLAIN_OBJECT.NOT_SATISFIED");
}
const { acceptedKeys, requiredKeys, additional } = criteria;
const definedKeys = Reflect.ownKeys(data), excessedKeys = [];
if (definedKeys.length < requiredKeys.size) {
return ("STRUCT.KEYS.NOT_SATISFIED");
}
let remainingRequiredKeys = requiredKeys.size;
for (let i = definedKeys.length - 1; i >= 0; i--) {
const key = definedKeys[i];
if (requiredKeys.has(key)) {
remainingRequiredKeys--;
}
else if (remainingRequiredKeys > i) {
return ("STRUCT.KEYS.NOT_SATISFIED");
}
else if (!acceptedKeys.has(key)) {
if (additional) {
excessedKeys.push(key);
continue;
}
else {
return ("STRUCT.KEYS.NOT_SATISFIED");
}
}
chunk.push({
data: data[key],
node: criteria.struct[key]
});
}
if (remainingRequiredKeys)
return ("STRUCT.KEYS.NOT_SATISFIED");
if (excessedKeys.length && typeof additional !== "boolean") {
const excessedProperties = {};
for (let i = 0; i < excessedKeys.length; i++) {
const key = excessedKeys[i];
excessedProperties[key] = data[key];
}
chunk.push({
data: excessedProperties,
node: additional
});
}
return (null);
}
};
const ArrayFormat = {
type: "array",
mount(chunk, criteria) {
Object.assign(criteria, {
empty: criteria.empty ?? true
});
chunk.push({
node: criteria.item,
partPaths: {
explicit: ["item"],
implicit: ["%", "number"],
}
});
},
check(chunk, criteria, data) {
if (!isArray(data)) {
return ("TYPE.ARRAY.NOT_SATISFIED");
}
const dataLength = data.length;
if (!dataLength) {
return (criteria.empty ? null : "EMPTY.NOT_ALLOWED");
}
else if (criteria.min != null && dataLength < criteria.min) {
return ("MIN.LENGTH.NOT_SATISFIED");
}
else if (criteria.max != null && dataLength > criteria.max) {
return ("MAX.LENGTH.NOT_SATISFIED");
}
for (let i = 0; i < dataLength; i++) {
chunk.push({
data: data[i],
node: criteria.item
});
}
return (null);
}
};
function isShorthandTuple(obj) {
return (isArray(obj));
}
const TupleFormat = {
type: "tuple",
mount(chunk, criteria) {
const additional = criteria.additional ?? false;
Object.assign(criteria, {
additional: additional
});
for (let i = 0; i < criteria.tuple.length; i++) {
let item = criteria.tuple[i];
if (isShorthandTuple(item)) {
item = {
type: "tuple",
tuple: item
};
criteria.tuple[i] = item;
}
chunk.push({
node: item,
partPaths: {
explicit: ["tuple", i],
implicit: ["&", i]
}
});
}
if (typeof additional !== "boolean") {
chunk.push({
node: additional,
partPaths: {
explicit: ["additional"],
implicit: []
}
});
}
},
check(chunk, criteria, data) {
if (!isArray(data)) {
return ("TYPE.ARRAY.NOT_SATISFIED");
}
const { tuple, additional } = criteria;
const dataLength = data.length, tupleLength = tuple.length;
if (dataLength < tupleLength) {
return ("TUPLE.ITEMS.NOT_SATISFIED");
}
else if (!additional && dataLength > tupleLength) {
return ("TUPLE.ITEMS.NOT_SATISFIED");
}
for (let i = 0; i < tupleLength; i++) {
chunk.push({
data: data[i],
node: tuple[i]
});
}
if (dataLength > tupleLength && typeof additional !== "boolean") {
chunk.push({
data: data.slice(tupleLength),
node: additional
});
}
return (null);
}
};
const UnionFormat = {
type: "union",
mount(chunk, criteria) {
const unionLength = criteria.union.length;
for (let i = 0; i < unionLength; i++) {
chunk.push({
node: criteria.union[i],
partPaths: {
explicit: ["union", i],
implicit: []
}
});
}
},
check(chunk, criteria, data) {
const unionLength = criteria.union.length;
const total = {
hooked: unionLength,
rejected: 0
};
const hooks = {
onAccept() {
return ({
action: "IGNORE",
target: "CHUNK"
});
},
onReject() {
total.rejected++;
if (total.rejected === total.hooked) {
return ({
action: "REJECT",
code: "UNION.NOT_SATISFIED"
});
}
return ({
action: "IGNORE",
target: "BRANCH"
});
}
};
for (let i = 0; i < unionLength; i++) {
chunk.push({
hooks,
data,
node: criteria.union[i]
});
}
return (null);
}
};
const formatNatives = [
BooleanFormat,
SymbolFormat,
NumberFormat,
StringFormat,
SimpleFormat,
RecordFormat,
StructFormat,
ArrayFormat,
TupleFormat,
UnionFormat
];
/**
* The `Schema` class is used to define and validate data structures,
* ensuring they conform to specified criteria.
*/
class Schema {
initiate(criteria) {
this.managers.formats.add(formatNatives);
const clonedCriteria = cloner(criteria);
this.mountedCriteria = mounter(this.managers, clonedCriteria);
}
constructor(criteria) {
this.managers = {
formats: new FormatsManager(),
events: new EventsManage