From b6c0016413770c7713780dbd6a7e37b8eb25e10f Mon Sep 17 00:00:00 2001 From: Jamie Clarke Date: Sat, 12 Dec 2020 10:34:01 +0000 Subject: [PATCH 1/2] feat(validation): add between validators --- validation/public_api.ts | 2 + validation/src/exclusive-between.spec.ts | 155 +++++++++++++++++++++++ validation/src/exclusive-between.ts | 70 ++++++++++ validation/src/inclusive-between.spec.ts | 119 +++++++++++++++++ validation/src/inclusive-between.ts | 70 ++++++++++ 5 files changed, 416 insertions(+) create mode 100644 validation/src/exclusive-between.spec.ts create mode 100644 validation/src/exclusive-between.ts create mode 100644 validation/src/inclusive-between.spec.ts create mode 100644 validation/src/inclusive-between.ts diff --git a/validation/public_api.ts b/validation/public_api.ts index 3c2fe37b..142887d7 100644 --- a/validation/public_api.ts +++ b/validation/public_api.ts @@ -7,8 +7,10 @@ */ export { email, EmailValidationError } from './src/email'; export { equalTo, EqualToValidationError } from './src/equal-to'; +export { exclusiveBetween, ExclusiveBetweenValidationError } from './src/exclusive-between'; export { greaterThan, GreaterThanValidationError } from './src/greater-than'; export { greaterThanOrEqualTo, GreaterThanOrEqualToValidationError } from './src/greater-than-or-equal-to'; +export { inclusiveBetween, InclusiveBetweenValidationError } from './src/inclusive-between'; export { lessThan, LessThanValidationError } from './src/less-than'; export { lessThanOrEqualTo, LessThanOrEqualToValidationError } from './src/less-than-or-equal-to'; export { maxLength, MaxLengthValidationError } from './src/max-length'; diff --git a/validation/src/exclusive-between.spec.ts b/validation/src/exclusive-between.spec.ts new file mode 100644 index 00000000..e3473fec --- /dev/null +++ b/validation/src/exclusive-between.spec.ts @@ -0,0 +1,155 @@ +import { AbstractControlState, box, unbox, validate } from 'ngrx-forms'; +import { exclusiveBetween } from './exclusive-between'; + +describe(exclusiveBetween.name, () => { + it('should throw for null min parameter', () => { + expect(() => exclusiveBetween(null as any, 100)).toThrow(); + }); + + it('should throw for undefined min parameter', () => { + expect(() => exclusiveBetween(undefined as any, 100)).toThrow(); + }); + + it('should throw for null max parameter', () => { + expect(() => exclusiveBetween(0, null as any)).toThrow(); + }); + + it('should throw for undefined max parameter', () => { + expect(() => exclusiveBetween(0, undefined as any)).toThrow(); + }); + + it('should not return an error for null', () => { + expect(exclusiveBetween(0, 100)(null)).toEqual({}); + }); + + it('should not return an error for undefined', () => { + expect(exclusiveBetween(0, 100)(undefined)).toEqual({}); + }); + + it('should not return an error for non-numeric value', () => { + expect(exclusiveBetween(0, 100)('string' as any)).toEqual({}); + }); + + it('should not return an error if value is greater than min and less than max', () => { + expect(exclusiveBetween(0, 100)(50)).toEqual({}); + }); + + it('should return errors with min, max and actual properties if less than min', () => { + const min = 0; + const max = 100; + const actual = -1; + expect(exclusiveBetween(0, 100)(actual)).toEqual({ + exclusiveBetween: { + min, + max, + actual, + }, + }); + }); + + it('should return an errors with min, max and actual properties if equal to min', () => { + const min = 0; + const max = 100; + const actual = 0; + expect(exclusiveBetween(0, 100)(actual)).toEqual({ + exclusiveBetween: { + min, + max, + actual, + }, + }); + }); + + it('should return errors with min, max and actual properties if greater than max', () => { + const min = 0; + const max = 100; + const actual = 101; + expect(exclusiveBetween(0, 100)(actual)).toEqual({ + exclusiveBetween: { + min, + max, + actual, + }, + }); + }); + + it('should return an errors with min, max and actual properties if equal to max', () => { + const min = 0; + const max = 100; + const actual = 100; + expect(exclusiveBetween(0, 100)(actual)).toEqual({ + exclusiveBetween: { + min, + max, + actual, + }, + }); + }); + + it('should not return an error if boxed value is greater than min and less than max', () => { + expect(exclusiveBetween(0, 100)(box(50))).toEqual({}); + }); + + it('should return errors with min, max and actual properties for boxed values if less than min', () => { + const min = 0; + const max = 100; + const actual = box(-1); + expect(exclusiveBetween(min, max)(actual)).toEqual({ + exclusiveBetween: { + min, + max, + actual: unbox(actual), + }, + }); + }); + + it('should return an errors with min, max and actual properties for boxed values if equal to min', () => { + const min = 0; + const max = 100; + const actual = box(0); + expect(exclusiveBetween(0, 100)(actual)).toEqual({ + exclusiveBetween: { + min, + max, + actual: unbox(actual), + }, + }); + }); + + it('should return errors with min, max and actual properties for boxed values if greater than max', () => { + const min = 0; + const max = 100; + const actual = box(101); + expect(exclusiveBetween(min, max)(actual)).toEqual({ + exclusiveBetween: { + min, + max, + actual: unbox(actual), + }, + }); + }); + + it('should return an errors with min, max and actual properties for boxed values if equal to max', () => { + const min = 0; + const max = 100; + const actual = box(100); + expect(exclusiveBetween(0, 100)(actual)).toEqual({ + exclusiveBetween: { + min, + max, + actual: unbox(actual), + }, + }); + }); + + it('should properly infer value type when used with validate update function', () => { + // this code is never meant to be executed, it should just pass the type checker + if (1 !== 1) { + // tslint:disable-next-line:no-non-null-assertion + const state: AbstractControlState = undefined!; + const v = validate(state, exclusiveBetween(0, 100)); + const v2: number = v.value; + console.log(v2); + } + }); +}); diff --git a/validation/src/exclusive-between.ts b/validation/src/exclusive-between.ts new file mode 100644 index 00000000..1c256562 --- /dev/null +++ b/validation/src/exclusive-between.ts @@ -0,0 +1,70 @@ +import { Boxed, unbox, ValidationErrors } from 'ngrx-forms'; + +export interface ExclusiveBetweenValidationError { + min: number; + max: number; + actual: number; +} + +// @ts-ignore +declare module 'ngrx-forms/src/state' { + export interface ValidationErrors { + exclusiveBetween?: ExclusiveBetweenValidationError; + } +} + +/** + * A validation function that requires the value to be between the given min and max values. + * Considers `null`, `undefined` and non-numeric values as valid. Combine this function with the `required` + * validation function if `null` or `undefined` should be considered invalid. + * + * The validation error returned by this validation function has the following shape: + * + ```typescript + { + exclusiveBetween: { + min: number; + max: number; + actual: number; + }; +} + ``` + * + * Usually you would use this validation function in conjunction with the `validate` + * update function to perform synchronous validation in your reducer: + * + ```typescript + updateGroup({ + amount: validate(exclusiveBetween(0, 100)), +}) + ``` + * + * Note that this function is generic to allow the compiler to properly infer the type + * of the `validate` function for both optional and non-optional controls. + */ +export function exclusiveBetween(min: number, max: number) { + // tslint:disable-next-line:strict-type-predicates (guard for users without strict type checking) + if (min === null || min === undefined || max === null || max === undefined) { + throw new Error(`The exclusiveBetween Validation function requires the min and max parameters to be a non-null number, got ${min} and ${max}!`); + } + + return | null | undefined>(value: T): ValidationErrors => { + value = unbox(value) as number | null | undefined as T; + + if (value === null || value === undefined || typeof value !== 'number') { + return {}; + } + + if (min < value && value < max) { + return {}; + } + + return { + exclusiveBetween: { + min, + max, + actual: value as number, + }, + }; + }; +} diff --git a/validation/src/inclusive-between.spec.ts b/validation/src/inclusive-between.spec.ts new file mode 100644 index 00000000..ba456636 --- /dev/null +++ b/validation/src/inclusive-between.spec.ts @@ -0,0 +1,119 @@ +import { AbstractControlState, box, unbox, validate } from 'ngrx-forms'; +import { inclusiveBetween } from './inclusive-between'; + +describe(inclusiveBetween.name, () => { + it('should throw for null min parameter', () => { + expect(() => inclusiveBetween(null as any, 100)).toThrow(); + }); + + it('should throw for undefined min parameter', () => { + expect(() => inclusiveBetween(undefined as any, 100)).toThrow(); + }); + + it('should throw for null max parameter', () => { + expect(() => inclusiveBetween(0, null as any)).toThrow(); + }); + + it('should throw for undefined max parameter', () => { + expect(() => inclusiveBetween(0, undefined as any)).toThrow(); + }); + + it('should not return an error for null', () => { + expect(inclusiveBetween(0, 100)(null)).toEqual({}); + }); + + it('should not return an error for undefined', () => { + expect(inclusiveBetween(0, 100)(undefined)).toEqual({}); + }); + + it('should not return an error for non-numeric value', () => { + expect(inclusiveBetween(0, 100)('string' as any)).toEqual({}); + }); + + it('should not return an error if value is greater than min and less than max', () => { + expect(inclusiveBetween(0, 100)(50)).toEqual({}); + }); + + it('should not return an error if value is equal to min', () => { + expect(inclusiveBetween(0, 100)(0)).toEqual({}); + }); + + it('should not return an error if value is less than max', () => { + expect(inclusiveBetween(0, 100)(99)).toEqual({}); + }); + + it('should return errors with min, max and actual properties if less than min', () => { + const min = 0; + const max = 100; + const actual = -1; + expect(inclusiveBetween(0, 100)(actual)).toEqual({ + inclusiveBetween: { + min, + max, + actual, + }, + }); + }); + + it('should return errors with min, max and actual properties if greater than max', () => { + const min = 0; + const max = 100; + const actual = 101; + expect(inclusiveBetween(0, 100)(actual)).toEqual({ + inclusiveBetween: { + min, + max, + actual, + }, + }); + }); + + it('should not return an error if boxed value is greater than min and less than max', () => { + expect(inclusiveBetween(0, 100)(box(50))).toEqual({}); + }); + + it('should not return an error if boxed value is equal to min', () => { + expect(inclusiveBetween(0, 100)(box(0))).toEqual({}); + }); + + it('should not return an error if boxed value is equal to max', () => { + expect(inclusiveBetween(0, 100)(box(100))).toEqual({}); + }); + + it('should return errors with min, max and actual properties for boxed values if less than min', () => { + const min = 0; + const max = 100; + const actual = box(-1); + expect(inclusiveBetween(min, max)(actual)).toEqual({ + inclusiveBetween: { + min, + max, + actual: unbox(actual), + }, + }); + }); + + it('should return errors with min, max and actual properties for boxed values if greater than max', () => { + const min = 0; + const max = 100; + const actual = box(101); + expect(inclusiveBetween(min, max)(actual)).toEqual({ + inclusiveBetween: { + min, + max, + actual: unbox(actual), + }, + }); + }); + + it('should properly infer value type when used with validate update function', () => { + // this code is never meant to be executed, it should just pass the type checker + if (1 !== 1) { + // tslint:disable-next-line:no-non-null-assertion + const state: AbstractControlState = undefined!; + const v = validate(state, inclusiveBetween(0, 100)); + const v2: number = v.value; + console.log(v2); + } + }); +}); diff --git a/validation/src/inclusive-between.ts b/validation/src/inclusive-between.ts new file mode 100644 index 00000000..edcd8210 --- /dev/null +++ b/validation/src/inclusive-between.ts @@ -0,0 +1,70 @@ +import { Boxed, unbox, ValidationErrors } from 'ngrx-forms'; + +export interface InclusiveBetweenValidationError { + min: number; + max: number; + actual: number; +} + +// @ts-ignore +declare module 'ngrx-forms/src/state' { + export interface ValidationErrors { + inclusiveBetween?: InclusiveBetweenValidationError; + } +} + +/** + * A validation function that requires the value to be between or equal to the given min and max values. + * Considers `null`, `undefined` and non-numeric values as valid. Combine this function with the `required` + * validation function if `null` or `undefined` should be considered invalid. + * + * The validation error returned by this validation function has the following shape: + * + ```typescript + { + inclusiveBetween: { + min: number; + max: number; + actual: number; + }; +} + ``` + * + * Usually you would use this validation function in conjunction with the `validate` + * update function to perform synchronous validation in your reducer: + * + ```typescript + updateGroup({ + amount: validate(inclusiveBetween(0, 100)), +}) + ``` + * + * Note that this function is generic to allow the compiler to properly infer the type + * of the `validate` function for both optional and non-optional controls. + */ +export function inclusiveBetween(min: number, max: number) { + // tslint:disable-next-line:strict-type-predicates (guard for users without strict type checking) + if (min === null || min === undefined || max === null || max === undefined) { + throw new Error(`The inclusiveBetween Validation function requires the min and max parameters to be a non-null number, got ${min} and ${max}!`); + } + + return | null | undefined>(value: T): ValidationErrors => { + value = unbox(value) as number | null | undefined as T; + + if (value === null || value === undefined || typeof value !== 'number') { + return {}; + } + + if (min <= value && value <= max) { + return {}; + } + + return { + inclusiveBetween: { + min, + max, + actual: value as number, + }, + }; + }; +} From 41e313185018415d27c26f8e11850d45974d6165 Mon Sep 17 00:00:00 2001 From: Jamie Clarke Date: Sun, 27 Dec 2020 13:02:08 +0000 Subject: [PATCH 2/2] docs: add between validators --- docs/user-guide/validation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user-guide/validation.md b/docs/user-guide/validation.md index 07bbece1..e7ddd134 100644 --- a/docs/user-guide/validation.md +++ b/docs/user-guide/validation.md @@ -13,6 +13,8 @@ The following table lists all validation functions provided by **ngrx-forms**. |`lessThanOrEqualTo`|Requires the `number` value to be less than or equal to another number| |`greaterThan`|Requires the `number` value to be greater than another number| |`greaterThanOrEqualTo`|Requires the `number` value to be greater than or equal to another number| +|`exclusiveBetween`|Requires the `number` value to be between min and max numbers| +|`inclusiveBetween`|Requires the `number` value to be between or equal to min and max numbers| |`minLength`|Requires a `string` or `array` value to have a minimum length. Empty strings and arrays are always valid to allow for optional form controls. Use this function together with `required` if those values should not be valid| |`maxLength`|Requires a `string` or `array` value to have a maximum length| |`email`|Requires a `string` value to be a valid e-mail address. Empty strings are always valid to allow for optional form controls. Use this function together with `required` if empty strings should not be valid|