From 801632388b9815d0c1695e8eb6f5cab1fd319dee Mon Sep 17 00:00:00 2001 From: Clemente Raposo Date: Tue, 29 Dec 2020 18:41:12 +0000 Subject: [PATCH] Add field type validators - Add validators for specific field types --- .../record/field/field.manager.spec.mock.ts | 3 +- .../validation.manager.spec.mock.ts | 24 +++- .../record/validation/validation.manager.ts | 24 ++++ .../validators/currency.validator.ts | 64 +++++++++ .../validation/validators/date.validator.ts | 54 ++++++++ .../validators/datetime.validator.ts | 51 ++++++++ .../validation/validators/email.validator.ts | 54 ++++++++ .../validation/validators/float.validator.ts | 63 +++++++++ .../validation/validators/int.validator.ts | 63 +++++++++ .../validation/validators/phone.validator.ts | 62 +++++++++ .../validation/validators/range.validator.ts | 122 ++++++++++++++++++ 11 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 core/app/src/services/record/validation/validators/currency.validator.ts create mode 100644 core/app/src/services/record/validation/validators/date.validator.ts create mode 100644 core/app/src/services/record/validation/validators/datetime.validator.ts create mode 100644 core/app/src/services/record/validation/validators/email.validator.ts create mode 100644 core/app/src/services/record/validation/validators/float.validator.ts create mode 100644 core/app/src/services/record/validation/validators/int.validator.ts create mode 100644 core/app/src/services/record/validation/validators/phone.validator.ts create mode 100644 core/app/src/services/record/validation/validators/range.validator.ts diff --git a/core/app/src/services/record/field/field.manager.spec.mock.ts b/core/app/src/services/record/field/field.manager.spec.mock.ts index 511f32a7a..6c919f9ee 100644 --- a/core/app/src/services/record/field/field.manager.spec.mock.ts +++ b/core/app/src/services/record/field/field.manager.spec.mock.ts @@ -1,4 +1,5 @@ import {FieldManager} from '@services/record/field/field.manager'; import {validationManagerMock} from '@services/record/validation/validation.manager.spec.mock'; +import {dataTypeFormatterMock} from '@services/formatters/data-type.formatter.spec.mock'; -export const fieldManagerMock = new FieldManager(validationManagerMock); +export const fieldManagerMock = new FieldManager(validationManagerMock, dataTypeFormatterMock); diff --git a/core/app/src/services/record/validation/validation.manager.spec.mock.ts b/core/app/src/services/record/validation/validation.manager.spec.mock.ts index 4696fa3f8..eaef09067 100644 --- a/core/app/src/services/record/validation/validation.manager.spec.mock.ts +++ b/core/app/src/services/record/validation/validation.manager.spec.mock.ts @@ -1,4 +1,26 @@ import {ValidationManager} from '@services/record/validation/validation.manager'; import {RequiredValidator} from '@services/record/validation/validators/required.validator'; +import {RangeValidator} from '@services/record/validation/validators/range.validator'; +import {CurrencyValidator} from '@services/record/validation/validators/currency.validator'; +import {DateValidator} from '@services/record/validation/validators/date.validator'; +import {DateTimeValidator} from '@services/record/validation/validators/datetime.validator'; +import {EmailValidator} from '@services/record/validation/validators/email.validator'; +import {FloatValidator} from '@services/record/validation/validators/float.validator'; +import {IntValidator} from '@services/record/validation/validators/int.validator'; +import {PhoneValidator} from '@services/record/validation/validators/phone.validator'; +import {numberFormatterMock} from '@services/formatters/number/number-formatter.spec.mock'; +import {dateFormatterMock} from '@services/formatters/datetime/date-formatter.service.spec.mock'; +import {datetimeFormatterMock} from '@services/formatters/datetime/datetime-formatter.service.spec.mock'; +import {phoneFormatterMock} from '@services/formatters/phone/phone-formatter.spec.mock'; -export const validationManagerMock = new ValidationManager(new RequiredValidator()); +export const validationManagerMock = new ValidationManager( + new RequiredValidator(), + new RangeValidator(), + new CurrencyValidator(numberFormatterMock), + new DateValidator(dateFormatterMock), + new DateTimeValidator(datetimeFormatterMock), + new EmailValidator(), + new FloatValidator(numberFormatterMock), + new IntValidator(numberFormatterMock), + new PhoneValidator(phoneFormatterMock) +); diff --git a/core/app/src/services/record/validation/validation.manager.ts b/core/app/src/services/record/validation/validation.manager.ts index b7c1b49d5..7a77de99e 100644 --- a/core/app/src/services/record/validation/validation.manager.ts +++ b/core/app/src/services/record/validation/validation.manager.ts @@ -6,6 +6,14 @@ import {AsyncValidatorFn, ValidatorFn} from '@angular/forms'; import {AsyncValidatorInterface} from '@services/record/validation/aync-validator.Interface'; import {OverridableMap} from '@app-common/types/OverridableMap'; import {RequiredValidator} from '@services/record/validation/validators/required.validator'; +import {CurrencyValidator} from '@services/record/validation/validators/currency.validator'; +import {DateValidator} from '@services/record/validation/validators/date.validator'; +import {DateTimeValidator} from '@services/record/validation/validators/datetime.validator'; +import {FloatValidator} from '@services/record/validation/validators/float.validator'; +import {IntValidator} from '@services/record/validation/validators/int.validator'; +import {EmailValidator} from '@services/record/validation/validators/email.validator'; +import {PhoneValidator} from '@services/record/validation/validators/phone.validator'; +import {RangeValidator} from '@services/record/validation/validators/range.validator'; export interface ValidationManagerInterface { registerValidator(module: string, key: string, validator: ValidatorInterface): void; @@ -30,12 +38,28 @@ export class ValidationManager implements ValidationManagerInterface { constructor( protected requiredValidator: RequiredValidator, + protected rangeValidator: RangeValidator, + protected currencyValidator: CurrencyValidator, + protected dateValidator: DateValidator, + protected datetimeValidator: DateTimeValidator, + protected emailValidator: EmailValidator, + protected floatValidator: FloatValidator, + protected intValidator: IntValidator, + protected phoneValidator: PhoneValidator, ) { this.validators = new OverridableMap(); this.asyncValidators = new OverridableMap(); this.validators.addEntry('default', 'required', requiredValidator); + this.validators.addEntry('default', 'range', rangeValidator); + this.validators.addEntry('default', 'currency', currencyValidator); + this.validators.addEntry('default', 'date', dateValidator); + this.validators.addEntry('default', 'datetime', datetimeValidator); + this.validators.addEntry('default', 'email', emailValidator); + this.validators.addEntry('default', 'float', floatValidator); + this.validators.addEntry('default', 'int', intValidator); + this.validators.addEntry('default', 'phone', phoneValidator); } public registerValidator(module: string, key: string, validator: ValidatorInterface): void { diff --git a/core/app/src/services/record/validation/validators/currency.validator.ts b/core/app/src/services/record/validation/validators/currency.validator.ts new file mode 100644 index 000000000..ac8489680 --- /dev/null +++ b/core/app/src/services/record/validation/validators/currency.validator.ts @@ -0,0 +1,64 @@ +import {ValidatorInterface} from '@services/record/validation/validator.Interface'; +import {AbstractControl} from '@angular/forms'; +import {Record} from '@app-common/record/record.model'; +import {ViewFieldDefinition} from '@app-common/metadata/metadata.model'; +import {Injectable} from '@angular/core'; +import {NumberFormatter} from '@services/formatters/number/number-formatter.service'; +import {StandardValidationErrors, StandardValidatorFn} from '@app-common/services/validators/validators.model'; + +export const currencyValidator = (formatter: NumberFormatter): StandardValidatorFn => ( + (control: AbstractControl): StandardValidationErrors | null => { + + if (control.value == null || control.value.length === 0) { + return null; + } + + const pattern = formatter.getFloatUserFormatPattern(); + const regex = new RegExp(pattern); + + if (regex.test(control.value)) { + return null; + } + + return { + currencyValidator: { + valid: false, + format: pattern, + message: { + labelKey: 'LBL_VALIDATION_ERROR_CURRENCY_FORMAT', + context: { + value: control.value, + expected: formatter.toUserFormat('1000.50') + } + } + } + }; + } +); + + +@Injectable({ + providedIn: 'root' +}) +export class CurrencyValidator implements ValidatorInterface { + + constructor(protected formatter: NumberFormatter) { + } + + applies(record: Record, viewField: ViewFieldDefinition): boolean { + if (!viewField || !viewField.fieldDefinition) { + return false; + } + + return viewField.type === 'currency'; + } + + getValidator(viewField: ViewFieldDefinition): StandardValidatorFn[] { + + if (!viewField || !viewField.fieldDefinition) { + return []; + } + + return [currencyValidator(this.formatter)]; + } +} diff --git a/core/app/src/services/record/validation/validators/date.validator.ts b/core/app/src/services/record/validation/validators/date.validator.ts new file mode 100644 index 000000000..6260a9c46 --- /dev/null +++ b/core/app/src/services/record/validation/validators/date.validator.ts @@ -0,0 +1,54 @@ +import {ValidatorInterface} from '@services/record/validation/validator.Interface'; +import {AbstractControl} from '@angular/forms'; +import {Record} from '@app-common/record/record.model'; +import {ViewFieldDefinition} from '@app-common/metadata/metadata.model'; +import {Injectable} from '@angular/core'; +import {DateFormatter} from '@services/formatters/datetime/date-formatter.service'; +import {StandardValidationErrors, StandardValidatorFn} from '@app-common/services/validators/validators.model'; + +export const dateValidator = (formatter: DateFormatter): StandardValidatorFn => ( + (control: AbstractControl): StandardValidationErrors | null => { + const invalid = formatter.validateUserFormat(control.value); + return invalid ? { + invalidDate: { + value: control.value, + message: { + labelKey: 'LBL_VALIDATION_ERROR_DATE_FORMAT', + context: { + value: control.value, + expected: formatter.toUserFormat('2020-01-12') + } + } + }, + + } : null; + } +); + +@Injectable({ + providedIn: 'root' +}) +export class DateValidator implements ValidatorInterface { + + constructor(protected formatter: DateFormatter) { + } + + applies(record: Record, viewField: ViewFieldDefinition): boolean { + if (!viewField || !viewField.fieldDefinition) { + return false; + } + + return viewField.type === 'date'; + } + + getValidator(viewField: ViewFieldDefinition): StandardValidatorFn[] { + + if (!viewField || !viewField.fieldDefinition) { + return []; + } + + return [dateValidator(this.formatter)]; + } + + +} diff --git a/core/app/src/services/record/validation/validators/datetime.validator.ts b/core/app/src/services/record/validation/validators/datetime.validator.ts new file mode 100644 index 000000000..f5e5fbf75 --- /dev/null +++ b/core/app/src/services/record/validation/validators/datetime.validator.ts @@ -0,0 +1,51 @@ +import {ValidatorInterface} from '@services/record/validation/validator.Interface'; +import {AbstractControl} from '@angular/forms'; +import {Record} from '@app-common/record/record.model'; +import {ViewFieldDefinition} from '@app-common/metadata/metadata.model'; +import {Injectable} from '@angular/core'; +import {DatetimeFormatter} from '@services/formatters/datetime/datetime-formatter.service'; +import {StandardValidationErrors, StandardValidatorFn} from '@app-common/services/validators/validators.model'; + +export const dateTimeValidator = (formatter: DatetimeFormatter): StandardValidatorFn => ( + (control: AbstractControl): StandardValidationErrors | null => { + const invalid = formatter.validateUserFormat(control.value); + return invalid ? { + dateTimeValidator: { + value: control.value, + message: { + labelKey: 'LBL_VALIDATION_ERROR_DATETIME_FORMAT', + context: { + value: control.value, + expected: formatter.toUserFormat('2020-01-12 12:30:40') + } + } + }, + } : null; + } +); + +@Injectable({ + providedIn: 'root' +}) +export class DateTimeValidator implements ValidatorInterface { + + constructor(protected formatter: DatetimeFormatter) { + } + + applies(record: Record, viewField: ViewFieldDefinition): boolean { + if (!viewField || !viewField.fieldDefinition) { + return false; + } + + return viewField.type === 'datetime'; + } + + getValidator(viewField: ViewFieldDefinition): StandardValidatorFn[] { + + if (!viewField || !viewField.fieldDefinition) { + return []; + } + + return [dateTimeValidator(this.formatter)]; + } +} diff --git a/core/app/src/services/record/validation/validators/email.validator.ts b/core/app/src/services/record/validation/validators/email.validator.ts new file mode 100644 index 000000000..4ccaeaf97 --- /dev/null +++ b/core/app/src/services/record/validation/validators/email.validator.ts @@ -0,0 +1,54 @@ +import {ValidatorInterface} from '@services/record/validation/validator.Interface'; +import {AbstractControl, Validators} from '@angular/forms'; +import {Record} from '@app-common/record/record.model'; +import {ViewFieldDefinition} from '@app-common/metadata/metadata.model'; +import {Injectable} from '@angular/core'; +import {StandardValidationErrors, StandardValidatorFn} from '@app-common/services/validators/validators.model'; + +export const emailValidator = (): StandardValidatorFn => ( + (control: AbstractControl): StandardValidationErrors | null => { + + const result = Validators.email(control); + + if (result === null) { + return null; + } + + return { + emailValidator: { + ...result, + message: { + labelKey: 'LBL_VALIDATION_ERROR_EMAIL_FORMAT', + context: { + value: control.value, + expected: 'example@example.org' + } + } + } + }; + } +); + + +@Injectable({ + providedIn: 'root' +}) +export class EmailValidator implements ValidatorInterface { + + applies(record: Record, viewField: ViewFieldDefinition): boolean { + if (!viewField || !viewField.fieldDefinition) { + return false; + } + + return viewField.type === 'email'; + } + + getValidator(viewField: ViewFieldDefinition): StandardValidatorFn[] { + + if (!viewField || !viewField.fieldDefinition) { + return []; + } + + return [emailValidator()]; + } +} diff --git a/core/app/src/services/record/validation/validators/float.validator.ts b/core/app/src/services/record/validation/validators/float.validator.ts new file mode 100644 index 000000000..1e8e4e9cb --- /dev/null +++ b/core/app/src/services/record/validation/validators/float.validator.ts @@ -0,0 +1,63 @@ +import {ValidatorInterface} from '@services/record/validation/validator.Interface'; +import {AbstractControl} from '@angular/forms'; +import {Record} from '@app-common/record/record.model'; +import {ViewFieldDefinition} from '@app-common/metadata/metadata.model'; +import {Injectable} from '@angular/core'; +import {NumberFormatter} from '@services/formatters/number/number-formatter.service'; +import {StandardValidationErrors, StandardValidatorFn} from '@app-common/services/validators/validators.model'; + +export const floatValidator = (formatter: NumberFormatter): StandardValidatorFn => ( + (control: AbstractControl): StandardValidationErrors | null => { + + if (control.value == null || control.value.length === 0) { + return null; + } + + const pattern = formatter.getFloatUserFormatPattern(); + const regex = new RegExp(pattern); + + if (regex.test(control.value)) { + return null; + } + + return { + floatValidator: { + valid: false, + format: pattern, + message: { + labelKey: 'LBL_VALIDATION_ERROR_FLOAT_FORMAT', + context: { + value: control.value, + expected: formatter.toUserFormat('1000.50') + } + } + } + }; + } +); + +@Injectable({ + providedIn: 'root' +}) +export class FloatValidator implements ValidatorInterface { + + constructor(protected formatter: NumberFormatter) { + } + + applies(record: Record, viewField: ViewFieldDefinition): boolean { + if (!viewField || !viewField.fieldDefinition) { + return false; + } + + return viewField.type === 'float'; + } + + getValidator(viewField: ViewFieldDefinition): StandardValidatorFn[] { + + if (!viewField || !viewField.fieldDefinition) { + return []; + } + + return [floatValidator(this.formatter)]; + } +} diff --git a/core/app/src/services/record/validation/validators/int.validator.ts b/core/app/src/services/record/validation/validators/int.validator.ts new file mode 100644 index 000000000..4c99297bd --- /dev/null +++ b/core/app/src/services/record/validation/validators/int.validator.ts @@ -0,0 +1,63 @@ +import {ValidatorInterface} from '@services/record/validation/validator.Interface'; +import {AbstractControl} from '@angular/forms'; +import {Record} from '@app-common/record/record.model'; +import {ViewFieldDefinition} from '@app-common/metadata/metadata.model'; +import {Injectable} from '@angular/core'; +import {NumberFormatter} from '@services/formatters/number/number-formatter.service'; +import {StandardValidationErrors, StandardValidatorFn} from '@app-common/services/validators/validators.model'; + +export const intValidator = (formatter: NumberFormatter): StandardValidatorFn => ( + (control: AbstractControl): StandardValidationErrors | null => { + + if (control.value == null || control.value.length === 0) { + return null; + } + + const pattern = formatter.getIntUserFormatPattern(); + const regex = new RegExp(pattern); + + if (regex.test(control.value)) { + return null; + } + + return { + intValidator: { + valid: false, + format: pattern, + message: { + labelKey: 'LBL_VALIDATION_ERROR_INT_FORMAT', + context: { + value: control.value, + expected: formatter.toUserFormat('1000') + } + } + } + }; + } +); + +@Injectable({ + providedIn: 'root' +}) +export class IntValidator implements ValidatorInterface { + + constructor(protected formatter: NumberFormatter) { + } + + applies(record: Record, viewField: ViewFieldDefinition): boolean { + if (!viewField || !viewField.fieldDefinition) { + return false; + } + + return viewField.type === 'int'; + } + + getValidator(viewField: ViewFieldDefinition): StandardValidatorFn[] { + + if (!viewField || !viewField.fieldDefinition) { + return []; + } + + return [intValidator(this.formatter)]; + } +} diff --git a/core/app/src/services/record/validation/validators/phone.validator.ts b/core/app/src/services/record/validation/validators/phone.validator.ts new file mode 100644 index 000000000..2e14f4bc1 --- /dev/null +++ b/core/app/src/services/record/validation/validators/phone.validator.ts @@ -0,0 +1,62 @@ +import {ValidatorInterface} from '@services/record/validation/validator.Interface'; +import {AbstractControl} from '@angular/forms'; +import {Record} from '@app-common/record/record.model'; +import {ViewFieldDefinition} from '@app-common/metadata/metadata.model'; +import {Injectable} from '@angular/core'; +import {PhoneFormatter} from '@services/formatters/phone/phone-formatter.service'; +import {StandardValidationErrors, StandardValidatorFn} from '@app-common/services/validators/validators.model'; + +export const phoneValidator = (formatter: PhoneFormatter): StandardValidatorFn => ( + (control: AbstractControl): StandardValidationErrors | null => { + + if (control.value == null || control.value.length === 0) { + return null; + } + + const pattern = formatter.getUserFormatPattern(); + const regex = new RegExp(pattern, 'g'); + + if (regex.test(control.value)) { + return null; + } + + return { + phoneValidator: { + valid: false, + format: pattern, + message: { + labelKey: 'LBL_VALIDATION_ERROR_PHONE_FORMAT', + context: { + value: control.value + } + } + } + }; + } +); + +@Injectable({ + providedIn: 'root' +}) +export class PhoneValidator implements ValidatorInterface { + + constructor(protected formatter: PhoneFormatter) { + } + + applies(record: Record, viewField: ViewFieldDefinition): boolean { + if (!viewField || !viewField.fieldDefinition) { + return false; + } + + return viewField.type === 'phone'; + } + + getValidator(viewField: ViewFieldDefinition): StandardValidatorFn[] { + + if (!viewField || !viewField.fieldDefinition) { + return []; + } + + return [phoneValidator(this.formatter)]; + } +} diff --git a/core/app/src/services/record/validation/validators/range.validator.ts b/core/app/src/services/record/validation/validators/range.validator.ts new file mode 100644 index 000000000..74bcc5c6d --- /dev/null +++ b/core/app/src/services/record/validation/validators/range.validator.ts @@ -0,0 +1,122 @@ +import {ValidatorInterface} from '@services/record/validation/validator.Interface'; +import {AbstractControl, Validators} from '@angular/forms'; +import {Record} from '@app-common/record/record.model'; +import {ViewFieldDefinition} from '@app-common/metadata/metadata.model'; +import {FieldDefinition, ValidationDefinition} from '@app-common/record/field.model'; +import {Injectable} from '@angular/core'; +import {StandardValidationErrors, StandardValidatorFn} from '@app-common/services/validators/validators.model'; + +export const minValidator = (min: number): StandardValidatorFn => ( + (control: AbstractControl): StandardValidationErrors | null => { + + const result = Validators.min(min)(control); + + if (result === null) { + return null; + } + + return { + emailValidator: { + ...result, + message: { + labelKey: 'LBL_VALIDATION_ERROR_MIN', + context: { + value: control.value, + min: '' + min + } + } + } + }; + } +); + +export const maxValidator = (max: number): StandardValidatorFn => ( + (control: AbstractControl): StandardValidationErrors | null => { + + const result = Validators.max(max)(control); + + if (result === null) { + return null; + } + + return { + emailValidator: { + ...result, + message: { + labelKey: 'LBL_VALIDATION_ERROR_MAX', + context: { + value: control.value, + max: '' + max + } + } + } + }; + } +); + +@Injectable({ + providedIn: 'root' +}) +export class RangeValidator implements ValidatorInterface { + + applies(record: Record, viewField: ViewFieldDefinition): boolean { + if (!viewField || !viewField.fieldDefinition) { + return false; + } + + const definition = viewField.fieldDefinition; + + return this.getRangeValidation(definition) !== null; + } + + getValidator(viewField: ViewFieldDefinition): StandardValidatorFn[] { + + if (!viewField || !viewField.fieldDefinition) { + return []; + } + + const validation = this.getRangeValidation(viewField.fieldDefinition); + + if (!validation) { + return []; + } + + const min = validation.min && parseInt('' + validation.min, 10); + const max = validation.max && parseInt('' + validation.max, 10); + const validations = []; + + if (isFinite(min)) { + validations.push(minValidator(min)); + } + + if (isFinite(max)) { + validations.push(maxValidator(max)); + } + + return validations; + } + + protected getRangeValidation(definition: FieldDefinition): ValidationDefinition { + + if (this.isRangeValidation(definition.validation)) { + return definition.validation; + } + + if (!definition.validations || !definition.validations.length) { + return null; + } + + let validation: ValidationDefinition = null; + + definition.validations.some(entry => { + validation = entry; + return this.isRangeValidation(entry); + }); + + return validation; + } + + protected isRangeValidation(validation: ValidationDefinition): boolean { + return validation && validation.type === 'range'; + } +}