Skip to content

Commit c411bfd

Browse files
committed
Change format validator regular expressions to match AJV.
1 parent 780a0a3 commit c411bfd

File tree

2 files changed

+97
-53
lines changed

2 files changed

+97
-53
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// AJV fast format regular expressions from:
2+
// https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js
3+
4+
export const jsonSchemaFormatTests = {
5+
6+
'date': /^\d\d\d\d-[0-1]\d-[0-3]\d$/,
7+
8+
'time': /^[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:z|[+-]\d\d:\d\d)?$/i,
9+
10+
'date-time': /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s][0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?(?:z|[+-]\d\d:\d\d)$/i,
11+
12+
// email (sources from jsen validator):
13+
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363
14+
// http://www.w3.org/TR/html5/forms.html#valid-e-mail-address (search for 'willful violation')
15+
'email': /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i,
16+
17+
'hostname': /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*$/i,
18+
19+
// optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html
20+
'ipv4': /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
21+
22+
// optimized http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses
23+
'ipv6': /^\s*(?:(?:(?:[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}))|:)))(?:%.+)?\s*$/i,
24+
25+
// uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
26+
'uri': /^(?:[a-z][a-z0-9+-.]*)(?::|\/)\/?[^\s]*$/i,
27+
28+
// uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A
29+
'uri-reference': /^(?:(?:[a-z][a-z0-9+-.]*:)?\/\/)?[^\s]*$/i,
30+
31+
// uri-template: https://tools.ietf.org/html/rfc6570
32+
'uri-template': /^(?:(?:[^\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,
33+
34+
// For the source: https://gist.github.com/dperini/729294
35+
// For test cases: https://mathiasbynens.be/demo/url-regex
36+
// @todo Delete current URL in favour of the commented out URL rule when this ajv issue is fixed https://github.com/eslint/eslint/issues/7983.
37+
// URL: /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!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-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)(?:\.(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu,
38+
'url': /^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i,
39+
40+
// uuid: http://tools.ietf.org/html/rfc4122
41+
'uuid': /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i,
42+
43+
// optimized https://gist.github.com/olmokramer/82ccce673f86db7cda5e
44+
'color': /^\s*(#(?:[\da-f]{3}){1,2}|rgb\((?:\d{1,3},\s*){2}\d{1,3}\)|rgba\((?:\d{1,3},\s*){3}\d*\.?\d+\)|hsl\(\d{1,3}(?:,\s*\d{1,3}%){2}\)|hsla\(\d{1,3}(?:,\s*\d{1,3}%){2},\s*\d*\.?\d+\))\s*$/gi,
45+
46+
// JSON-pointer: https://tools.ietf.org/html/rfc6901
47+
'json-pointer': /^(?:\/(?:[^~/]|~0|~1)*)*$|^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i,
48+
49+
'relative-json-pointer': /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/,
50+
51+
'regex': function(str) {
52+
if (/[^\\]\\Z/.test(str)) { return false; }
53+
try {
54+
new RegExp(str);
55+
return true;
56+
} catch(e) {
57+
return false;
58+
}
59+
}
60+
61+
};
62+
63+
export type JsonSchemaFormatNames =
64+
'date'|'time'|'date-time'|'email'|'hostname'|'ipv4'|'ipv6'|
65+
'uri'|'uri-reference'|'uri-template'|'url'|'uuid'|'color'|
66+
'json-pointer'|'relative-json-pointer'|'regex';

src/lib/src/shared/json.validators.ts

Lines changed: 31 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
PlainObject, IValidatorFn, AsyncIValidatorFn
1313
} from './validator.functions';
1414
import { forEachCopy } from './utility.functions';
15+
import { jsonSchemaFormatTests, JsonSchemaFormatNames } from './format-regex.constants';
1516

1617
/**
1718
* 'JsonValidators' class
@@ -53,15 +54,15 @@ import { forEachCopy } from './utility.functions';
5354
* NOTE / TODO: The dependencies validator is not complete.
5455
* NOTE / TODO: The contains validator is not complete.
5556
*
56-
* Validators not used by JSON Schema, but included for Angular compatibility,
57+
* Validators not used by JSON Schema (but included for compatibility)
5758
* and their JSON Schema equivalents:
5859
*
59-
* Angular function : JSON Schema equivalent
60-
* -------------------:-----------------------
61-
* min(number) : minimum(number)
62-
* max(number) : maximum(number)
63-
* requiredTrue() : const(true)
64-
* email() : format('email')
60+
* Angular validator | JSON Schema equivalent
61+
* ------------------|-----------------------
62+
* min(number) | minimum(number)
63+
* max(number) | maximum(number)
64+
* requiredTrue() | const(true)
65+
* email() | format('email')
6566
*
6667
* Validator transformation functions:
6768
* composeAnyOf, composeOneOf, composeAllOf, composeNot
@@ -92,10 +93,6 @@ import { forEachCopy } from './utility.functions';
9293
* Original Angular Validators:
9394
* https://github.com/angular/angular/blob/master/packages/forms/src/validators.ts
9495
*/
95-
96-
export type JsonSchemaStringFormat =
97-
'date-time'|'email'|'hostname'|'ipv4'|'ipv6'|'uri'|'url'|'color';
98-
9996
export class JsonValidators {
10097

10198
/**
@@ -319,14 +316,17 @@ export class JsonValidators {
319316
* Requires a control to have a value of a certain format.
320317
*
321318
* This validator currently checks the following formsts:
322-
* 'date-time'|'email'|'hostname'|'ipv4'|'ipv6'|'uri'|'url'|'color'
319+
* date, time, date-time, email, hostname, ipv4, ipv6,
320+
* uri, uri-reference, uri-template, url, uuid, color,
321+
* json-pointer, relative-json-pointer, regex
323322
*
324-
* TODO: add 'regex'format
323+
* Fast format regular expressions copied from AJV:
324+
* https://github.com/epoberezkin/ajv/blob/master/lib/compile/formats.js
325325
*
326-
* @param {JsonSchemaStringFormat} requiredFormat - format to check
326+
* @param {JsonSchemaFormatNames} requiredFormat - format to check
327327
* @return {IValidatorFn}
328328
*/
329-
static format(requiredFormat: JsonSchemaStringFormat): IValidatorFn {
329+
static format(requiredFormat: JsonSchemaFormatNames): IValidatorFn {
330330
if (!hasValue(requiredFormat)) { return JsonValidators.nullValidator; }
331331
return (control: AbstractControl, invert = false): ValidationErrors|null => {
332332
if (isEmpty(control.value)) { return null; }
@@ -335,36 +335,14 @@ export class JsonValidators {
335335
if (!isString(currentValue)) {
336336
isValid = false;
337337
} else {
338-
switch (requiredFormat) {
339-
case 'date-time':
340-
isValid = !!currentValue.match(/^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/);
341-
break;
342-
case 'email':
343-
let parts: string[] = currentValue.split('@');
344-
isValid =
345-
!!parts && parts.length === 2 &&
346-
!!parts[0].match(/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")$/)
347-
&&
348-
!!parts[1].match(/(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*\.?/);
349-
break;
350-
case 'hostname':
351-
isValid = !!currentValue.match(/(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*\.?/);
352-
break;
353-
case 'ipv4':
354-
isValid = !!currentValue.match(/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/);
355-
break;
356-
case 'ipv6':
357-
isValid = !!currentValue.match(/(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-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-Fa-f]{1,4}:){5}(((:[0-9A-Fa-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-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-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-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-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-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-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-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-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-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-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}))|:))/);
358-
break;
359-
case 'uri': case 'url':
360-
isValid = !!currentValue.match(/^((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)$/);
361-
break;
362-
case 'color':
363-
isValid = !!currentValue.match(/^#[A-Fa-f0-9]{6}$/);
364-
break;
365-
default:
366-
console.error(`format validator error: "${requiredFormat}" is not a recognized format.`);
367-
isValid = true;
338+
const formatTest: Function|RegExp = jsonSchemaFormatTests[requiredFormat];
339+
if (typeof formatTest === 'object') {
340+
isValid = (<RegExp>formatTest).test(currentValue);
341+
} else if (typeof formatTest === 'function') {
342+
isValid = (<Function>formatTest)(currentValue);
343+
} else {
344+
console.error(`format validator error: "${requiredFormat}" is not a recognized format.`);
345+
isValid = true;
368346
}
369347
}
370348
return xor(isValid, invert) ?
@@ -378,8 +356,8 @@ export class JsonValidators {
378356
* Requires a control's numeric value to be greater than or equal to
379357
* a minimum amount.
380358
*
381-
* Any non-numeric value is also valid (because a non-numeric value doesn't
382-
* have a minimum, according to the HTML forms spec).
359+
* Any non-numeric value is also valid (according to the HTML forms spec,
360+
* a non-numeric value doesn't have a minimum).
383361
* https://www.w3.org/TR/html5/forms.html#attr-input-max
384362
*
385363
* @param {number} minimum - minimum allowed value
@@ -401,8 +379,8 @@ export class JsonValidators {
401379
*
402380
* Requires a control's numeric value to be less than a maximum amount.
403381
*
404-
* Any non-numeric value is also valid (because a non-numeric value doesn't
405-
* have a maximum, according to the HTML forms spec).
382+
* Any non-numeric value is also valid (according to the HTML forms spec,
383+
* a non-numeric value doesn't have a maximum).
406384
* https://www.w3.org/TR/html5/forms.html#attr-input-max
407385
*
408386
* @param {number} exclusiveMinimumValue - maximum allowed value
@@ -425,8 +403,8 @@ export class JsonValidators {
425403
* Requires a control's numeric value to be less than or equal to
426404
* a maximum amount.
427405
*
428-
* Any non-numeric value is also valid (because a non-numeric value doesn't
429-
* have a maximum, according to the HTML forms spec).
406+
* Any non-numeric value is also valid (according to the HTML forms spec,
407+
* a non-numeric value doesn't have a maximum).
430408
* https://www.w3.org/TR/html5/forms.html#attr-input-max
431409
*
432410
* @param {number} maximumValue - maximum allowed value
@@ -448,8 +426,8 @@ export class JsonValidators {
448426
*
449427
* Requires a control's numeric value to be less than a maximum amount.
450428
*
451-
* Any non-numeric value is also valid (because a non-numeric value doesn't
452-
* have a maximum, according to the HTML forms spec).
429+
* Any non-numeric value is also valid (according to the HTML forms spec,
430+
* a non-numeric value doesn't have a maximum).
453431
* https://www.w3.org/TR/html5/forms.html#attr-input-max
454432
*
455433
* @param {number} exclusiveMaximumValue - maximum allowed value

0 commit comments

Comments
 (0)