Introduction
I have built a TypeScript based generic, fluent validation framework. Also, an Angular 6 CLI app to demo on how to use the framework for client-side validation.
Rules |
Description |
If |
Used for program flow. The then part is only evaluated if the if part is true. |
ForEach |
Used to iterate and apply validations to an array. |
ForStringProperty |
Used to stack multiple validations against a single string property. |
ForDateProperty |
Used to stack multiple validations against a single Date property. |
ForNumberProperty |
Used to stack multiple validations against a single Number property. |
For (deprecated) |
Used to stack multiple validations against a single property. |
ForType |
Used to chain validation rules for a type against a single property. |
Required |
Used to test if a property is true to a condition. |
NotNull |
Used to test if a property is not null. |
IsNull |
Used to test if a property is null. |
ToResult |
Returns the validation result. |
String Rules |
Description |
NotEmpty |
Used to test if a string is not empty. |
IsEmpty |
Used to test if a string is empty. |
Length |
Used to test if a string length is between specified lengths. |
Matches |
Used to test if a string matches a regular expression. |
NotMatches |
Used to test if a string does not match a regular expression. |
Email |
Used to test if a string is a valid email address. |
IsLowercase |
Used to test if a string is lower case. |
IsUppercase |
Used to test if a string is an upper case. |
IsMixedcase |
Used to test if a string is a mixed case. |
IsNumeric |
Used to test if a string is numeric. |
IsAlpha |
Used to test if a string is an alpha. |
IsAlphaNumeric |
Used to test if a string is alphanumeric. |
IsGuid |
Used to test if a string is a guide/UUID. |
IsBase64 |
Used to test if a string is base64. |
IsUrl |
Used to test if a string is a URL. |
IsCountryCode |
Used to test if a string is a 2 letter country code. |
Contains |
Used to test if a substring is contained in the string. |
IsCreditCard |
Used to test if a string is a valid credit card number. |
Date Rules |
Description |
IsDateOn |
Used to test if a date is on the specified date. |
IsDateAfter |
Used to test if a date is after the specified date. |
IsDateOnOrAfter |
Used to test if a date is on or after the specified date. |
IsDateBefore |
Used to test if a date is before the specified date. |
IsDateOnOrBefore |
Used to test if a date is on or before the specified date. |
IsDateBetween |
Used to test if a date is between two specified dates. |
IsDateLeapYear |
Used to test if a date is in a leap year. |
Number Rules |
Description |
IsNumberEqual |
Used to test if a number is equal to a specified number. |
IsNumberNotEqual |
Used to test if a number is not equal to a specified number. |
IsNumberLessThan |
Used to test if a number is less than a specified number. |
IsNumberLessThanOrEqual |
Used to test if a number is less than or equal to a specified number. |
IsNumberGreaterThan |
Used to test if a number is greater than a specified number. |
IsNumberGreaterThanOrEqual |
Used to test if a number is greater than or equal to a specified number. |
CreditCard |
Used to test if a number is a valid credit card number. |
- The framework provides these rules to build validators with.
- The rules can be chained in a fluent manner to create the validation rules around a model using IValidator<T> interface the framework provides.
- The framework being generic can be adapted for any model.
- The validator validates the entire model as per the rules and returns a validation result.
- The validation result contains validation errors.
Sample Usage
Models
- class Employee {
- Name: string;
- Password: string;
- PreviousPasswords: string[];
- CreditCards: CreditCard[];
- Super: Super;
- Email: string;
- }
- class CreditCard {
- Number: string;
- Name: string;
- ExpiryDate: Date;
- }
- class Super {
- Name: string;
- Code: string;
- }
Validation rules
-
- import {
- IValidator,
- Validator,
- ValidationResult
- } from 'ts.validator.fluent/dist';
- let validateSuperRules = (validator: IValidator < Super > ): ValidationResult => {
- return validator
- .NotNull(m => m.Name, "Should not be null", "Super.Name.Null")
- .NotNull(m => m.Code, "Should not be null", "Super.Code.Null")
- .If(m => m.Name != null && m.Code != null, validator => validator
- .NotEmpty(m => m.Name, "Should not be empty", "Super.Name.Empty")
- .Matches(m => m.Code, "^[a-zA-Z]{2}\\d{4}$", "Should not be invalid", "Super.Code.Invalid")
- .ToResult())
- .ToResult();
- };
- let validateCreditCardRules = (validator: IValidator < CreditCard > ): ValidationResult => {
- return validator
- .NotNull(m => m.Name, "Should not be null", "CreditCard.Name.Null")
- .NotNull(m => m.Number, "Should not be null", "CreditCard.Number.Null")
- .NotNull(m => m.ExpiryDate, "Should not be null", "CreditCard.ExpiryDate.Null")
- .If(m => m.Name != null && m.ExpiryDate != null, validator => validator
- .NotEmpty(m => m.Name, "Should not be empty", "CreditCard.Name.Empty")
- .IsCreditCard(m => m.Number, "Should not be invalid", "CreditCard.Number.Invalid")
- .IsDateOnOrAfter(m => m.ExpiryDate, new Date(), "Should be on or after today's date", "CreditCard.ExpiryDate.Invalid")
- .ToResult())
- .ToResult();
- };
- let validateEmployeeRules = (validator: IValidator < Employee > ): ValidationResult => {
- return validator.NotEmpty(m => m.Name, "Should not be empty", "Employee.Name.Empty").NotNull(m => m.CreditCards, "Should not be null", "CreditCard.Null").NotNull(m => m.Super, "Should not be null", "Super.Null").NotEmpty(m => m.Email, "Should not be empty", "Employee.Email.Empty").If(m => m.Super != null, validator => validator.ForType(m => m.Super, validateSuperRules).ToResult()).If(m => m.Email != '', validator => validator.Email(m => m.Email, "Should not be invalid", "Employee.Email.Invalid").ToResult()).Required(m => m.CreditCards, (m, creditCards) => creditCards.length > 0, "Must have atleast 1 credit card", "Employee.CreditCards.Required").If(m => m.CreditCards != null && m.CreditCards.length > 0, validator => validator.ForEach(m => m.CreditCards, validateCreditCardRules).ToResult()).If(m => m.Password != '', validator => validator.ForStringProperty(m => m.Password, passwordValidator => passwordValidator.Matches("(?=.*?[0-9])(?=.*?[a-z])(?=.*?[A-Z])", "Password strength is not valid", "Employee.Password.Strength").Required((m, pwd) => pwd.length > 3, "Password length should be greater than 3", "Employee.Password.Length").Required((m, pwd) => !m.PreviousPasswords.some(prevPwd => prevPwd == pwd), "Password is already used", "Employee.Password.AlreadyUsed").ToResult()).ToResult()).ToResult();
- };
Populate models
- let model = new Employee();
- model.Name = "John Doe";
- model.Password = "sD4A3";
- model.PreviousPasswords = new Array < string > ()
- model.PreviousPasswords.push("sD4A");
- model.PreviousPasswords.push("sD4A1");
- model.PreviousPasswords.push("sD4A2");
- var expiryDate = new Date();
- model.CreditCards = new Array < CreditCard > ();
- var masterCard = new CreditCard();
- masterCard.Number = "5105105105105100";
- masterCard.Name = "John Doe"
- masterCard.ExpiryDate = expiryDate;
- var amexCard = new CreditCard();
- amexCard.Number = "371449635398431";
- amexCard.Name = "John Doe"
- amexCard.ExpiryDate = expiryDate;
- model.CreditCards.push(masterCard);
- model.CreditCards.push(amexCard);
- model.Super = new Super();
- model.Super.Name = "XYZ Super Fund";
- model.Super.Code = "XY1234";
- model.Email = "[email protected]";
Synchronous validation
- let validationResult = new Validator(model).Validate(validateEmployeeRules);
Asynchronous validation
- let validationResult = await new Validator(model).ValidateAsync(validateEmployeeRules);
Validation result
- let isValid = validationResult.IsValid;
- let allErrors = validationResult.Errors;
- let employeeNameError = validationResult.Identifier("Employee.Name.Empty");
- let superCodeErrors = validationResult.IdentifierStartsWith("Super.Code");
Summary of above code snippets
- The models are Employee, Credit Card, Super.
- The Employee model has CreditCard and Super as the child models.
- First, an object of the Employee model is created and the data for the properties populated.
- The rules for Super and Employee validation are laid in the validateSuperRules, validateCreditCardRules and validateEmployeeRules functions, using the IValidator<T> interface the framework provides.
- The Super rules are chained and used in Employee validation.
- The rules are the same for both Sync and Async.
- For Sync and Async validation, the Validate and ValidateAsync methods on the framework class Validator are used.
- The Employee object is passed to this class and goes through the validation rules laid.
- Each validation rule comprises of a property on which the validation will apply, a message for any error and an identifier string for the error.
- The identifier string is used to group messages together for a field.
- The framework provides an API called IdentifierStartsWith which fetches all the validation errors for a particular identifier that starts with the text.
- Eg. “Super. Code” will fetch all errors whose identifier starts with Super. Code.
Inheritance support
Let us say there is a class Accountant that inherits from Employee.
Models
- class Accountant extends Employee {
- Code: string;
- }
Validation rules
- let validateAccountantRules = (validator: IValidator < Accountant > ): ValidationResult => {
- return validator.NotEmpty(m => m.Code, "Should not be empty").ToResult();
- };
Populate models
- let accountant = new Accountant();
- accountant.Code = "ACC001";
- accountant.Name = "John Doe";
- accountant.Password = "sD4A3";
- accountant.PreviousPasswords = new Array < string > ()
- accountant.PreviousPasswords.push("sD4A");
- accountant.PreviousPasswords.push("sD4A1");
- accountant.PreviousPasswords.push("sD4A2");
- var expiryDate = new Date();
- accountant.CreditCards = new Array < CreditCard > ();
- var masterCard = new CreditCard();
- masterCard.Number = "5105105105105100";
- masterCard.Name = "John Doe"
- masterCard.ExpiryDate = expiryDate;
- var amexCard = new CreditCard();
- amexCard.Number = "371449635398431";
- amexCard.Name = "John Doe";
- amexCard.ExpiryDate = expiryDate;
- accountant.CreditCards.push(masterCard);
- accountant.CreditCards.push(amexCard);
- accountant.Super = new Super();
- accountant.Super.Name = "XYZ Super Fund";
- accountant.Super.Code = "XY1234";
- accountant.Email = "[email protected]";
Synchronous validation
- let validationResult = new Validator(accountant).ValidateBase(validateEmployeeRules).Validate(validateAccountantRules);
Asynchronous validation
- let validationResult = await new Validator(accountant).ValidateBaseAsync(validateEmployeeRules).ValidateAsync(validateAccountantRules);
Summary of above code snippets
- The Accountant model inherits from Employee.
- The validation rules for the Accountant model (validateAccountantRules) only validate the properties of the Accountant class.
- The rules for base class Employee are validated using ValidateBase and ValidateBaseAsync methods and the Employee validation rules.
Angular 6 CLI app to demo the framework
The app uses a Service-oriented approach to validation, which has the below advantages,
- The business rules around validation are separated from the components.
- The business rules are centralized in the validation service.
- The service is re-usable.
- The service can be injected into any component.
- You can unit test the business rules by unit testing the service.
Service
- import {
- Injectable
- } from '@angular/core';
- import {
- IValidator,
- Validator,
- ValidationResult
- } from 'ts.validator.fluent/dist';
- import {
- IValidationService
- } from './ivalidation-service';
- import {
- User,
- RegisterUser,
- AgeGroupEnum
- } from '../models/models.component';
- @Injectable()
- export class ValidationService implements IValidationService {
- validateUser(model: User): ValidationResult {
- return new Validator(model).Validate(this.validateUserRules);
- }
- async validateUserAsync(model: User): Promise < ValidationResult > {
- return await new Validator(model).ValidateAsync(this.validateUserRules);
- }
- validateRegisterUser(model: RegisterUser): ValidationResult {
- return new Validator(model).Validate(this.validateRegisterUserRules);
- }
- async validateRegisterUserAsync(model: RegisterUser): Promise < ValidationResult > {
- return await new Validator(model).ValidateAsync(this.validateRegisterUserRules);
- }
- validateUserRules = (validator: IValidator < User > ): ValidationResult => {
- return validator
- .NotEmpty(m => m.Id, "Id cannot be empty", "Id")
- .NotEmpty(m => m.Pwd, "Pwd cannot be empty", "Pwd")
- .ToResult();
- };
- validateRegisterUserRules = (validator: IValidator < RegisterUser > ): ValidationResult => {
- return validator
- .NotEmpty(m => m.Name, "Name cannot be empty", "Name:Empty")
- .NotEmpty(m => m.CreditCardNo, "Credit Card Number cannot be empty", "CreditCardNo:Empty")
- .If(m => m.CreditCardNo != "", validator =>
- validator.ForStringProperty(m => m.CreditCardNo, creditCardValidator =>
- creditCardValidator.Length(13, 19, "Credit Card Number length is invalid", "CreditCardNo:LengthInvalid")
- .IsCreditCard("Credit Card Number is invalid", "CreditCardNo:Invalid")
- .ToResult()
- )
- .ToResult())
- .NotEmpty(m => m.Id, "Id cannot be empty", "Id:Empty")
- .NotEmpty(m => m.Email, "Email cannot be empty", "Email:Empty")
- .If(m => m.Email != "", validator =>
- validator.Email(m => m.Email, "Email is invalid", "Email:Invalid")
- .ToResult())
- .NotEmpty(m => m.Password, "Pwd cannot be empty", "Password:Empty")
- .NotEmpty(m => m.ConfirmPassword, "Confirm Pwd cannot be empty", "ConfirmPassword:Empty")
- .If(m => m.Password != "", validator =>
- validator.ForStringProperty(m => m.Password, passwordValidator =>
- passwordValidator.Matches("(?=.*?[0-9])(?=.*?[a-z])(?=.*?[A-Z])", "Password strength is not valid", "Password:InvalidStrength")
- .Required((m, pwd) => pwd.length > 3, "Password length should be greater than 3", "Password:LengthInvalid")
- .Required((m, pwd) => pwd == m.ConfirmPassword, "Password and Confirm Password are not the same", "Password:ConfirmNotSame")
- .ToResult()
- )
- .ToResult())
- .For(m => m.AgeGroup, ageGroupValidator => ageGroupValidator
- .Required((m, ageGroup) => ageGroup != AgeGroupEnum.None, "Select Age Group", "AgeGroup.Empty")
- .Required((m, ageGroup) => ageGroup == AgeGroupEnum.Minor ? m.IsParentalSupervisionProvided : true, "Check Parental Supervision", "IsParentalSupervisionProvided:Required")
- .ToResult())
- .ToResult();
- };
- }
In the Component,
- the injected Validation Service is invoked to perform the validation.
- this can be Sync or Async validation.
- the components Login, Register inherit from ComponentBase.
Base component class
- the form-related validation methods are in the base class.
- the Validation Service is injected into the base class.
- wrappers like below allow the derived components to leverage these methods.
- async validateFormAsync(service: (validationService: ValidationService) => Promise < ValidationResult > ): Promise < boolean > {
- this.validationResult = await service(this.validationService);this.validationResult.IsValid ? alert("Congrats! Validation passed.") : this.showValidationTooltips();
- return this.validationResult.IsValid;
- }
Login component
- async login() {
- var isValidModel = await this.validateFormAsync(validationService => validationService.validateUserAsync(this.loginUser));
- if (isValidModel) {}
- }
Register component
- async register() {
- var isValidModel = await this.validateFormAsync(validationService => validationService.validateRegisterUserAsync(this.registerUser));
- if (isValidModel) {}
- }
- Html markup of an input element.
- Using Tooltip to display the errors.
- < div class = "form-group" > < label class = "col-md-4" > Id < /label><ng-template#tipContent><ul class="tooltip-inner"><li*ngFor="let error of validationResult?.IdentifierStartsWith('Id')">{{error.Message}}</li > < /ul></ng - template > < input
- type = "text"
- id = "Id"
- class = "form-control"
- name = "Id" [(ngModel)] = "loginUser.Id"
- (ngModelChange) = "!validateMe('Id') ? t.open() : t.close()" [ngbTooltip] = "tipContent"#
- t = "ngbTooltip"
- placeholder = "Id" / > < /div>
Summary of above code snippets
- There is a Validation Service in the Angular 6 CLI app.
- All business rules around model validation are centralized in this service.
- There are 2 models for the components Login and Register. These models are User and register user.
- The framework interface IValidator<T> interface is used to lay the validation rules for these models.
- The Validation Service creates 2 sync and 2 async methods to validate these models.
- The sync methods are validated user and validateRegisterUser. The async ones are validateUserAsync and validateRegisterUserAsync.
- In the sync methods, the framework method Validate is used.
- In the async methods, the framework method ValidateAsync is used.
- This service is injected into the components.
- The methods of the service are used for model validation.
- The IdentifierStartsWith API the framework provides is used to display the validation errors.
- The validation errors are shown in angular bootstrap Tooltips. But, you can show your errors anyhow you like.
Validation Service unit test
You can unit test the business rules by unit testing the validation service.
- it('User should have 2 validation errors - Async', async () => {
- var model = new User("", "");
- validationService = TestBed.get(ValidationService);
- var validationResult = await validationService.validateUserAsync(model);
- expect(validationResult.IsValid).toBeFalsy();
- expect(validationResult.Errors.length == 2).toBeTruthy();
- expect(validationResult.Errors[0].Value == "").toBeTruthy();
- expect(validationResult.Errors[0].Identifier == "Id").toBeTruthy();
- expect(validationResult.Errors[0].Message == "Id cannot be empty").toBeTruthy();
- expect(validationResult.Errors[1].Value == "").toBeTruthy();
- expect(validationResult.Errors[1].Identifier == "Pwd").toBeTruthy();
- expect(validationResult.Errors[1].Message == "Pwd cannot be empty").toBeTruthy();
- });
You can download/clone the source code from GitHub,
By studying the code,
- you can learn to build generic frameworks in TypeScript.
- you can learn how to build Angular 6 CLI apps and use the framework.