kanun 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -27,6 +27,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
 
28
28
  //#endregion
29
29
  let node_async_hooks = require("node:async_hooks");
30
+ let node_url = require("node:url");
31
+ let node_net = require("node:net");
30
32
  let dayjs_plugin_customParseFormat_js = require("dayjs/plugin/customParseFormat.js");
31
33
  dayjs_plugin_customParseFormat_js = __toESM(dayjs_plugin_customParseFormat_js);
32
34
  let dayjs = require("dayjs");
@@ -103,10 +105,12 @@ var en_default = {
103
105
  declined: "The :attribute must be declined.",
104
106
  declined_if: "The :attribute must be declined when :other is :value.",
105
107
  different: "The :attribute and :other must be different.",
108
+ distinct: "The :attribute field has a duplicate value.",
106
109
  digits: "The :attribute must be :digits digits.",
107
110
  digits_between: "The :attribute must be between :min and :max digits.",
108
111
  email: "The :attribute must be a valid email address.",
109
112
  ends_with: "The :attribute must end with one of the following: :values.",
113
+ filled: "The :attribute field must have a value.",
110
114
  exists: "The selected :attribute is invalid.",
111
115
  gt: {
112
116
  number: "The :attribute must be greater than :value.",
@@ -124,6 +128,9 @@ var en_default = {
124
128
  in: "The :attribute must be one of the following :values.",
125
129
  includes: "The :attribute must include one of the following values: :values.",
126
130
  integer: "The :attribute must be an integer.",
131
+ ip: "The :attribute must be a valid IP address.",
132
+ ipv4: "The :attribute must be a valid IPv4 address.",
133
+ ipv6: "The :attribute must be a valid IPv6 address.",
127
134
  json: "The :attribute must be a valid JSON string.",
128
135
  lt: {
129
136
  number: "The :attribute must be less than :value.",
@@ -149,6 +156,7 @@ var en_default = {
149
156
  array: "The :attribute must have at least :min items.",
150
157
  object: "The :attribute must have at least :min items."
151
158
  },
159
+ mac_address: "The :attribute must be a valid MAC address.",
152
160
  not_in: "The selected :attribute is invalid.",
153
161
  not_regex: "The :attribute format is invalid.",
154
162
  not_includes: "The :attribute must not include any of the following values: :values.",
@@ -167,6 +175,10 @@ var en_default = {
167
175
  upper_cases: "The :attribute must contain at least :amount uppercase letters."
168
176
  },
169
177
  present: "The :attribute field must be present.",
178
+ presentsame: "The :attribute field must be present.",
179
+ prohibited: "The :attribute field is prohibited.",
180
+ prohibited_unless: "The :attribute field is prohibited unless :other is in :values.",
181
+ prohibits: "The :attribute field prohibits :values from being present.",
170
182
  regex: "The :attribute format is invalid.",
171
183
  required: "The :attribute field is required.",
172
184
  required_if: "The :attribute field is required when :other is :value.",
@@ -184,8 +196,10 @@ var en_default = {
184
196
  object: "The :attribute must contain :size items."
185
197
  },
186
198
  string: "The :attribute must be a string.",
199
+ timezone: "The :attribute must be a valid timezone.",
187
200
  unique: "The :attribute has already been taken.",
188
- url: "The :attribute must have a valid URL format."
201
+ url: "The :attribute must have a valid URL format.",
202
+ multiple_of: "The :attribute must be a multiple of :value."
189
203
  };
190
204
 
191
205
  //#endregion
@@ -654,6 +668,12 @@ const replaceAttributes = {
654
668
  };
655
669
  return message.replace(/:other|:value/gi, (matched) => values[matched]);
656
670
  },
671
+ replaceProhibitedUnless: function(payload) {
672
+ return this.replaceRequiredUnless(payload);
673
+ },
674
+ replaceProhibits: function({ message, parameters, getDisplayableAttribute }) {
675
+ return message.replace(":values", parameters.map((attribute) => getDisplayableAttribute(attribute)).join(", "));
676
+ },
657
677
  replaceRequiredUnless: function({ message, parameters, getDisplayableAttribute }) {
658
678
  const [other] = parameters;
659
679
  const values = {
@@ -668,40 +688,14 @@ const replaceAttributes = {
668
688
  replaceSize: function({ message, parameters }) {
669
689
  return message.replace(":size", parameters[0]);
670
690
  },
691
+ replaceMultipleOf: function({ message, parameters }) {
692
+ return message.replace(":value", parameters[0]);
693
+ },
671
694
  replaceUnique: function({ message, parameters, data }) {
672
695
  return message.replace(":value", data[parameters[1]]);
673
696
  }
674
697
  };
675
698
 
676
- //#endregion
677
- //#region src/Rules/closureValidationRule.ts
678
- var ClosureValidationRule = class extends IRuleContract {
679
- /**
680
- * The callback that validates the attribute
681
- */
682
- callback;
683
- /**
684
- * Indicates if the validation callback failed.
685
- */
686
- failed = false;
687
- constructor(callback) {
688
- super();
689
- this.callback = callback;
690
- }
691
- /**
692
- * Determine if the validation rule passes.
693
- */
694
- passes(value, attribute) {
695
- this.failed = false;
696
- const result = this.callback(value, (message) => {
697
- this.failed = true;
698
- this.message = message;
699
- }, attribute);
700
- if (result instanceof Promise) return result.then(() => !this.failed);
701
- return !this.failed;
702
- }
703
- };
704
-
705
699
  //#endregion
706
700
  //#region src/validators/validationData.ts
707
701
  const validationData = {
@@ -742,6 +736,35 @@ const validationData = {
742
736
  }
743
737
  };
744
738
 
739
+ //#endregion
740
+ //#region src/Rules/closureValidationRule.ts
741
+ var ClosureValidationRule = class extends IRuleContract {
742
+ /**
743
+ * The callback that validates the attribute
744
+ */
745
+ callback;
746
+ /**
747
+ * Indicates if the validation callback failed.
748
+ */
749
+ failed = false;
750
+ constructor(callback) {
751
+ super();
752
+ this.callback = callback;
753
+ }
754
+ /**
755
+ * Determine if the validation rule passes.
756
+ */
757
+ passes(value, attribute) {
758
+ this.failed = false;
759
+ const result = this.callback(value, (message) => {
760
+ this.failed = true;
761
+ this.message = message;
762
+ }, attribute);
763
+ if (result instanceof Promise) return result.then(() => !this.failed);
764
+ return !this.failed;
765
+ }
766
+ };
767
+
745
768
  //#endregion
746
769
  //#region src/validators/validationRuleParser.ts
747
770
  const validationRuleParser = {
@@ -989,7 +1012,7 @@ var validateAttributes = class {
989
1012
  */
990
1013
  validateDeclinedIf(value, parameters) {
991
1014
  this.requireParameterCount(2, parameters, "declined_if");
992
- const other = deepFind(this.data, parameters[0]);
1015
+ const other = this.getAttributeValue(parameters[0]);
993
1016
  if (!other) return true;
994
1017
  if (parameters.slice(1).indexOf(other) !== -1) return this.validateDeclined(value);
995
1018
  return true;
@@ -999,7 +1022,7 @@ var validateAttributes = class {
999
1022
  */
1000
1023
  validateDifferent(value, parameters) {
1001
1024
  this.requireParameterCount(1, parameters, "different");
1002
- const other = deepFind(this.data, parameters[0]);
1025
+ const other = this.getAttributeValue(parameters[0]);
1003
1026
  if (!sameType(value, other)) return true;
1004
1027
  if (value !== null && typeof value === "object") return !deepEqual(value, other);
1005
1028
  return value !== other;
@@ -1034,13 +1057,23 @@ var validateAttributes = class {
1034
1057
  /**
1035
1058
  * Validate that an attribute is a valid email address.
1036
1059
  */
1037
- validateEmail(value) {
1060
+ validateEmail(value, parameters = []) {
1038
1061
  if (typeof value !== "string") return false;
1039
- /**
1040
- * Max allowed length for a top-level-domain is 24 characters.
1041
- * reference to list of top-level-domains: https://data.iana.org/TLD/tlds-alpha-by-domain.txt
1042
- */
1043
- return value.toLowerCase().match(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,24})+$/) !== null;
1062
+ const normalized = parameters.map((parameter) => parameter.replace(/[{}]/g, "").toLowerCase());
1063
+ if (normalized.length === 0 || normalized.includes("filter") || normalized.includes("rfc")) {
1064
+ if (!this.isEmailByFilter(value)) return false;
1065
+ }
1066
+ if (normalized.includes("strict") && !this.isStrictEmail(value)) return false;
1067
+ if (normalized.includes("dns") && !this.hasResolvableEmailDomain(value)) return false;
1068
+ if (normalized.includes("spoof") && !this.isSpoofSafeEmail(value)) return false;
1069
+ return true;
1070
+ }
1071
+ /**
1072
+ * Validate that a present attribute is not empty.
1073
+ */
1074
+ validateFilled(value) {
1075
+ if (typeof value === "undefined") return true;
1076
+ return this.validateRequired(value);
1044
1077
  }
1045
1078
  /**
1046
1079
  * Validate the attribute ends with a given substring.
@@ -1100,7 +1133,7 @@ var validateAttributes = class {
1100
1133
  */
1101
1134
  validateRequiredIf(value, parameters) {
1102
1135
  this.requireParameterCount(2, parameters, "required_if");
1103
- const other = deepFind(this.data, parameters[0]);
1136
+ const other = this.getAttributeValue(parameters[0]);
1104
1137
  if (typeof other === "undefined") return true;
1105
1138
  if (this.parseDependentRuleParameters(other, parameters).indexOf(other) !== -1) return this.validateRequired(value);
1106
1139
  return true;
@@ -1110,7 +1143,7 @@ var validateAttributes = class {
1110
1143
  */
1111
1144
  validateRequiredUnless(value, parameters) {
1112
1145
  this.requireParameterCount(2, parameters, "required_unless");
1113
- let other = deepFind(this.data, parameters[0]);
1146
+ let other = this.getAttributeValue(parameters[0]);
1114
1147
  other = typeof other === "undefined" ? null : other;
1115
1148
  if (this.parseDependentRuleParameters(other, parameters).indexOf(other) === -1) return this.validateRequired(value);
1116
1149
  return true;
@@ -1147,14 +1180,14 @@ var validateAttributes = class {
1147
1180
  * Determine if any of the given attributes fail the required test.
1148
1181
  */
1149
1182
  anyFailingRequired(attributes) {
1150
- for (let i = 0; i < attributes.length; i++) if (!this.validateRequired(deepFind(this.data, attributes[i]))) return true;
1183
+ for (let i = 0; i < attributes.length; i++) if (!this.validateRequired(this.getAttributeValue(attributes[i]))) return true;
1151
1184
  return false;
1152
1185
  }
1153
1186
  /**
1154
1187
  * Determine if all of the given attributes fail the required test.
1155
1188
  */
1156
1189
  allFailingRequired(attributes) {
1157
- for (let i = 0; i < attributes.length; i++) if (this.validateRequired(deepFind(this.data, attributes[i]))) return false;
1190
+ for (let i = 0; i < attributes.length; i++) if (this.validateRequired(this.getAttributeValue(attributes[i]))) return false;
1158
1191
  return true;
1159
1192
  }
1160
1193
  /**
@@ -1196,7 +1229,13 @@ var validateAttributes = class {
1196
1229
  * Validate that an attribute exists even if not filled.
1197
1230
  */
1198
1231
  validatePresent(value, parameters, attribute) {
1199
- return typeof deepFind(this.data, attribute) !== "undefined";
1232
+ return typeof this.getAttributeValue(attribute) !== "undefined";
1233
+ }
1234
+ /**
1235
+ * Alias of the present rule kept for compatibility with requested rule naming.
1236
+ */
1237
+ validatePresentsame(value, parameters, attribute) {
1238
+ return this.validatePresent(value, parameters, attribute);
1200
1239
  }
1201
1240
  /**
1202
1241
  * Validate that an attribute is an integer.
@@ -1218,12 +1257,103 @@ var validateAttributes = class {
1218
1257
  return true;
1219
1258
  }
1220
1259
  /**
1260
+ * Validate that an attribute is prohibited.
1261
+ */
1262
+ validateProhibited(value) {
1263
+ return !this.validateRequired(value);
1264
+ }
1265
+ /**
1266
+ * Validate that an attribute is prohibited unless another field matches one of the given values.
1267
+ */
1268
+ validateProhibitedUnless(value, parameters) {
1269
+ this.requireParameterCount(2, parameters, "prohibited_unless");
1270
+ let other = this.getAttributeValue(parameters[0]);
1271
+ other = typeof other === "undefined" ? null : other;
1272
+ if (this.parseDependentRuleParameters(other, parameters).indexOf(other) !== -1) return true;
1273
+ return this.validateProhibited(value);
1274
+ }
1275
+ /**
1276
+ * Validate that present attributes prohibit other attributes from being present.
1277
+ */
1278
+ validateProhibits(value, parameters) {
1279
+ this.requireParameterCount(1, parameters, "prohibits");
1280
+ if (!this.validateRequired(value)) return true;
1281
+ return parameters.every((attribute) => this.validateProhibited(this.getAttributeValue(attribute)));
1282
+ }
1283
+ /**
1284
+ * Validate that an attribute is a valid IP address.
1285
+ */
1286
+ validateIp(value) {
1287
+ return typeof value === "string" && (0, node_net.isIP)(value) !== 0;
1288
+ }
1289
+ /**
1290
+ * Validate that an attribute is a valid IPv4 address.
1291
+ */
1292
+ validateIpv4(value) {
1293
+ return typeof value === "string" && (0, node_net.isIP)(value) === 4;
1294
+ }
1295
+ /**
1296
+ * Validate that an attribute is a valid IPv6 address.
1297
+ */
1298
+ validateIpv6(value) {
1299
+ return typeof value === "string" && (0, node_net.isIP)(value) === 6;
1300
+ }
1301
+ /**
1302
+ * Validate that an attribute is a valid MAC address.
1303
+ */
1304
+ validateMacAddress(value) {
1305
+ return typeof value === "string" && /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(value);
1306
+ }
1307
+ /**
1308
+ * Validate that an attribute is a valid timezone identifier.
1309
+ */
1310
+ validateTimezone(value) {
1311
+ if (typeof value !== "string" || value.length === 0) return false;
1312
+ if (typeof Intl.supportedValuesOf === "function" && Intl.supportedValuesOf("timeZone").includes(value)) return true;
1313
+ try {
1314
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
1315
+ return true;
1316
+ } catch {
1317
+ return false;
1318
+ }
1319
+ }
1320
+ /**
1321
+ * Validate that an attribute is a multiple of the given value.
1322
+ */
1323
+ validateMultipleOf(value, parameters) {
1324
+ this.requireParameterCount(1, parameters, "multiple_of");
1325
+ const numericValue = Number(value);
1326
+ const divisor = Number(parameters[0]);
1327
+ if (!Number.isFinite(numericValue) || !Number.isFinite(divisor) || divisor === 0) return false;
1328
+ const scale = 10 ** Math.max(this.getDecimalPlaces(value), this.getDecimalPlaces(parameters[0]));
1329
+ const scaledValue = Math.round(numericValue * scale);
1330
+ const scaledDivisor = Math.round(divisor * scale);
1331
+ return scaledDivisor !== 0 && scaledValue % scaledDivisor === 0;
1332
+ }
1333
+ /**
1334
+ * Validate that values matched by a wildcard attribute are distinct.
1335
+ */
1336
+ validateDistinct(value, parameters, attribute, primaryAttribute = attribute) {
1337
+ if (Array.isArray(value)) return new Set(value.map((entry) => this.normalizeDistinctValue(entry, parameters))).size === value.length;
1338
+ if (primaryAttribute.indexOf("*") === -1) return true;
1339
+ const strict = parameters.includes("strict");
1340
+ const values = this.getDistinctValues(primaryAttribute);
1341
+ const current = this.normalizeDistinctValue(value, parameters);
1342
+ let matches = 0;
1343
+ for (const candidate of values) {
1344
+ const normalized = this.normalizeDistinctValue(candidate, parameters);
1345
+ if (strict ? normalized === current : normalized == current) matches += 1;
1346
+ if (matches > 1) return false;
1347
+ }
1348
+ return true;
1349
+ }
1350
+ /**
1221
1351
  * Validate that an attribute is greater than another attribute.
1222
1352
  */
1223
1353
  validateGt(value, parameters, attribute) {
1224
1354
  this.requireParameterCount(1, parameters, "gt");
1225
1355
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1226
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1356
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1227
1357
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) > compartedToValue;
1228
1358
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1229
1359
  return getSize(value) > getSize(compartedToValue);
@@ -1234,7 +1364,7 @@ var validateAttributes = class {
1234
1364
  validateGte(value, parameters, attribute) {
1235
1365
  this.requireParameterCount(1, parameters, "gte");
1236
1366
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1237
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1367
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1238
1368
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) >= compartedToValue;
1239
1369
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1240
1370
  return getSize(value) >= getSize(compartedToValue);
@@ -1245,7 +1375,7 @@ var validateAttributes = class {
1245
1375
  validateLt(value, parameters, attribute) {
1246
1376
  this.requireParameterCount(1, parameters, "lt");
1247
1377
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1248
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1378
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1249
1379
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) < compartedToValue;
1250
1380
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1251
1381
  return getSize(value) < getSize(compartedToValue);
@@ -1256,7 +1386,7 @@ var validateAttributes = class {
1256
1386
  validateLte(value, parameters, attribute) {
1257
1387
  this.requireParameterCount(1, parameters, "lte");
1258
1388
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1259
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1389
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1260
1390
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) <= compartedToValue;
1261
1391
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1262
1392
  return getSize(value) <= getSize(compartedToValue);
@@ -1314,7 +1444,7 @@ var validateAttributes = class {
1314
1444
  compareDates(value, parameter, operator, rule) {
1315
1445
  value = toDate(value);
1316
1446
  if (!value) throw `Validation rule ${rule} requires the field under valation to be a date.`;
1317
- const compartedToValue = toDate(deepFind(this.data, parameter) || parameter);
1447
+ const compartedToValue = toDate(this.getAttributeValue(parameter) || parameter);
1318
1448
  if (!compartedToValue) throw `Validation rule ${rule} requires the parameter to be a date.`;
1319
1449
  return compare(value.getTime(), compartedToValue.getTime(), operator);
1320
1450
  }
@@ -1326,6 +1456,10 @@ var validateAttributes = class {
1326
1456
  }
1327
1457
  /**
1328
1458
  * Prepare the values for validation
1459
+ *
1460
+ * @param other The value to compare against
1461
+ * @param parameters The parameters for the validation rule
1462
+ * @returns The prepared values for validation
1329
1463
  */
1330
1464
  parseDependentRuleParameters(other, parameters) {
1331
1465
  let values = parameters.slice(1);
@@ -1334,6 +1468,55 @@ var validateAttributes = class {
1334
1468
  if (typeof other === "boolean") values = convertValuesToBoolean(values);
1335
1469
  return values;
1336
1470
  }
1471
+ getAttributeValue(attribute) {
1472
+ const dataValue = deepFind(this.data, attribute);
1473
+ if (typeof dataValue !== "undefined") return dataValue;
1474
+ return deepFind(this.context.requestFiles ?? {}, attribute);
1475
+ }
1476
+ getDistinctValues(primaryAttribute) {
1477
+ const gathered = validationData.initializeAndGatherData(primaryAttribute, this.data);
1478
+ const pattern = new RegExp(`^${primaryAttribute.replace(/\./g, "\\.").replace(/\*/g, "[^.]+")}$`);
1479
+ return Object.keys(gathered).filter((attribute) => pattern.test(attribute)).map((attribute) => deepFind(this.data, attribute)).filter((value) => typeof value !== "undefined");
1480
+ }
1481
+ normalizeDistinctValue(value, parameters) {
1482
+ if (parameters.includes("ignore_case") && typeof value === "string") return value.toLowerCase();
1483
+ return value;
1484
+ }
1485
+ isEmailByFilter(value) {
1486
+ return value.toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]{2,24}$/) !== null;
1487
+ }
1488
+ isStrictEmail(value) {
1489
+ if (!this.isEmailByFilter(value)) return false;
1490
+ const [localPart, domain] = value.split("@");
1491
+ if (!localPart || !domain || localPart.length > 64 || domain.length > 255) return false;
1492
+ if (localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes("..")) return false;
1493
+ return domain.split(".").every((label) => {
1494
+ return label.length > 0 && label.length <= 63 && /^[a-z0-9-]+$/i.test(label) && !label.startsWith("-") && !label.endsWith("-");
1495
+ });
1496
+ }
1497
+ hasResolvableEmailDomain(value) {
1498
+ if (!this.isStrictEmail(value)) return false;
1499
+ const domain = value.split("@")[1];
1500
+ const asciiDomain = (0, node_url.domainToASCII)(domain);
1501
+ return asciiDomain.length > 0 && asciiDomain.includes(".");
1502
+ }
1503
+ isSpoofSafeEmail(value) {
1504
+ if (!this.isEmailByFilter(value)) return false;
1505
+ const [localPart, domain] = value.split("@");
1506
+ return (0, node_url.domainToASCII)(domain).length > 0 && !this.hasAsciiControlCharacters(value) && localPart.normalize("NFKC") === localPart && domain.normalize("NFKC") === domain;
1507
+ }
1508
+ hasAsciiControlCharacters(value) {
1509
+ for (let index = 0; index < value.length; index++) {
1510
+ const codePoint = value.charCodeAt(index);
1511
+ if (codePoint <= 31 || codePoint === 127) return true;
1512
+ }
1513
+ return false;
1514
+ }
1515
+ getDecimalPlaces(value) {
1516
+ const stringValue = String(value).toLowerCase();
1517
+ if (!stringValue.includes(".")) return 0;
1518
+ return stringValue.split(".")[1]?.length ?? 0;
1519
+ }
1337
1520
  };
1338
1521
 
1339
1522
  //#endregion
@@ -1402,6 +1585,7 @@ const implicitRues = [
1402
1585
  "declined_if",
1403
1586
  "filled",
1404
1587
  "present",
1588
+ "presentsame",
1405
1589
  "required",
1406
1590
  "required_if",
1407
1591
  "required_unless",
@@ -2030,6 +2214,7 @@ var replaceAttributePayload = class {
2030
2214
  //#endregion
2031
2215
  //#region src/BaseValidator.ts
2032
2216
  var BaseValidator = class BaseValidator {
2217
+ excludedAttributes = /* @__PURE__ */ new Set();
2033
2218
  /**
2034
2219
  * The lang used to return error messages
2035
2220
  */
@@ -2174,6 +2359,9 @@ var BaseValidator = class BaseValidator {
2174
2359
  errors() {
2175
2360
  return this.messages;
2176
2361
  }
2362
+ getExcludedAttributes() {
2363
+ return [...this.excludedAttributes];
2364
+ }
2177
2365
  /**
2178
2366
  * Clear the error messages for the given keys.
2179
2367
  * If no keys are provided, all error messages will be cleared.
@@ -2290,6 +2478,7 @@ var BaseValidator = class BaseValidator {
2290
2478
  runAllValidations() {
2291
2479
  this.messages = new ErrorBag();
2292
2480
  this.validateAttributes = new validateAttributes(this.data, this.rules, this.getContext());
2481
+ this.excludedAttributes.clear();
2293
2482
  for (const property in this.rules) if (this.runValidation(property) === false) break;
2294
2483
  }
2295
2484
  /**
@@ -2298,6 +2487,7 @@ var BaseValidator = class BaseValidator {
2298
2487
  async runAllValidationsAsync() {
2299
2488
  this.messages = new ErrorBag();
2300
2489
  this.validateAttributes = new validateAttributes(this.data, this.rules, this.getContext());
2490
+ this.excludedAttributes.clear();
2301
2491
  for (const property in this.rules) if (await this.runValidationAsync(property) === false) break;
2302
2492
  }
2303
2493
  /**
@@ -2320,26 +2510,39 @@ var BaseValidator = class BaseValidator {
2320
2510
  * Run validation rules for the specified property and stop validation if needed
2321
2511
  */
2322
2512
  runValidation(property) {
2323
- if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) for (let i = 0; i < this.rules[property].length; i++) {
2324
- this.validateAttribute(property, this.rules[property][i]);
2325
- if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2326
- if (this.shouldStopValidating(property)) break;
2513
+ if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) {
2514
+ if (validationRuleParser.hasRule(property, ["exclude"], this.rules)) {
2515
+ this.excludedAttributes.add(property);
2516
+ return;
2517
+ }
2518
+ for (let i = 0; i < this.rules[property].length; i++) {
2519
+ this.validateAttribute(property, this.rules[property][i]);
2520
+ if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2521
+ if (this.shouldStopValidating(property)) break;
2522
+ }
2327
2523
  }
2328
2524
  }
2329
2525
  /**
2330
2526
  * Run validation rules for the specified property asynchronously and stop validation if needed
2331
2527
  */
2332
2528
  async runValidationAsync(property) {
2333
- if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) for (let i = 0; i < this.rules[property].length; i++) {
2334
- await this.validateAttribute(property, this.rules[property][i]);
2335
- if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2336
- if (this.shouldStopValidating(property)) break;
2529
+ if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) {
2530
+ if (validationRuleParser.hasRule(property, ["exclude"], this.rules)) {
2531
+ this.excludedAttributes.add(property);
2532
+ return;
2533
+ }
2534
+ for (let i = 0; i < this.rules[property].length; i++) {
2535
+ await this.validateAttribute(property, this.rules[property][i]);
2536
+ if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2537
+ if (this.shouldStopValidating(property)) break;
2538
+ }
2337
2539
  }
2338
2540
  }
2339
2541
  /**
2340
2542
  * Check if we should stop further validations on a given attribute.
2341
2543
  */
2342
2544
  shouldStopValidating(attribute) {
2545
+ if (this.excludedAttributes.has(attribute)) return true;
2343
2546
  return this.messages.has(attribute) && validationRuleParser.hasRule(attribute, ["bail"], this.rules);
2344
2547
  }
2345
2548
  /**
@@ -2364,7 +2567,7 @@ var BaseValidator = class BaseValidator {
2364
2567
  const method = `validate${buildValidationMethodName(rule)}`;
2365
2568
  if (rule !== "" && typeof this.validateAttributes[method] === "undefined") throw `Rule ${rule} is not valid`;
2366
2569
  if (!validatable) return;
2367
- const validation = this.validateAttributes[method](value, parameters, attribute);
2570
+ const validation = this.validateAttributes[method](value, parameters, attribute, this.getPrimaryAttribute(attribute));
2368
2571
  if (validation instanceof Promise) return validation.then((result) => {
2369
2572
  if (!result) this.addFailure(attribute, rule, value, parameters);
2370
2573
  });
@@ -3129,8 +3332,10 @@ var Validator = class Validator {
3129
3332
  */
3130
3333
  validatedData() {
3131
3334
  const validKeys = Object.keys(this.rules);
3335
+ const excluded = new Set(this.instance?.getExcludedAttributes() ?? []);
3132
3336
  const clean = {};
3133
3337
  for (const key of validKeys) {
3338
+ if (excluded.has(key)) continue;
3134
3339
  const value = deepFind(this.data, key);
3135
3340
  const resolvedValue = typeof value !== "undefined" ? value : deepFind(this.getContext().requestFiles ?? {}, key);
3136
3341
  if (typeof resolvedValue !== "undefined") deepSet(clean, key, resolvedValue);
@@ -3141,7 +3346,8 @@ var Validator = class Validator {
3141
3346
  * Return all validated input.
3142
3347
  */
3143
3348
  validated() {
3144
- return Object.fromEntries(Object.entries(this.data).filter(([key]) => key in this.rules));
3349
+ const excluded = new Set(this.instance?.getExcludedAttributes() ?? []);
3350
+ return Object.fromEntries(Object.entries(this.data).filter(([key]) => key in this.rules && !excluded.has(key)));
3145
3351
  }
3146
3352
  /**
3147
3353
  * Return a portion of validated input
package/dist/index.d.ts CHANGED
@@ -127,8 +127,8 @@ type LiteralUnion<T extends U, U = string> = T | (U & Record<never, never>);
127
127
  type PluginValidationRuleNameMap = ValidationRuleAutocompleteMap & CustomValidationRuleNameMap;
128
128
  type CustomParamableValidationRuleName = Extract<{ [K in keyof PluginValidationRuleNameMap]: PluginValidationRuleNameMap[K] extends 'paramable' ? K : never }[keyof PluginValidationRuleNameMap], string>;
129
129
  type CustomPlainRuleName = Extract<{ [K in keyof PluginValidationRuleNameMap]: PluginValidationRuleNameMap[K] extends 'plain' ? K : never }[keyof PluginValidationRuleNameMap], string>;
130
- type ParamableValidationRuleName = 'accepted_if' | 'after' | 'after_or_equal' | 'before' | 'before_or_equal' | 'between' | 'date_equals' | 'datetime' | 'declined_if' | 'digits_between' | 'different' | 'exists' | 'ends_with' | 'gt' | 'gte' | 'in' | 'includes' | 'lt' | 'lte' | 'max' | 'min' | 'not_in' | 'not_includes' | 'required_if' | 'required_unless' | 'required_with' | 'required_with_all' | 'required_without' | 'required_without_all' | 'same' | 'size' | 'starts_with' | 'unique' | CustomParamableValidationRuleName;
131
- type PlainRuleName = 'accepted' | 'alpha' | 'alpha_dash' | 'alpha_num' | 'array' | 'array_unique' | 'bail' | 'boolean' | 'confirmed' | 'date' | 'declined' | 'digits' | 'email' | 'integer' | 'json' | 'not_regex' | 'nullable' | 'numeric' | 'object' | 'present' | 'regex' | 'required' | 'sometimes' | 'string' | 'url' | 'hex' | 'uuid' | CustomPlainRuleName;
130
+ type ParamableValidationRuleName = 'accepted_if' | 'after' | 'after_or_equal' | 'before' | 'before_or_equal' | 'between' | 'date_equals' | 'datetime' | 'declined_if' | 'digits_between' | 'different' | 'email' | 'exists' | 'ends_with' | 'gt' | 'gte' | 'in' | 'includes' | 'lt' | 'lte' | 'max' | 'min' | 'not_in' | 'not_includes' | 'multiple_of' | 'prohibited_unless' | 'prohibits' | 'required_if' | 'required_unless' | 'required_with' | 'required_with_all' | 'required_without' | 'required_without_all' | 'same' | 'size' | 'starts_with' | 'unique' | CustomParamableValidationRuleName;
131
+ type PlainRuleName = 'accepted' | 'alpha' | 'alpha_dash' | 'alpha_num' | 'array' | 'array_unique' | 'bail' | 'boolean' | 'confirmed' | 'date' | 'declined' | 'distinct' | 'digits' | 'email' | 'exclude' | 'filled' | 'ip' | 'ipv4' | 'ipv6' | 'integer' | 'json' | 'mac_address' | 'not_regex' | 'nullable' | 'numeric' | 'object' | 'present' | 'presentsame' | 'prohibited' | 'regex' | 'required' | 'sometimes' | 'string' | 'timezone' | 'url' | 'hex' | 'uuid' | CustomPlainRuleName;
132
132
  type ValidationRuleName = ParamableValidationRuleName | PlainRuleName;
133
133
  type MethodRules = Regex | In | NotIn | RequiredIf;
134
134
  type ParamableRuleString = `${ParamableValidationRuleName}:${string}`;
@@ -308,10 +308,13 @@ interface ReplaceAttributeInterface {
308
308
  replaceGte: (payload: replaceAttributePayload) => string;
309
309
  replaceLte: (payload: replaceAttributePayload) => string;
310
310
  replaceRequiredIf: (payload: replaceAttributePayload) => string;
311
+ replaceProhibitedUnless: (payload: replaceAttributePayload) => string;
312
+ replaceProhibits: (payload: replaceAttributePayload) => string;
311
313
  replaceStartsWith: (payload: replaceAttributePayload) => string;
312
314
  replaceRequiredUnless: (payload: replaceAttributePayload) => string;
313
315
  replaceSame: (payload: replaceAttributePayload) => string;
314
316
  replaceSize: (payload: replaceAttributePayload) => string;
317
+ replaceMultipleOf: (payload: replaceAttributePayload) => string;
315
318
  replaceUnique: (payload: replaceAttributePayload) => string;
316
319
  }
317
320
  type ValidationCallback = (value: any, fail: (message: string) => void, attribute: string) => void;
@@ -402,6 +405,7 @@ declare function getValidationSize(value: any, hasNumericRule?: boolean): number
402
405
  //#endregion
403
406
  //#region src/BaseValidator.d.ts
404
407
  declare class BaseValidator<D extends GenericObject = GenericObject> {
408
+ private excludedAttributes;
405
409
  /**
406
410
  * The lang used to return error messages
407
411
  */
@@ -497,6 +501,7 @@ declare class BaseValidator<D extends GenericObject = GenericObject> {
497
501
  * @returns
498
502
  */
499
503
  errors(): ErrorBag;
504
+ getExcludedAttributes(): string[];
500
505
  /**
501
506
  * Clear the error messages for the given keys.
502
507
  * If no keys are provided, all error messages will be cleared.
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { domainToASCII } from "node:url";
3
+ import { isIP } from "node:net";
2
4
  import customParseFormat from "dayjs/plugin/customParseFormat.js";
3
5
  import dayjs from "dayjs";
4
6
 
@@ -73,10 +75,12 @@ var en_default = {
73
75
  declined: "The :attribute must be declined.",
74
76
  declined_if: "The :attribute must be declined when :other is :value.",
75
77
  different: "The :attribute and :other must be different.",
78
+ distinct: "The :attribute field has a duplicate value.",
76
79
  digits: "The :attribute must be :digits digits.",
77
80
  digits_between: "The :attribute must be between :min and :max digits.",
78
81
  email: "The :attribute must be a valid email address.",
79
82
  ends_with: "The :attribute must end with one of the following: :values.",
83
+ filled: "The :attribute field must have a value.",
80
84
  exists: "The selected :attribute is invalid.",
81
85
  gt: {
82
86
  number: "The :attribute must be greater than :value.",
@@ -94,6 +98,9 @@ var en_default = {
94
98
  in: "The :attribute must be one of the following :values.",
95
99
  includes: "The :attribute must include one of the following values: :values.",
96
100
  integer: "The :attribute must be an integer.",
101
+ ip: "The :attribute must be a valid IP address.",
102
+ ipv4: "The :attribute must be a valid IPv4 address.",
103
+ ipv6: "The :attribute must be a valid IPv6 address.",
97
104
  json: "The :attribute must be a valid JSON string.",
98
105
  lt: {
99
106
  number: "The :attribute must be less than :value.",
@@ -119,6 +126,7 @@ var en_default = {
119
126
  array: "The :attribute must have at least :min items.",
120
127
  object: "The :attribute must have at least :min items."
121
128
  },
129
+ mac_address: "The :attribute must be a valid MAC address.",
122
130
  not_in: "The selected :attribute is invalid.",
123
131
  not_regex: "The :attribute format is invalid.",
124
132
  not_includes: "The :attribute must not include any of the following values: :values.",
@@ -137,6 +145,10 @@ var en_default = {
137
145
  upper_cases: "The :attribute must contain at least :amount uppercase letters."
138
146
  },
139
147
  present: "The :attribute field must be present.",
148
+ presentsame: "The :attribute field must be present.",
149
+ prohibited: "The :attribute field is prohibited.",
150
+ prohibited_unless: "The :attribute field is prohibited unless :other is in :values.",
151
+ prohibits: "The :attribute field prohibits :values from being present.",
140
152
  regex: "The :attribute format is invalid.",
141
153
  required: "The :attribute field is required.",
142
154
  required_if: "The :attribute field is required when :other is :value.",
@@ -154,8 +166,10 @@ var en_default = {
154
166
  object: "The :attribute must contain :size items."
155
167
  },
156
168
  string: "The :attribute must be a string.",
169
+ timezone: "The :attribute must be a valid timezone.",
157
170
  unique: "The :attribute has already been taken.",
158
- url: "The :attribute must have a valid URL format."
171
+ url: "The :attribute must have a valid URL format.",
172
+ multiple_of: "The :attribute must be a multiple of :value."
159
173
  };
160
174
 
161
175
  //#endregion
@@ -624,6 +638,12 @@ const replaceAttributes = {
624
638
  };
625
639
  return message.replace(/:other|:value/gi, (matched) => values[matched]);
626
640
  },
641
+ replaceProhibitedUnless: function(payload) {
642
+ return this.replaceRequiredUnless(payload);
643
+ },
644
+ replaceProhibits: function({ message, parameters, getDisplayableAttribute }) {
645
+ return message.replace(":values", parameters.map((attribute) => getDisplayableAttribute(attribute)).join(", "));
646
+ },
627
647
  replaceRequiredUnless: function({ message, parameters, getDisplayableAttribute }) {
628
648
  const [other] = parameters;
629
649
  const values = {
@@ -638,40 +658,14 @@ const replaceAttributes = {
638
658
  replaceSize: function({ message, parameters }) {
639
659
  return message.replace(":size", parameters[0]);
640
660
  },
661
+ replaceMultipleOf: function({ message, parameters }) {
662
+ return message.replace(":value", parameters[0]);
663
+ },
641
664
  replaceUnique: function({ message, parameters, data }) {
642
665
  return message.replace(":value", data[parameters[1]]);
643
666
  }
644
667
  };
645
668
 
646
- //#endregion
647
- //#region src/Rules/closureValidationRule.ts
648
- var ClosureValidationRule = class extends IRuleContract {
649
- /**
650
- * The callback that validates the attribute
651
- */
652
- callback;
653
- /**
654
- * Indicates if the validation callback failed.
655
- */
656
- failed = false;
657
- constructor(callback) {
658
- super();
659
- this.callback = callback;
660
- }
661
- /**
662
- * Determine if the validation rule passes.
663
- */
664
- passes(value, attribute) {
665
- this.failed = false;
666
- const result = this.callback(value, (message) => {
667
- this.failed = true;
668
- this.message = message;
669
- }, attribute);
670
- if (result instanceof Promise) return result.then(() => !this.failed);
671
- return !this.failed;
672
- }
673
- };
674
-
675
669
  //#endregion
676
670
  //#region src/validators/validationData.ts
677
671
  const validationData = {
@@ -712,6 +706,35 @@ const validationData = {
712
706
  }
713
707
  };
714
708
 
709
+ //#endregion
710
+ //#region src/Rules/closureValidationRule.ts
711
+ var ClosureValidationRule = class extends IRuleContract {
712
+ /**
713
+ * The callback that validates the attribute
714
+ */
715
+ callback;
716
+ /**
717
+ * Indicates if the validation callback failed.
718
+ */
719
+ failed = false;
720
+ constructor(callback) {
721
+ super();
722
+ this.callback = callback;
723
+ }
724
+ /**
725
+ * Determine if the validation rule passes.
726
+ */
727
+ passes(value, attribute) {
728
+ this.failed = false;
729
+ const result = this.callback(value, (message) => {
730
+ this.failed = true;
731
+ this.message = message;
732
+ }, attribute);
733
+ if (result instanceof Promise) return result.then(() => !this.failed);
734
+ return !this.failed;
735
+ }
736
+ };
737
+
715
738
  //#endregion
716
739
  //#region src/validators/validationRuleParser.ts
717
740
  const validationRuleParser = {
@@ -959,7 +982,7 @@ var validateAttributes = class {
959
982
  */
960
983
  validateDeclinedIf(value, parameters) {
961
984
  this.requireParameterCount(2, parameters, "declined_if");
962
- const other = deepFind(this.data, parameters[0]);
985
+ const other = this.getAttributeValue(parameters[0]);
963
986
  if (!other) return true;
964
987
  if (parameters.slice(1).indexOf(other) !== -1) return this.validateDeclined(value);
965
988
  return true;
@@ -969,7 +992,7 @@ var validateAttributes = class {
969
992
  */
970
993
  validateDifferent(value, parameters) {
971
994
  this.requireParameterCount(1, parameters, "different");
972
- const other = deepFind(this.data, parameters[0]);
995
+ const other = this.getAttributeValue(parameters[0]);
973
996
  if (!sameType(value, other)) return true;
974
997
  if (value !== null && typeof value === "object") return !deepEqual(value, other);
975
998
  return value !== other;
@@ -1004,13 +1027,23 @@ var validateAttributes = class {
1004
1027
  /**
1005
1028
  * Validate that an attribute is a valid email address.
1006
1029
  */
1007
- validateEmail(value) {
1030
+ validateEmail(value, parameters = []) {
1008
1031
  if (typeof value !== "string") return false;
1009
- /**
1010
- * Max allowed length for a top-level-domain is 24 characters.
1011
- * reference to list of top-level-domains: https://data.iana.org/TLD/tlds-alpha-by-domain.txt
1012
- */
1013
- return value.toLowerCase().match(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,24})+$/) !== null;
1032
+ const normalized = parameters.map((parameter) => parameter.replace(/[{}]/g, "").toLowerCase());
1033
+ if (normalized.length === 0 || normalized.includes("filter") || normalized.includes("rfc")) {
1034
+ if (!this.isEmailByFilter(value)) return false;
1035
+ }
1036
+ if (normalized.includes("strict") && !this.isStrictEmail(value)) return false;
1037
+ if (normalized.includes("dns") && !this.hasResolvableEmailDomain(value)) return false;
1038
+ if (normalized.includes("spoof") && !this.isSpoofSafeEmail(value)) return false;
1039
+ return true;
1040
+ }
1041
+ /**
1042
+ * Validate that a present attribute is not empty.
1043
+ */
1044
+ validateFilled(value) {
1045
+ if (typeof value === "undefined") return true;
1046
+ return this.validateRequired(value);
1014
1047
  }
1015
1048
  /**
1016
1049
  * Validate the attribute ends with a given substring.
@@ -1070,7 +1103,7 @@ var validateAttributes = class {
1070
1103
  */
1071
1104
  validateRequiredIf(value, parameters) {
1072
1105
  this.requireParameterCount(2, parameters, "required_if");
1073
- const other = deepFind(this.data, parameters[0]);
1106
+ const other = this.getAttributeValue(parameters[0]);
1074
1107
  if (typeof other === "undefined") return true;
1075
1108
  if (this.parseDependentRuleParameters(other, parameters).indexOf(other) !== -1) return this.validateRequired(value);
1076
1109
  return true;
@@ -1080,7 +1113,7 @@ var validateAttributes = class {
1080
1113
  */
1081
1114
  validateRequiredUnless(value, parameters) {
1082
1115
  this.requireParameterCount(2, parameters, "required_unless");
1083
- let other = deepFind(this.data, parameters[0]);
1116
+ let other = this.getAttributeValue(parameters[0]);
1084
1117
  other = typeof other === "undefined" ? null : other;
1085
1118
  if (this.parseDependentRuleParameters(other, parameters).indexOf(other) === -1) return this.validateRequired(value);
1086
1119
  return true;
@@ -1117,14 +1150,14 @@ var validateAttributes = class {
1117
1150
  * Determine if any of the given attributes fail the required test.
1118
1151
  */
1119
1152
  anyFailingRequired(attributes) {
1120
- for (let i = 0; i < attributes.length; i++) if (!this.validateRequired(deepFind(this.data, attributes[i]))) return true;
1153
+ for (let i = 0; i < attributes.length; i++) if (!this.validateRequired(this.getAttributeValue(attributes[i]))) return true;
1121
1154
  return false;
1122
1155
  }
1123
1156
  /**
1124
1157
  * Determine if all of the given attributes fail the required test.
1125
1158
  */
1126
1159
  allFailingRequired(attributes) {
1127
- for (let i = 0; i < attributes.length; i++) if (this.validateRequired(deepFind(this.data, attributes[i]))) return false;
1160
+ for (let i = 0; i < attributes.length; i++) if (this.validateRequired(this.getAttributeValue(attributes[i]))) return false;
1128
1161
  return true;
1129
1162
  }
1130
1163
  /**
@@ -1166,7 +1199,13 @@ var validateAttributes = class {
1166
1199
  * Validate that an attribute exists even if not filled.
1167
1200
  */
1168
1201
  validatePresent(value, parameters, attribute) {
1169
- return typeof deepFind(this.data, attribute) !== "undefined";
1202
+ return typeof this.getAttributeValue(attribute) !== "undefined";
1203
+ }
1204
+ /**
1205
+ * Alias of the present rule kept for compatibility with requested rule naming.
1206
+ */
1207
+ validatePresentsame(value, parameters, attribute) {
1208
+ return this.validatePresent(value, parameters, attribute);
1170
1209
  }
1171
1210
  /**
1172
1211
  * Validate that an attribute is an integer.
@@ -1188,12 +1227,103 @@ var validateAttributes = class {
1188
1227
  return true;
1189
1228
  }
1190
1229
  /**
1230
+ * Validate that an attribute is prohibited.
1231
+ */
1232
+ validateProhibited(value) {
1233
+ return !this.validateRequired(value);
1234
+ }
1235
+ /**
1236
+ * Validate that an attribute is prohibited unless another field matches one of the given values.
1237
+ */
1238
+ validateProhibitedUnless(value, parameters) {
1239
+ this.requireParameterCount(2, parameters, "prohibited_unless");
1240
+ let other = this.getAttributeValue(parameters[0]);
1241
+ other = typeof other === "undefined" ? null : other;
1242
+ if (this.parseDependentRuleParameters(other, parameters).indexOf(other) !== -1) return true;
1243
+ return this.validateProhibited(value);
1244
+ }
1245
+ /**
1246
+ * Validate that present attributes prohibit other attributes from being present.
1247
+ */
1248
+ validateProhibits(value, parameters) {
1249
+ this.requireParameterCount(1, parameters, "prohibits");
1250
+ if (!this.validateRequired(value)) return true;
1251
+ return parameters.every((attribute) => this.validateProhibited(this.getAttributeValue(attribute)));
1252
+ }
1253
+ /**
1254
+ * Validate that an attribute is a valid IP address.
1255
+ */
1256
+ validateIp(value) {
1257
+ return typeof value === "string" && isIP(value) !== 0;
1258
+ }
1259
+ /**
1260
+ * Validate that an attribute is a valid IPv4 address.
1261
+ */
1262
+ validateIpv4(value) {
1263
+ return typeof value === "string" && isIP(value) === 4;
1264
+ }
1265
+ /**
1266
+ * Validate that an attribute is a valid IPv6 address.
1267
+ */
1268
+ validateIpv6(value) {
1269
+ return typeof value === "string" && isIP(value) === 6;
1270
+ }
1271
+ /**
1272
+ * Validate that an attribute is a valid MAC address.
1273
+ */
1274
+ validateMacAddress(value) {
1275
+ return typeof value === "string" && /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(value);
1276
+ }
1277
+ /**
1278
+ * Validate that an attribute is a valid timezone identifier.
1279
+ */
1280
+ validateTimezone(value) {
1281
+ if (typeof value !== "string" || value.length === 0) return false;
1282
+ if (typeof Intl.supportedValuesOf === "function" && Intl.supportedValuesOf("timeZone").includes(value)) return true;
1283
+ try {
1284
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
1285
+ return true;
1286
+ } catch {
1287
+ return false;
1288
+ }
1289
+ }
1290
+ /**
1291
+ * Validate that an attribute is a multiple of the given value.
1292
+ */
1293
+ validateMultipleOf(value, parameters) {
1294
+ this.requireParameterCount(1, parameters, "multiple_of");
1295
+ const numericValue = Number(value);
1296
+ const divisor = Number(parameters[0]);
1297
+ if (!Number.isFinite(numericValue) || !Number.isFinite(divisor) || divisor === 0) return false;
1298
+ const scale = 10 ** Math.max(this.getDecimalPlaces(value), this.getDecimalPlaces(parameters[0]));
1299
+ const scaledValue = Math.round(numericValue * scale);
1300
+ const scaledDivisor = Math.round(divisor * scale);
1301
+ return scaledDivisor !== 0 && scaledValue % scaledDivisor === 0;
1302
+ }
1303
+ /**
1304
+ * Validate that values matched by a wildcard attribute are distinct.
1305
+ */
1306
+ validateDistinct(value, parameters, attribute, primaryAttribute = attribute) {
1307
+ if (Array.isArray(value)) return new Set(value.map((entry) => this.normalizeDistinctValue(entry, parameters))).size === value.length;
1308
+ if (primaryAttribute.indexOf("*") === -1) return true;
1309
+ const strict = parameters.includes("strict");
1310
+ const values = this.getDistinctValues(primaryAttribute);
1311
+ const current = this.normalizeDistinctValue(value, parameters);
1312
+ let matches = 0;
1313
+ for (const candidate of values) {
1314
+ const normalized = this.normalizeDistinctValue(candidate, parameters);
1315
+ if (strict ? normalized === current : normalized == current) matches += 1;
1316
+ if (matches > 1) return false;
1317
+ }
1318
+ return true;
1319
+ }
1320
+ /**
1191
1321
  * Validate that an attribute is greater than another attribute.
1192
1322
  */
1193
1323
  validateGt(value, parameters, attribute) {
1194
1324
  this.requireParameterCount(1, parameters, "gt");
1195
1325
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1196
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1326
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1197
1327
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) > compartedToValue;
1198
1328
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1199
1329
  return getSize(value) > getSize(compartedToValue);
@@ -1204,7 +1334,7 @@ var validateAttributes = class {
1204
1334
  validateGte(value, parameters, attribute) {
1205
1335
  this.requireParameterCount(1, parameters, "gte");
1206
1336
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1207
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1337
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1208
1338
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) >= compartedToValue;
1209
1339
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1210
1340
  return getSize(value) >= getSize(compartedToValue);
@@ -1215,7 +1345,7 @@ var validateAttributes = class {
1215
1345
  validateLt(value, parameters, attribute) {
1216
1346
  this.requireParameterCount(1, parameters, "lt");
1217
1347
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1218
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1348
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1219
1349
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) < compartedToValue;
1220
1350
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1221
1351
  return getSize(value) < getSize(compartedToValue);
@@ -1226,7 +1356,7 @@ var validateAttributes = class {
1226
1356
  validateLte(value, parameters, attribute) {
1227
1357
  this.requireParameterCount(1, parameters, "lte");
1228
1358
  if (typeof value !== "number" && typeof value !== "string" && typeof value !== "object") throw "The field under validation must be a number, string, array or object";
1229
- const compartedToValue = deepFind(this.data, parameters[0]) || parameters[0];
1359
+ const compartedToValue = this.getAttributeValue(parameters[0]) || parameters[0];
1230
1360
  if (!Array.isArray(compartedToValue) && isNaN(compartedToValue) === false) return getSize(value, validationRuleParser.hasRule(attribute, getNumericRules(), this.rules)) <= compartedToValue;
1231
1361
  if (sameType(value, compartedToValue) === false) throw "The fields under validation must be of the same type";
1232
1362
  return getSize(value) <= getSize(compartedToValue);
@@ -1284,7 +1414,7 @@ var validateAttributes = class {
1284
1414
  compareDates(value, parameter, operator, rule) {
1285
1415
  value = toDate(value);
1286
1416
  if (!value) throw `Validation rule ${rule} requires the field under valation to be a date.`;
1287
- const compartedToValue = toDate(deepFind(this.data, parameter) || parameter);
1417
+ const compartedToValue = toDate(this.getAttributeValue(parameter) || parameter);
1288
1418
  if (!compartedToValue) throw `Validation rule ${rule} requires the parameter to be a date.`;
1289
1419
  return compare(value.getTime(), compartedToValue.getTime(), operator);
1290
1420
  }
@@ -1296,6 +1426,10 @@ var validateAttributes = class {
1296
1426
  }
1297
1427
  /**
1298
1428
  * Prepare the values for validation
1429
+ *
1430
+ * @param other The value to compare against
1431
+ * @param parameters The parameters for the validation rule
1432
+ * @returns The prepared values for validation
1299
1433
  */
1300
1434
  parseDependentRuleParameters(other, parameters) {
1301
1435
  let values = parameters.slice(1);
@@ -1304,6 +1438,55 @@ var validateAttributes = class {
1304
1438
  if (typeof other === "boolean") values = convertValuesToBoolean(values);
1305
1439
  return values;
1306
1440
  }
1441
+ getAttributeValue(attribute) {
1442
+ const dataValue = deepFind(this.data, attribute);
1443
+ if (typeof dataValue !== "undefined") return dataValue;
1444
+ return deepFind(this.context.requestFiles ?? {}, attribute);
1445
+ }
1446
+ getDistinctValues(primaryAttribute) {
1447
+ const gathered = validationData.initializeAndGatherData(primaryAttribute, this.data);
1448
+ const pattern = new RegExp(`^${primaryAttribute.replace(/\./g, "\\.").replace(/\*/g, "[^.]+")}$`);
1449
+ return Object.keys(gathered).filter((attribute) => pattern.test(attribute)).map((attribute) => deepFind(this.data, attribute)).filter((value) => typeof value !== "undefined");
1450
+ }
1451
+ normalizeDistinctValue(value, parameters) {
1452
+ if (parameters.includes("ignore_case") && typeof value === "string") return value.toLowerCase();
1453
+ return value;
1454
+ }
1455
+ isEmailByFilter(value) {
1456
+ return value.toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]{2,24}$/) !== null;
1457
+ }
1458
+ isStrictEmail(value) {
1459
+ if (!this.isEmailByFilter(value)) return false;
1460
+ const [localPart, domain] = value.split("@");
1461
+ if (!localPart || !domain || localPart.length > 64 || domain.length > 255) return false;
1462
+ if (localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes("..")) return false;
1463
+ return domain.split(".").every((label) => {
1464
+ return label.length > 0 && label.length <= 63 && /^[a-z0-9-]+$/i.test(label) && !label.startsWith("-") && !label.endsWith("-");
1465
+ });
1466
+ }
1467
+ hasResolvableEmailDomain(value) {
1468
+ if (!this.isStrictEmail(value)) return false;
1469
+ const domain = value.split("@")[1];
1470
+ const asciiDomain = domainToASCII(domain);
1471
+ return asciiDomain.length > 0 && asciiDomain.includes(".");
1472
+ }
1473
+ isSpoofSafeEmail(value) {
1474
+ if (!this.isEmailByFilter(value)) return false;
1475
+ const [localPart, domain] = value.split("@");
1476
+ return domainToASCII(domain).length > 0 && !this.hasAsciiControlCharacters(value) && localPart.normalize("NFKC") === localPart && domain.normalize("NFKC") === domain;
1477
+ }
1478
+ hasAsciiControlCharacters(value) {
1479
+ for (let index = 0; index < value.length; index++) {
1480
+ const codePoint = value.charCodeAt(index);
1481
+ if (codePoint <= 31 || codePoint === 127) return true;
1482
+ }
1483
+ return false;
1484
+ }
1485
+ getDecimalPlaces(value) {
1486
+ const stringValue = String(value).toLowerCase();
1487
+ if (!stringValue.includes(".")) return 0;
1488
+ return stringValue.split(".")[1]?.length ?? 0;
1489
+ }
1307
1490
  };
1308
1491
 
1309
1492
  //#endregion
@@ -1372,6 +1555,7 @@ const implicitRues = [
1372
1555
  "declined_if",
1373
1556
  "filled",
1374
1557
  "present",
1558
+ "presentsame",
1375
1559
  "required",
1376
1560
  "required_if",
1377
1561
  "required_unless",
@@ -2000,6 +2184,7 @@ var replaceAttributePayload = class {
2000
2184
  //#endregion
2001
2185
  //#region src/BaseValidator.ts
2002
2186
  var BaseValidator = class BaseValidator {
2187
+ excludedAttributes = /* @__PURE__ */ new Set();
2003
2188
  /**
2004
2189
  * The lang used to return error messages
2005
2190
  */
@@ -2144,6 +2329,9 @@ var BaseValidator = class BaseValidator {
2144
2329
  errors() {
2145
2330
  return this.messages;
2146
2331
  }
2332
+ getExcludedAttributes() {
2333
+ return [...this.excludedAttributes];
2334
+ }
2147
2335
  /**
2148
2336
  * Clear the error messages for the given keys.
2149
2337
  * If no keys are provided, all error messages will be cleared.
@@ -2260,6 +2448,7 @@ var BaseValidator = class BaseValidator {
2260
2448
  runAllValidations() {
2261
2449
  this.messages = new ErrorBag();
2262
2450
  this.validateAttributes = new validateAttributes(this.data, this.rules, this.getContext());
2451
+ this.excludedAttributes.clear();
2263
2452
  for (const property in this.rules) if (this.runValidation(property) === false) break;
2264
2453
  }
2265
2454
  /**
@@ -2268,6 +2457,7 @@ var BaseValidator = class BaseValidator {
2268
2457
  async runAllValidationsAsync() {
2269
2458
  this.messages = new ErrorBag();
2270
2459
  this.validateAttributes = new validateAttributes(this.data, this.rules, this.getContext());
2460
+ this.excludedAttributes.clear();
2271
2461
  for (const property in this.rules) if (await this.runValidationAsync(property) === false) break;
2272
2462
  }
2273
2463
  /**
@@ -2290,26 +2480,39 @@ var BaseValidator = class BaseValidator {
2290
2480
  * Run validation rules for the specified property and stop validation if needed
2291
2481
  */
2292
2482
  runValidation(property) {
2293
- if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) for (let i = 0; i < this.rules[property].length; i++) {
2294
- this.validateAttribute(property, this.rules[property][i]);
2295
- if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2296
- if (this.shouldStopValidating(property)) break;
2483
+ if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) {
2484
+ if (validationRuleParser.hasRule(property, ["exclude"], this.rules)) {
2485
+ this.excludedAttributes.add(property);
2486
+ return;
2487
+ }
2488
+ for (let i = 0; i < this.rules[property].length; i++) {
2489
+ this.validateAttribute(property, this.rules[property][i]);
2490
+ if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2491
+ if (this.shouldStopValidating(property)) break;
2492
+ }
2297
2493
  }
2298
2494
  }
2299
2495
  /**
2300
2496
  * Run validation rules for the specified property asynchronously and stop validation if needed
2301
2497
  */
2302
2498
  async runValidationAsync(property) {
2303
- if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) for (let i = 0; i < this.rules[property].length; i++) {
2304
- await this.validateAttribute(property, this.rules[property][i]);
2305
- if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2306
- if (this.shouldStopValidating(property)) break;
2499
+ if (Object.prototype.hasOwnProperty.call(this.rules, property) && Array.isArray(this.rules[property])) {
2500
+ if (validationRuleParser.hasRule(property, ["exclude"], this.rules)) {
2501
+ this.excludedAttributes.add(property);
2502
+ return;
2503
+ }
2504
+ for (let i = 0; i < this.rules[property].length; i++) {
2505
+ await this.validateAttribute(property, this.rules[property][i]);
2506
+ if (this.messages.keys().length > 0 && this.stopOnFirstFailureFlag === true) return false;
2507
+ if (this.shouldStopValidating(property)) break;
2508
+ }
2307
2509
  }
2308
2510
  }
2309
2511
  /**
2310
2512
  * Check if we should stop further validations on a given attribute.
2311
2513
  */
2312
2514
  shouldStopValidating(attribute) {
2515
+ if (this.excludedAttributes.has(attribute)) return true;
2313
2516
  return this.messages.has(attribute) && validationRuleParser.hasRule(attribute, ["bail"], this.rules);
2314
2517
  }
2315
2518
  /**
@@ -2334,7 +2537,7 @@ var BaseValidator = class BaseValidator {
2334
2537
  const method = `validate${buildValidationMethodName(rule)}`;
2335
2538
  if (rule !== "" && typeof this.validateAttributes[method] === "undefined") throw `Rule ${rule} is not valid`;
2336
2539
  if (!validatable) return;
2337
- const validation = this.validateAttributes[method](value, parameters, attribute);
2540
+ const validation = this.validateAttributes[method](value, parameters, attribute, this.getPrimaryAttribute(attribute));
2338
2541
  if (validation instanceof Promise) return validation.then((result) => {
2339
2542
  if (!result) this.addFailure(attribute, rule, value, parameters);
2340
2543
  });
@@ -3099,8 +3302,10 @@ var Validator = class Validator {
3099
3302
  */
3100
3303
  validatedData() {
3101
3304
  const validKeys = Object.keys(this.rules);
3305
+ const excluded = new Set(this.instance?.getExcludedAttributes() ?? []);
3102
3306
  const clean = {};
3103
3307
  for (const key of validKeys) {
3308
+ if (excluded.has(key)) continue;
3104
3309
  const value = deepFind(this.data, key);
3105
3310
  const resolvedValue = typeof value !== "undefined" ? value : deepFind(this.getContext().requestFiles ?? {}, key);
3106
3311
  if (typeof resolvedValue !== "undefined") deepSet(clean, key, resolvedValue);
@@ -3111,7 +3316,8 @@ var Validator = class Validator {
3111
3316
  * Return all validated input.
3112
3317
  */
3113
3318
  validated() {
3114
- return Object.fromEntries(Object.entries(this.data).filter(([key]) => key in this.rules));
3319
+ const excluded = new Set(this.instance?.getExcludedAttributes() ?? []);
3320
+ return Object.fromEntries(Object.entries(this.data).filter(([key]) => key in this.rules && !excluded.has(key)));
3115
3321
  }
3116
3322
  /**
3117
3323
  * Return a portion of validated input
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kanun",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Framework-agnostic TypeScript-first validation library.",
5
5
  "type": "module",
6
6
  "files": [