From 1cb07ad032d1afcd44efbc55139f0e88e87ecc4f Mon Sep 17 00:00:00 2001 From: sinclair Date: Mon, 19 Dec 2022 15:30:14 +0900 Subject: [PATCH] Example String Formats --- example/formats/additional.ts | 94 +++++++++++++++++++++++++++++++++++ example/formats/index.ts | 1 + 2 files changed, 95 insertions(+) create mode 100644 example/formats/additional.ts create mode 100644 example/formats/index.ts diff --git a/example/formats/additional.ts b/example/formats/additional.ts new file mode 100644 index 0000000..60396d3 --- /dev/null +++ b/example/formats/additional.ts @@ -0,0 +1,94 @@ +import { Format } from '@sinclair/typebox/format' + +/** Configures TypeBox for additional string formats */ +export namespace AdditionalFormats { + // ------------------------------------------------------------------------------------------- + // These formats are ported from the ajv-formats library. All credit goes to the original + // authors with the original code located at the following location. + // + // https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts + // ------------------------------------------------------------------------------------------- + + const UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i + const DATE_TIME_SEPARATOR = /t|\s/i + const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i + const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/ + const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + const IPV4 = /^(?:(?: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)$/ + const IPV6 = + /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i + const URL = + /^(?:https?|wss?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[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]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu + const EMAIL = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i + + function IsLeapYear(year: number): boolean { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) + } + + function IsDate(str: string): boolean { + const matches: string[] | null = DATE.exec(str) + if (!matches) return false + const year: number = +matches[1] + const month: number = +matches[2] + const day: number = +matches[3] + return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && IsLeapYear(year) ? 29 : DAYS[month]) + } + + function IsTime(str: string, strictTimeZone?: boolean): boolean { + const matches: string[] | null = TIME.exec(str) + if (!matches) return false + const hr: number = +matches[1] + const min: number = +matches[2] + const sec: number = +matches[3] + const tz: string | undefined = matches[4] + const tzSign: number = matches[5] === '-' ? -1 : 1 + const tzH: number = +(matches[6] || 0) + const tzM: number = +(matches[7] || 0) + if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false + if (hr <= 23 && min <= 59 && sec < 60) return true + // leap second + const utcMin = min - tzM * tzSign + const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0) + return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61 + } + + function IsDateTime(value: string, strictTimeZone?: boolean): boolean { + const dateTime: string[] = value.split(DATE_TIME_SEPARATOR) + return dateTime.length === 2 && IsDate(dateTime[0]) && IsTime(dateTime[1], strictTimeZone) + } + + function IsEmail(value: string) { + return EMAIL.test(value) + } + + function IsUuid(value: string) { + return UUID.test(value) + } + + function IsUrl(value: string) { + return URL.test(value) + } + + function IsIPv6(value: string) { + return IPV6.test(value) + } + + function IsIPv4(value: string) { + return IPV4.test(value) + } + + /** Configures additional string formats */ + export function Configure(): void { + Format.Set('date-time', (value) => IsDateTime(value, true)) + Format.Set('date', (value) => IsDate(value)) + Format.Set('time', (value) => IsTime(value)) + Format.Set('email', (value) => IsEmail(value)) + Format.Set('uuid', (value) => IsUuid(value)) + Format.Set('url', (value) => IsUrl(value)) + Format.Set('ipv6', (value) => IsIPv6(value)) + Format.Set('ipv4', (value) => IsIPv4(value)) + // + // optional: add additional formats + // + } +} diff --git a/example/formats/index.ts b/example/formats/index.ts new file mode 100644 index 0000000..2923362 --- /dev/null +++ b/example/formats/index.ts @@ -0,0 +1 @@ +export * from './additional'