-
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
306 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'fets': patch | ||
--- | ||
|
||
Add `registerFormats` to support string formats such as `uuid`, `email` or `url` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
// based on https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js & | ||
// https://github.com/cfworker/cfworker/blob/main/packages/json-schema/src/format.ts | ||
|
||
import { FormatRegistry } from '@sinclair/typebox'; | ||
|
||
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 TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d(?::?\d\d)?)?$/i; | ||
const HOSTNAME = | ||
/^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i; | ||
// const URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i; | ||
const URIREF = | ||
/^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i; | ||
// uri-template: https://tools.ietf.org/html/rfc6570 | ||
const URITEMPLATE = | ||
// eslint-disable-next-line no-control-regex | ||
/^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i; | ||
const UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i; | ||
const JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$/; | ||
const JSON_POINTER_URI_FRAGMENT = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i; | ||
const RELATIVE_JSON_POINTER = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/; | ||
|
||
// date: http://tools.ietf.org/html/rfc3339#section-5.6 | ||
const FASTDATE = /^\d\d\d\d-[0-1]\d-[0-3]\d$/; | ||
// date-time: http://tools.ietf.org/html/rfc3339#section-5.6 | ||
const FASTTIME = /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i; | ||
const FASTDATETIME = | ||
/^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i; | ||
// uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js | ||
// const FASTURI = /^(?:[a-z][a-z0-9+-.]*:)(?:\/?\/)?[^\s]*$/i; | ||
const FASTURIREFERENCE = /^(?:(?:[a-z][a-z0-9+-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i; | ||
|
||
// https://github.com/ExodusMovement/schemasafe/blob/master/src/formats.js | ||
const EMAIL = (input: string) => { | ||
if (input[0] === '"') return false; | ||
const [name, host, ...rest] = input.split('@'); | ||
if (!name || !host || rest.length !== 0 || name.length > 64 || host.length > 253) return false; | ||
if (name[0] === '.' || name.endsWith('.') || name.includes('..')) return false; | ||
if (!/^[a-z0-9.-]+$/i.test(host) || !/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+$/i.test(name)) return false; | ||
return host.split('.').every(part => /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i.test(part)); | ||
}; | ||
|
||
// optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html | ||
const IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/; | ||
// optimized http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses | ||
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; | ||
|
||
// https://github.com/ExodusMovement/schemasafe/blob/master/src/formats.js | ||
const DURATION = (input: string) => | ||
input.length > 1 && | ||
input.length < 80 && | ||
(/^P\d+([.,]\d+)?W$/.test(input) || | ||
(/^P[\dYMDTHS]*(\d[.,]\d+)?[YMDHS]$/.test(input) && | ||
/^P([.,\d]+Y)?([.,\d]+M)?([.,\d]+D)?(T([.,\d]+H)?([.,\d]+M)?([.,\d]+S)?)?$/.test(input))); | ||
|
||
function bind(r: RegExp) { | ||
return r.test.bind(r); | ||
} | ||
|
||
const BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm; | ||
|
||
function byte(str: string): boolean { | ||
BYTE.lastIndex = 0; | ||
return BYTE.test(str); | ||
} | ||
|
||
function float(str: string): boolean { | ||
const n = +str; | ||
return !Number.isNaN(n) && Number.isFinite(n); | ||
} | ||
|
||
export const fullFormat: Record<FormatName, (s: string) => boolean> = { | ||
date, | ||
time: time.bind(undefined, false), | ||
// eslint-disable-next-line camelcase | ||
'date-time': date_time, | ||
duration: DURATION, | ||
uri, | ||
'uri-reference': bind(URIREF), | ||
'uri-template': bind(URITEMPLATE), | ||
url: v => { | ||
try { | ||
// eslint-disable-next-line no-new | ||
new URL(v); | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
}, | ||
email: EMAIL, | ||
hostname: bind(HOSTNAME), | ||
ipv4: bind(IPV4), | ||
ipv6: bind(IPV6), | ||
regex, | ||
uuid: bind(UUID), | ||
'json-pointer': bind(JSON_POINTER), | ||
'json-pointer-uri-fragment': bind(JSON_POINTER_URI_FRAGMENT), | ||
'relative-json-pointer': bind(RELATIVE_JSON_POINTER), | ||
'iso-date-time': bind( | ||
/^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, | ||
), | ||
'iso-time': bind(/^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i), | ||
byte, | ||
int32: v => Number.isSafeInteger(+v) && +v >= -2147483648 && +v <= 2147483647, | ||
int64: v => { | ||
const vNum = BigInt(v); | ||
return vNum >= -9223372036854775808n && vNum <= 9223372036854775807n; | ||
}, | ||
float, | ||
double: float, | ||
password: () => true, | ||
binary: () => true, | ||
}; | ||
|
||
export const fastFormat: Record<FormatName, (s: string) => boolean> = { | ||
...fullFormat, | ||
date: bind(FASTDATE), | ||
time: bind(FASTTIME), | ||
'date-time': bind(FASTDATETIME), | ||
'uri-reference': bind(FASTURIREFERENCE), | ||
}; | ||
|
||
function isLeapYear(year: number) { | ||
// https://tools.ietf.org/html/rfc3339#appendix-C | ||
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); | ||
} | ||
|
||
function date(str: string) { | ||
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6 | ||
const matches = str.match(DATE); | ||
if (!matches) return false; | ||
|
||
const year = +matches[1]; | ||
const month = +matches[2]; | ||
const day = +matches[3]; | ||
|
||
return ( | ||
month >= 1 && | ||
month <= 12 && | ||
day >= 1 && | ||
// eslint-disable-next-line eqeqeq | ||
day <= (month == 2 && isLeapYear(year) ? 29 : DAYS[month]) | ||
); | ||
} | ||
|
||
function time(full: boolean, str: string) { | ||
const matches = str.match(TIME); | ||
if (!matches) return false; | ||
|
||
const hour = +matches[1]; | ||
const minute = +matches[2]; | ||
const second = +matches[3]; | ||
const timeZone = !!matches[5]; | ||
return ( | ||
((hour <= 23 && minute <= 59 && second <= 59) || | ||
// eslint-disable-next-line eqeqeq | ||
(hour == 23 && minute == 59 && second == 60)) && | ||
(!full || timeZone) | ||
); | ||
} | ||
|
||
const DATE_TIME_SEPARATOR = /t|\s/i; | ||
// eslint-disable-next-line camelcase | ||
function date_time(str: string) { | ||
// http://tools.ietf.org/html/rfc3339#section-5.6 | ||
const dateTime = str.split(DATE_TIME_SEPARATOR); | ||
// eslint-disable-next-line eqeqeq | ||
return dateTime.length == 2 && date(dateTime[0]) && time(true, dateTime[1]); | ||
} | ||
|
||
const NOT_URI_FRAGMENT = /\/|:/; | ||
const URI_PATTERN = | ||
/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i; | ||
|
||
function uri(str: string): boolean { | ||
// http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "." | ||
return NOT_URI_FRAGMENT.test(str) && URI_PATTERN.test(str); | ||
} | ||
|
||
const Z_ANCHOR = /[^\\]\\Z/; | ||
function regex(str: string) { | ||
if (Z_ANCHOR.test(str)) return false; | ||
try { | ||
// eslint-disable-next-line no-new | ||
new RegExp(str); | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
|
||
export type FormatName = | ||
| 'date' | ||
| 'time' | ||
| 'date-time' | ||
| 'iso-time' | ||
| 'iso-date-time' | ||
| 'duration' | ||
| 'uri' | ||
| 'uri-reference' | ||
| 'uri-template' | ||
| 'url' | ||
| 'email' | ||
| 'hostname' | ||
| 'ipv4' | ||
| 'ipv6' | ||
| 'regex' | ||
| 'uuid' | ||
| 'json-pointer' | ||
| 'json-pointer-uri-fragment' | ||
| 'relative-json-pointer' | ||
| 'byte' | ||
| 'int32' | ||
| 'int64' | ||
| 'float' | ||
| 'double' | ||
| 'password' | ||
| 'binary'; | ||
|
||
interface RegisterFormatOpts { | ||
formats?: FormatName[]; | ||
type?: 'fast' | 'full'; | ||
} | ||
|
||
export function registerFormats({ | ||
formats = Object.keys(fastFormat) as FormatName[], | ||
type = 'fast', | ||
}: RegisterFormatOpts = {}) { | ||
const formatMap = type === 'full' ? fullFormat : fastFormat; | ||
for (const format of formats) { | ||
FormatRegistry.Set(format, formatMap[format]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters