From d93271ac4a5d982957b108a617e0ce572b306a16 Mon Sep 17 00:00:00 2001 From: Volcanoxp Date: Wed, 4 Dec 2024 16:56:30 -0500 Subject: [PATCH] feat: Add support for ISO 4217 Currencies --- README.md | 1 + README_ZH.md | 1 + deno/lib/README.md | 1 + deno/lib/ZodError.ts | 1 + deno/lib/__tests__/string.test.ts | 39 ++++++ deno/lib/types.ts | 208 +++++++++++++++++++++++++++++- src/ZodError.ts | 1 + src/__tests__/string.test.ts | 39 ++++++ src/types.ts | 208 +++++++++++++++++++++++++++++- 9 files changed, 497 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a3c888695..d9d087e33 100644 --- a/README.md +++ b/README.md @@ -798,6 +798,7 @@ z.string().date(); // ISO date format (YYYY-MM-DD) z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS]) z.string().duration(); // ISO 8601 duration z.string().base64(); +z.string().currency(); // ISO 4217 currencies ``` > Check out [validator.js](https://github.com/validatorjs/validator.js) for a bunch of other useful string validation functions that can be used in conjunction with [Refinements](#refine). diff --git a/README_ZH.md b/README_ZH.md index 0c9ab46a0..cdde89041 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -492,6 +492,7 @@ z.string().ip(); // 默认为 IPv4 和 IPv6,选项见下文 z.string().trim(); // 减除空白 z.string().toLowerCase(); // 小写化 z.string().toUpperCase(); // 大写化 +z.string().currency(); // ISO 4217 ``` > 请查看 [validator.js](https://github.com/validatorjs/validator.js),了解可与 [Refinements](#refine) 结合使用的大量其他有用字符串验证函数。 diff --git a/deno/lib/README.md b/deno/lib/README.md index a3c888695..d9d087e33 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -798,6 +798,7 @@ z.string().date(); // ISO date format (YYYY-MM-DD) z.string().time(); // ISO time format (HH:mm:ss[.SSSSSS]) z.string().duration(); // ISO 8601 duration z.string().base64(); +z.string().currency(); // ISO 4217 currencies ``` > Check out [validator.js](https://github.com/validatorjs/validator.js) for a bunch of other useful string validation functions that can be used in conjunction with [Refinements](#refine). diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts index e757cd8ba..59af7e540 100644 --- a/deno/lib/ZodError.ts +++ b/deno/lib/ZodError.ts @@ -104,6 +104,7 @@ export type StringValidation = | "duration" | "ip" | "base64" + | "currency" | { includes: string; position?: number } | { startsWith: string } | { endsWith: string }; diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index 64438717a..67c028226 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -374,6 +374,7 @@ test("checks getters", () => { expect(z.string().email().isNANOID).toEqual(false); expect(z.string().email().isIP).toEqual(false); expect(z.string().email().isULID).toEqual(false); + expect(z.string().email().isCurrency).toEqual(false); expect(z.string().url().isEmail).toEqual(false); expect(z.string().url().isURL).toEqual(true); @@ -383,6 +384,7 @@ test("checks getters", () => { expect(z.string().url().isNANOID).toEqual(false); expect(z.string().url().isIP).toEqual(false); expect(z.string().url().isULID).toEqual(false); + expect(z.string().url().isCurrency).toEqual(false); expect(z.string().cuid().isEmail).toEqual(false); expect(z.string().cuid().isURL).toEqual(false); @@ -392,6 +394,7 @@ test("checks getters", () => { expect(z.string().cuid().isNANOID).toEqual(false); expect(z.string().cuid().isIP).toEqual(false); expect(z.string().cuid().isULID).toEqual(false); + expect(z.string().cuid().isCurrency).toEqual(false); expect(z.string().cuid2().isEmail).toEqual(false); expect(z.string().cuid2().isURL).toEqual(false); @@ -401,6 +404,7 @@ test("checks getters", () => { expect(z.string().cuid2().isNANOID).toEqual(false); expect(z.string().cuid2().isIP).toEqual(false); expect(z.string().cuid2().isULID).toEqual(false); + expect(z.string().cuid2().isCurrency).toEqual(false); expect(z.string().uuid().isEmail).toEqual(false); expect(z.string().uuid().isURL).toEqual(false); @@ -410,6 +414,7 @@ test("checks getters", () => { expect(z.string().uuid().isNANOID).toEqual(false); expect(z.string().uuid().isIP).toEqual(false); expect(z.string().uuid().isULID).toEqual(false); + expect(z.string().uuid().isCurrency).toEqual(false); expect(z.string().nanoid().isEmail).toEqual(false); expect(z.string().nanoid().isURL).toEqual(false); @@ -419,6 +424,7 @@ test("checks getters", () => { expect(z.string().nanoid().isNANOID).toEqual(true); expect(z.string().nanoid().isIP).toEqual(false); expect(z.string().nanoid().isULID).toEqual(false); + expect(z.string().nanoid().isCurrency).toEqual(false); expect(z.string().ip().isEmail).toEqual(false); expect(z.string().ip().isURL).toEqual(false); @@ -428,6 +434,7 @@ test("checks getters", () => { expect(z.string().ip().isNANOID).toEqual(false); expect(z.string().ip().isIP).toEqual(true); expect(z.string().ip().isULID).toEqual(false); + expect(z.string().ip().isCurrency).toEqual(false); expect(z.string().ulid().isEmail).toEqual(false); expect(z.string().ulid().isURL).toEqual(false); @@ -437,6 +444,17 @@ test("checks getters", () => { expect(z.string().ulid().isNANOID).toEqual(false); expect(z.string().ulid().isIP).toEqual(false); expect(z.string().ulid().isULID).toEqual(true); + expect(z.string().ulid().isCurrency).toEqual(false); + + expect(z.string().currency().isEmail).toEqual(false); + expect(z.string().currency().isURL).toEqual(false); + expect(z.string().currency().isCUID).toEqual(false); + expect(z.string().currency().isCUID2).toEqual(false); + expect(z.string().currency().isUUID).toEqual(false); + expect(z.string().currency().isNANOID).toEqual(false); + expect(z.string().currency().isIP).toEqual(false); + expect(z.string().currency().isULID).toEqual(false); + expect(z.string().currency().isCurrency).toEqual(true); }); test("min max getters", () => { @@ -769,3 +787,24 @@ test("IP validation", () => { invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) ).toBe(true); }); + +test("currency", () => { + const currency = z.string().currency(); + expect(currency.isCurrency).toEqual(true); + + const validCurrencies = ["USD", "EUR", "CAD", "TOP", "ALL", "PEN"]; + + const invalidCurrencies = ["AAA", "322", "AXAXAX"]; + + const currencySchema = z.string().currency(); + expect( + validCurrencies.every( + (currency) => currencySchema.safeParse(currency).success + ) + ).toBe(true); + expect( + invalidCurrencies.every( + (currency) => currencySchema.safeParse(currency).success === false + ) + ).toBe(true); +}); diff --git a/deno/lib/types.ts b/deno/lib/types.ts index 5d020d278..024b60488 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -565,7 +565,8 @@ export type ZodStringCheck = } | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } - | { kind: "base64"; message?: string }; + | { kind: "base64"; message?: string } + | { kind: "currency"; message?: string }; export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -671,6 +672,193 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +// ISO 4217 +const currencies = new Set([ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRU", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLE", + "SOS", + "SRD", + "SSP", + "STN", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UYW", + "UZS", + "VED", + "VES", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "XSU", + "XTS", + "XUA", + "XXX", + "YER", + "ZAR", + "ZMW", + "ZWL", +]); + +function isValidCurrency(currency: string) { + if (currencies.has(currency)) return true; + return false; +} + export class ZodString extends ZodType { _parse(input: ParseInput): ParseReturnType { if (this._def.coerce) { @@ -943,6 +1131,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "currency") { + if (!isValidCurrency(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "currency", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else { util.assertNever(check); } @@ -1002,6 +1200,10 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); } + currency(message?: errorUtil.ErrMessage) { + return this._addCheck({ kind: "currency", ...errorUtil.errToObj(message) }); + } + ip(options?: string | { version?: IpVersion; message?: string }) { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } @@ -1203,6 +1405,10 @@ export class ZodString extends ZodType { return !!this._def.checks.find((ch) => ch.kind === "base64"); } + get isCurrency() { + return !!this._def.checks.find((ch) => ch.kind === "currency"); + } + get minLength() { let min: number | null = null; for (const ch of this._def.checks) { diff --git a/src/ZodError.ts b/src/ZodError.ts index c1f7aa3ee..ba18555d6 100644 --- a/src/ZodError.ts +++ b/src/ZodError.ts @@ -104,6 +104,7 @@ export type StringValidation = | "duration" | "ip" | "base64" + | "currency" | { includes: string; position?: number } | { startsWith: string } | { endsWith: string }; diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index f7037fcc2..d10b0fe40 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -373,6 +373,7 @@ test("checks getters", () => { expect(z.string().email().isNANOID).toEqual(false); expect(z.string().email().isIP).toEqual(false); expect(z.string().email().isULID).toEqual(false); + expect(z.string().email().isCurrency).toEqual(false); expect(z.string().url().isEmail).toEqual(false); expect(z.string().url().isURL).toEqual(true); @@ -382,6 +383,7 @@ test("checks getters", () => { expect(z.string().url().isNANOID).toEqual(false); expect(z.string().url().isIP).toEqual(false); expect(z.string().url().isULID).toEqual(false); + expect(z.string().url().isCurrency).toEqual(false); expect(z.string().cuid().isEmail).toEqual(false); expect(z.string().cuid().isURL).toEqual(false); @@ -391,6 +393,7 @@ test("checks getters", () => { expect(z.string().cuid().isNANOID).toEqual(false); expect(z.string().cuid().isIP).toEqual(false); expect(z.string().cuid().isULID).toEqual(false); + expect(z.string().cuid().isCurrency).toEqual(false); expect(z.string().cuid2().isEmail).toEqual(false); expect(z.string().cuid2().isURL).toEqual(false); @@ -400,6 +403,7 @@ test("checks getters", () => { expect(z.string().cuid2().isNANOID).toEqual(false); expect(z.string().cuid2().isIP).toEqual(false); expect(z.string().cuid2().isULID).toEqual(false); + expect(z.string().cuid2().isCurrency).toEqual(false); expect(z.string().uuid().isEmail).toEqual(false); expect(z.string().uuid().isURL).toEqual(false); @@ -409,6 +413,7 @@ test("checks getters", () => { expect(z.string().uuid().isNANOID).toEqual(false); expect(z.string().uuid().isIP).toEqual(false); expect(z.string().uuid().isULID).toEqual(false); + expect(z.string().uuid().isCurrency).toEqual(false); expect(z.string().nanoid().isEmail).toEqual(false); expect(z.string().nanoid().isURL).toEqual(false); @@ -418,6 +423,7 @@ test("checks getters", () => { expect(z.string().nanoid().isNANOID).toEqual(true); expect(z.string().nanoid().isIP).toEqual(false); expect(z.string().nanoid().isULID).toEqual(false); + expect(z.string().nanoid().isCurrency).toEqual(false); expect(z.string().ip().isEmail).toEqual(false); expect(z.string().ip().isURL).toEqual(false); @@ -427,6 +433,7 @@ test("checks getters", () => { expect(z.string().ip().isNANOID).toEqual(false); expect(z.string().ip().isIP).toEqual(true); expect(z.string().ip().isULID).toEqual(false); + expect(z.string().ip().isCurrency).toEqual(false); expect(z.string().ulid().isEmail).toEqual(false); expect(z.string().ulid().isURL).toEqual(false); @@ -436,6 +443,17 @@ test("checks getters", () => { expect(z.string().ulid().isNANOID).toEqual(false); expect(z.string().ulid().isIP).toEqual(false); expect(z.string().ulid().isULID).toEqual(true); + expect(z.string().ulid().isCurrency).toEqual(false); + + expect(z.string().currency().isEmail).toEqual(false); + expect(z.string().currency().isURL).toEqual(false); + expect(z.string().currency().isCUID).toEqual(false); + expect(z.string().currency().isCUID2).toEqual(false); + expect(z.string().currency().isUUID).toEqual(false); + expect(z.string().currency().isNANOID).toEqual(false); + expect(z.string().currency().isIP).toEqual(false); + expect(z.string().currency().isULID).toEqual(false); + expect(z.string().currency().isCurrency).toEqual(true); }); test("min max getters", () => { @@ -768,3 +786,24 @@ test("IP validation", () => { invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false) ).toBe(true); }); + +test("currency", () => { + const currency = z.string().currency(); + expect(currency.isCurrency).toEqual(true); + + const validCurrencies = ["USD", "EUR", "CAD", "TOP", "ALL", "PEN"]; + + const invalidCurrencies = ["AAA", "322", "AXAXAX"]; + + const currencySchema = z.string().currency(); + expect( + validCurrencies.every( + (currency) => currencySchema.safeParse(currency).success + ) + ).toBe(true); + expect( + invalidCurrencies.every( + (currency) => currencySchema.safeParse(currency).success === false + ) + ).toBe(true); +}); diff --git a/src/types.ts b/src/types.ts index f3730ae14..fe36a78e3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -565,7 +565,8 @@ export type ZodStringCheck = } | { kind: "duration"; message?: string } | { kind: "ip"; version?: IpVersion; message?: string } - | { kind: "base64"; message?: string }; + | { kind: "base64"; message?: string } + | { kind: "currency"; message?: string }; export interface ZodStringDef extends ZodTypeDef { checks: ZodStringCheck[]; @@ -671,6 +672,193 @@ function isValidIP(ip: string, version?: IpVersion) { return false; } +// ISO 4217 +const currencies = new Set([ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRU", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLE", + "SOS", + "SRD", + "SSP", + "STN", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UYW", + "UZS", + "VED", + "VES", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "XSU", + "XTS", + "XUA", + "XXX", + "YER", + "ZAR", + "ZMW", + "ZWL", +]); + +function isValidCurrency(currency: string) { + if (currencies.has(currency)) return true; + return false; +} + export class ZodString extends ZodType { _parse(input: ParseInput): ParseReturnType { if (this._def.coerce) { @@ -943,6 +1131,16 @@ export class ZodString extends ZodType { }); status.dirty(); } + } else if (check.kind === "currency") { + if (!isValidCurrency(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "currency", + code: ZodIssueCode.invalid_string, + message: check.message, + }); + status.dirty(); + } } else { util.assertNever(check); } @@ -1002,6 +1200,10 @@ export class ZodString extends ZodType { return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); } + currency(message?: errorUtil.ErrMessage) { + return this._addCheck({ kind: "currency", ...errorUtil.errToObj(message) }); + } + ip(options?: string | { version?: IpVersion; message?: string }) { return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); } @@ -1203,6 +1405,10 @@ export class ZodString extends ZodType { return !!this._def.checks.find((ch) => ch.kind === "base64"); } + get isCurrency() { + return !!this._def.checks.find((ch) => ch.kind === "currency"); + } + get minLength() { let min: number | null = null; for (const ch of this._def.checks) {