anyvali 0.3.1 → 0.3.3
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/CHANGELOG.md +60 -44
- package/README.md +370 -370
- package/dist/schemas/optional.d.ts.map +1 -1
- package/dist/schemas/optional.js +4 -3
- package/dist/schemas/optional.js.map +1 -1
- package/package.json +40 -40
- package/sdk/js/CHANGELOG.md +13 -13
- package/src/format/validators.ts +71 -71
- package/src/index.ts +285 -285
- package/src/infer.ts +12 -12
- package/src/interchange/importer.ts +285 -285
- package/src/issue-codes.ts +19 -19
- package/src/schemas/base.ts +322 -322
- package/src/schemas/intersection.ts +81 -81
- package/src/schemas/object.ts +203 -203
- package/src/schemas/optional.ts +4 -3
- package/src/schemas/record.ts +55 -55
- package/src/schemas/string.ts +192 -192
- package/src/schemas/union.ts +53 -53
- package/src/types.ts +239 -239
- package/tests/unit/collections.test.ts +99 -99
- package/tests/unit/date-format.test.ts +18 -18
- package/tests/unit/default-mutation.test.ts +32 -32
- package/tests/unit/defaults.test.ts +70 -1
- package/tests/unit/inference.test.ts +306 -306
- package/tests/unit/interchange.test.ts +191 -191
- package/tests/unit/object.test.ts +208 -208
- package/tests/unit/security-recursion.test.ts +105 -105
- package/tests/unit/security.test.ts +945 -945
- package/tests/unit/shared-ref-falsepos.test.ts +33 -33
- package/tests/unit/string-pattern-redos.test.ts +46 -46
- package/tests/unit/string.test.ts +147 -147
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optional.d.ts","sourceRoot":"","sources":["../../src/schemas/optional.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAU,MAAM,WAAW,CAAC;AAE/C,qBAAa,cAAc,CACzB,CAAC,SAAS,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,UAAU,CAC3C,SAAQ,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;IACrD,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3B,gBAAgB,CAAC,kBAAkB,UAAQ;gBAE/B,KAAK,EAAE,CAAC;IAQpB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;IAOrD,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;
|
|
1
|
+
{"version":3,"file":"optional.d.ts","sourceRoot":"","sources":["../../src/schemas/optional.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAU,MAAM,WAAW,CAAC;AAE/C,qBAAa,cAAc,CACzB,CAAC,SAAS,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,UAAU,CAC3C,SAAQ,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;IACrD,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3B,gBAAgB,CAAC,kBAAkB,UAAQ;gBAE/B,KAAK,EAAE,CAAC;IAQpB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;IAOrD,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO;IAiBxD,OAAO,IAAI,UAAU;CAQtB"}
|
package/dist/schemas/optional.js
CHANGED
|
@@ -17,9 +17,10 @@ export class OptionalSchema extends BaseSchema {
|
|
|
17
17
|
}
|
|
18
18
|
_runPipeline(input, ctx) {
|
|
19
19
|
const isAbsent = input === undefined || input === ABSENT;
|
|
20
|
-
// If absent and we have a default
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// If absent and we have a default on this wrapper, use the normal
|
|
21
|
+
// BaseSchema default path so validation and cloning still run.
|
|
22
|
+
if (isAbsent && this._defaultValue !== ABSENT) {
|
|
23
|
+
return super._runPipeline(input, ctx);
|
|
23
24
|
}
|
|
24
25
|
if (isAbsent) {
|
|
25
26
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optional.js","sourceRoot":"","sources":["../../src/schemas/optional.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,OAAO,cAEX,SAAQ,UAA6C;IACrD,gBAAgB,CAAC,MAAM,CAAI;IAC3B,gBAAgB,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAE3C,YAAY,KAAQ;QAClB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,uCAAuC;QACvC,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC,aAAoB,CAAC;QAChD,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC,eAAe,CAAC;IAC/C,CAAC;IAED,SAAS,CAAC,KAAc,EAAE,GAAiB;QACzC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC5C,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC3C,CAAC;IAED,YAAY,CAAC,KAAc,EAAE,GAAiB;QAC5C,MAAM,QAAQ,GAAG,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,CAAC;QAEzD,
|
|
1
|
+
{"version":3,"file":"optional.js","sourceRoot":"","sources":["../../src/schemas/optional.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,OAAO,cAEX,SAAQ,UAA6C;IACrD,gBAAgB,CAAC,MAAM,CAAI;IAC3B,gBAAgB,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAE3C,YAAY,KAAQ;QAClB,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,uCAAuC;QACvC,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC,aAAoB,CAAC;QAChD,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC,eAAe,CAAC;IAC/C,CAAC;IAED,SAAS,CAAC,KAAc,EAAE,GAAiB;QACzC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC5C,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC3C,CAAC;IAED,YAAY,CAAC,KAAc,EAAE,GAAiB;QAC5C,MAAM,QAAQ,GAAG,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,CAAC;QAEzD,kEAAkE;QAClE,+DAA+D;QAC/D,IAAI,QAAQ,IAAI,IAAI,CAAC,aAAa,KAAK,MAAM,EAAE,CAAC;YAC9C,OAAO,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACxC,CAAC;QAED,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,iDAAiD;QACjD,OAAO,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO;QACL,MAAM,IAAI,GAAG;YACX,IAAI,EAAE,UAAmB;YACzB,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE;SAC7B,CAAC;QACF,IAAI,CAAC,WAAW,CAAC,IAA6B,CAAC,CAAC;QAChD,OAAO,IAA6B,CAAC;IACvC,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
2
|
"name": "anyvali",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Native validation with portable schema interchange",
|
|
5
|
-
"homepage": "https://anyvali.com",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "https://github.com/BetterCorp/AnyVali",
|
|
9
|
-
"directory": "sdk/js"
|
|
10
|
-
},
|
|
11
|
-
"type": "module",
|
|
12
|
-
"main": "./dist/index.js",
|
|
13
|
-
"types": "./dist/index.d.ts",
|
|
14
|
-
"exports": {
|
|
15
|
-
".": {
|
|
16
|
-
"import": "./dist/index.js",
|
|
17
|
-
"types": "./dist/index.d.ts"
|
|
18
|
-
},
|
|
19
|
-
"./forms": {
|
|
20
|
-
"import": "./dist/forms/index.js",
|
|
21
|
-
"types": "./dist/forms/index.d.ts"
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"build": "tsc",
|
|
26
|
-
"test": "vitest run",
|
|
27
|
-
"test:watch": "vitest",
|
|
28
|
-
"prepack": "node -e \"require('fs').copyFileSync('../../README.md','README.md')\"",
|
|
29
|
-
"postpack": "node -e \"try{require('fs').unlinkSync('README.md')}catch{}\""
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@vitest/coverage-v8": "^4.1.2",
|
|
33
|
-
"jsdom": "^29.1.1",
|
|
34
|
-
"typescript": "^6.0.2",
|
|
35
|
-
"vitest": "^4.1.2"
|
|
36
|
-
},
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=22"
|
|
39
|
-
},
|
|
40
|
-
"license": "MIT"
|
|
41
|
-
}
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Native validation with portable schema interchange",
|
|
5
|
+
"homepage": "https://anyvali.com",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/BetterCorp/AnyVali",
|
|
9
|
+
"directory": "sdk/js"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./forms": {
|
|
20
|
+
"import": "./dist/forms/index.js",
|
|
21
|
+
"types": "./dist/forms/index.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"prepack": "node -e \"require('fs').copyFileSync('../../README.md','README.md')\"",
|
|
29
|
+
"postpack": "node -e \"try{require('fs').unlinkSync('README.md')}catch{}\""
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
33
|
+
"jsdom": "^29.1.1",
|
|
34
|
+
"typescript": "^6.0.2",
|
|
35
|
+
"vitest": "^4.1.2"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|
package/sdk/js/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [0.1.0](https://github.com/BetterCorp/AnyVali/compare/js-v0.0.1...js-v0.1.0) (2026-03-30)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
### Features
|
|
7
|
-
|
|
8
|
-
* **js:** add forms bindings and DOM enhancement tests ([1cb7fa1](https://github.com/BetterCorp/AnyVali/commit/1cb7fa1493282c5db3207f1e552d86ac3c57b4b0))
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Bug Fixes
|
|
12
|
-
|
|
13
|
-
* update CI for Node 22/24 and fix CLI test runner ([43938a4](https://github.com/BetterCorp/AnyVali/commit/43938a4fb8f2e4867784b6729b71581e9e0dc8bf))
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0](https://github.com/BetterCorp/AnyVali/compare/js-v0.0.1...js-v0.1.0) (2026-03-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **js:** add forms bindings and DOM enhancement tests ([1cb7fa1](https://github.com/BetterCorp/AnyVali/commit/1cb7fa1493282c5db3207f1e552d86ac3c57b4b0))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* update CI for Node 22/24 and fix CLI test runner ([43938a4](https://github.com/BetterCorp/AnyVali/commit/43938a4fb8f2e4867784b6729b71581e9e0dc8bf))
|
package/src/format/validators.ts
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import type { StringFormat } from "../types.js";
|
|
2
|
-
|
|
3
|
-
// Email must have at least one dot after the @
|
|
4
|
-
const EMAIL_RE =
|
|
5
|
-
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|
|
6
|
-
|
|
7
|
-
const UUID_RE =
|
|
8
|
-
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
9
|
-
|
|
10
|
-
const IPV4_RE =
|
|
11
|
-
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
12
|
-
|
|
13
|
-
const IPV6_RE =
|
|
14
|
-
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?::[0-9a-fA-F]{1,4}){1,7}|::)$/;
|
|
15
|
-
|
|
16
|
-
// ISO 8601 date: YYYY-MM-DD
|
|
17
|
-
const DATE_RE = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
|
|
18
|
-
|
|
19
|
-
// ISO 8601 date-time: YYYY-MM-DDTHH:MM:SS with optional fractional seconds and timezone
|
|
20
|
-
const DATETIME_RE =
|
|
21
|
-
/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
22
|
-
|
|
23
|
-
function isValidDate(str: string): boolean {
|
|
24
|
-
if (!DATE_RE.test(str)) return false;
|
|
25
|
-
const [y, m, d] = str.split("-").map(Number);
|
|
26
|
-
const date = new Date(y, m - 1, d);
|
|
27
|
-
// new Date() maps years 0-99 to 1900-1999; undo that so early ISO years
|
|
28
|
-
// (e.g. 0050-01-01) are not falsely rejected.
|
|
29
|
-
date.setFullYear(y);
|
|
30
|
-
return (
|
|
31
|
-
date.getFullYear() === y &&
|
|
32
|
-
date.getMonth() === m - 1 &&
|
|
33
|
-
date.getDate() === d
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function isValidDateTime(str: string): boolean {
|
|
38
|
-
if (!DATETIME_RE.test(str)) return false;
|
|
39
|
-
// Validate the date portion
|
|
40
|
-
const datePart = str.substring(0, 10);
|
|
41
|
-
return isValidDate(datePart);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isValidUrl(str: string): boolean {
|
|
45
|
-
try {
|
|
46
|
-
const url = new URL(str);
|
|
47
|
-
// Only accept http and https protocols
|
|
48
|
-
return url.protocol === "http:" || url.protocol === "https:";
|
|
49
|
-
} catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const FORMAT_VALIDATORS: Record<StringFormat, (val: string) => boolean> = {
|
|
55
|
-
email: (val) => EMAIL_RE.test(val),
|
|
56
|
-
url: isValidUrl,
|
|
57
|
-
uuid: (val) => UUID_RE.test(val),
|
|
58
|
-
ipv4: (val) => IPV4_RE.test(val),
|
|
59
|
-
ipv6: (val) => IPV6_RE.test(val),
|
|
60
|
-
date: isValidDate,
|
|
61
|
-
"date-time": isValidDateTime,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const FORMAT_NAME_RE = /^[A-Za-z][A-Za-z0-9-]*$/;
|
|
65
|
-
|
|
66
|
-
export function validateFormat(value: string, format: string): boolean {
|
|
67
|
-
if (!FORMAT_NAME_RE.test(format)) return false;
|
|
68
|
-
const validator = FORMAT_VALIDATORS[format as StringFormat];
|
|
69
|
-
if (!validator) return true; // valid custom formats pass
|
|
70
|
-
return validator(value);
|
|
71
|
-
}
|
|
1
|
+
import type { StringFormat } from "../types.js";
|
|
2
|
+
|
|
3
|
+
// Email must have at least one dot after the @
|
|
4
|
+
const EMAIL_RE =
|
|
5
|
+
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|
|
6
|
+
|
|
7
|
+
const UUID_RE =
|
|
8
|
+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
9
|
+
|
|
10
|
+
const IPV4_RE =
|
|
11
|
+
/^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
12
|
+
|
|
13
|
+
const IPV6_RE =
|
|
14
|
+
/^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?::[0-9a-fA-F]{1,4}){1,7}|::)$/;
|
|
15
|
+
|
|
16
|
+
// ISO 8601 date: YYYY-MM-DD
|
|
17
|
+
const DATE_RE = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
|
|
18
|
+
|
|
19
|
+
// ISO 8601 date-time: YYYY-MM-DDTHH:MM:SS with optional fractional seconds and timezone
|
|
20
|
+
const DATETIME_RE =
|
|
21
|
+
/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
22
|
+
|
|
23
|
+
function isValidDate(str: string): boolean {
|
|
24
|
+
if (!DATE_RE.test(str)) return false;
|
|
25
|
+
const [y, m, d] = str.split("-").map(Number);
|
|
26
|
+
const date = new Date(y, m - 1, d);
|
|
27
|
+
// new Date() maps years 0-99 to 1900-1999; undo that so early ISO years
|
|
28
|
+
// (e.g. 0050-01-01) are not falsely rejected.
|
|
29
|
+
date.setFullYear(y);
|
|
30
|
+
return (
|
|
31
|
+
date.getFullYear() === y &&
|
|
32
|
+
date.getMonth() === m - 1 &&
|
|
33
|
+
date.getDate() === d
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isValidDateTime(str: string): boolean {
|
|
38
|
+
if (!DATETIME_RE.test(str)) return false;
|
|
39
|
+
// Validate the date portion
|
|
40
|
+
const datePart = str.substring(0, 10);
|
|
41
|
+
return isValidDate(datePart);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isValidUrl(str: string): boolean {
|
|
45
|
+
try {
|
|
46
|
+
const url = new URL(str);
|
|
47
|
+
// Only accept http and https protocols
|
|
48
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const FORMAT_VALIDATORS: Record<StringFormat, (val: string) => boolean> = {
|
|
55
|
+
email: (val) => EMAIL_RE.test(val),
|
|
56
|
+
url: isValidUrl,
|
|
57
|
+
uuid: (val) => UUID_RE.test(val),
|
|
58
|
+
ipv4: (val) => IPV4_RE.test(val),
|
|
59
|
+
ipv6: (val) => IPV6_RE.test(val),
|
|
60
|
+
date: isValidDate,
|
|
61
|
+
"date-time": isValidDateTime,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const FORMAT_NAME_RE = /^[A-Za-z][A-Za-z0-9-]*$/;
|
|
65
|
+
|
|
66
|
+
export function validateFormat(value: string, format: string): boolean {
|
|
67
|
+
if (!FORMAT_NAME_RE.test(format)) return false;
|
|
68
|
+
const validator = FORMAT_VALIDATORS[format as StringFormat];
|
|
69
|
+
if (!validator) return true; // valid custom formats pass
|
|
70
|
+
return validator(value);
|
|
71
|
+
}
|