diff --git a/mod.ts b/mod.ts index 2f6dfc5..5b0eca1 100644 --- a/mod.ts +++ b/mod.ts @@ -6,7 +6,7 @@ export { Validatable, ArraySymbol, } from "./Validator.ts"; -export { isString } from "./validators/string.ts"; +export { isString, isUrl, isEmail } from "./validators/string.ts"; export { isNumber, isInteger } from "./validators/number.ts"; export { isArray } from "./validators/array.ts"; export { or } from "./validators/logic.ts"; diff --git a/validators/string.test.ts b/validators/string.test.ts index 070ad5b..43ac0ef 100644 --- a/validators/string.test.ts +++ b/validators/string.test.ts @@ -1,4 +1,4 @@ -import { isString, isURL } from "./string.ts"; +import { isString, isUrl, isEmail } from "./string.ts"; import { validate } from "../mod.ts"; import { assertEquals, @@ -35,20 +35,24 @@ Deno.test("isString (no match)", async () => { } }); -Deno.test("isURL (match)", async () => { +Deno.test("isUrl (match)", async () => { const values = [ "http://google.com", "http://10.1.1.1", "http://10.1.1.254", "http://223.255.255.254", - " data:,Hello World!" + " data:,Hello World!", ]; for (const value of values) { - assertEquals(await validate(value, isURL({allowLocal: true, allowDataUrl: true})), [], value); + assertEquals( + await validate(value, isUrl({ allowLocal: true, allowDataUrl: true })), + [], + "" + value, + ); } }); -Deno.test("isURL (no match)", async () => { +Deno.test("isUrl (no match)", async () => { const values = [ "invalid", "http://0.0.0.0", @@ -56,9 +60,67 @@ Deno.test("isURL (no match)", async () => { "http://10.1.1.255", "http://224.1.1.1", "http://1.1.1.1.1", - " data:,Hello World!" + " data:,Hello World!", ]; for (const value of values) { - assertNotEquals(await validate(value, isURL({allowLocal: true})), [], value); + assertNotEquals( + await validate(value, isUrl({ allowLocal: true })), + [], + "" + value, + ); + } +}); +Deno.test("isEmail (match)", async () => { + const values = [ + "email@example.com", + "firstname.lastname@example.com", + "email@subdomain.example.com", + "firstname+lastname@example.com", + "email@123.123.123.123", + "email@[123.123.123.123]", + "1234567890@example.com", + "234567890@example.com", + "email@example-one.com", + "_______@example.com", + "email@example.name", + "email@example.museum", + "email@example.co.jp", + "firstname-lastname@example.com", + ]; + for (const value of values) { + assertEquals( + await validate(value, isEmail()), + [], + "" + value, + ); + } +}); + +Deno.test("isEmail (no match)", async () => { + const values = [ + "", + 1, + "foo§@bar.baz", + "#@%^%#$@#$@#.com", + "@example.com", + "Joe Smith ", + "email.example.com", + "email@example@example.com", + ".email@example.com", + "email.@example.com", + "email..email@example.com", + "あいうえお@example.com", + "email@example.com (Joe Smith)", + "email@example", + "email@-example.com", + "email@example..com", + "Abc..123@example.com", + ]; + for (const value of values) { + assertNotEquals( + await validate(value, isEmail()), + [], + "" + value, + ); } }); diff --git a/validators/string.ts b/validators/string.ts index 0e5aa1a..696ad46 100644 --- a/validators/string.ts +++ b/validators/string.ts @@ -21,7 +21,7 @@ export function isString(): Validator { * * @param param Options */ -export function isURL( +export function isUrl( { protocols = ["http", "https"], allowDataUrl = false, @@ -31,7 +31,7 @@ export function isURL( allowDomain = true, allowBasicAuth = false, allowPort = true, - allowRecourcePath = true + allowRecourcePath = true, }: { protocols?: string[] | null; allowDataUrl?: boolean; @@ -45,19 +45,18 @@ export function isURL( } = {}, ): Validator { return { - type: "isURL", + type: "isUrl", extends: [isString()], check: (value: any) => { if (value === null || value === undefined) return; if (allowUrl) { - let regex = "^"; // protocol identifier (optional) // short syntax // still required if (protocols) { - regex += `(?:(?:(?:${protocols.join("|")}):)?\\/\\/)` + regex += `(?:(?:(?:${protocols.join("|")}):)?\\/\\/)`; } else { - regex += `(?:(?:(?:[a-z]+):)?\/\/)` + regex += `(?:(?:(?:[a-z]+):)?\/\/)`; } // user:pass BasicAuth (optional) if (allowBasicAuth) { @@ -71,8 +70,8 @@ export function isURL( } if (allowIp) { regex += "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + - "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + - "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))"; + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))"; } if (allowIp || allowDomain) { regex += "|"; // [hostname] ip end / domain start @@ -81,25 +80,24 @@ export function isURL( // host & domain names, may end with dot // can be replaced by a shortest alternative // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ - regex += + regex += "(?:" + "(?:" + - "(?:" + - "[a-z0-9\\u00a1-\\uffff]" + - "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + - ")?" + - "[a-z0-9\\u00a1-\\uffff]\\." + + "[a-z0-9\\u00a1-\\uffff]" + + "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + + ")?" + + "[a-z0-9\\u00a1-\\uffff]\\." + ")+" + // TLD identifier name, may end with dot - "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)"; } regex += ")"; // [hostname] end if (allowPort) { // port number (optional) - regex += "(?::\\d{2,5})?" + regex += "(?::\\d{2,5})?"; } if (allowRecourcePath) { // resource path (optional) - regex += "(?:[/?#]\\S*)?" + regex += "(?:[/?#]\\S*)?"; } regex += "$"; if (value.match(new RegExp(regex, "i"))) { @@ -107,15 +105,38 @@ export function isURL( } } if (allowDataUrl) { - const regex = /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i; + const regex = + /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i; if (value.match(regex)) { return; } } - return { }; + return {}; }, message: (value: any, args?: Args) => { return `This value is not a valid URL.`; }, }; } + +/** + * https://stackoverflow.com/questions/201323/how-to-validate-an-email-address-using-a-regular-expression + */ +export function isEmail(): Validator { + return { + type: "isEmail", + extends: [isString()], + check: (value: any) => { + if (value === null || value === undefined) return; + const regex = + /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i; + if (value.match(regex)) { + return; + } + return {}; + }, + message: (value: any, args?: Args) => { + return `This value has to be an email address.`; + }, + }; +}