ts.validator.fluent - TypeScript Based Generic, Fluent Validation Framework

I have created a TypeScript based generic, fluent validation framework. Also, I have built a demo Angular 6 CLI app which uses the framework for client-side form validation.

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.
 

The framework

 
 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 
  1. class Employee {    
  2.  Name: string;    
  3.  Password: string;    
  4.  PreviousPasswords: string[];    
  5.  CreditCards: CreditCard[];    
  6.  Super: Super;    
  7.  Email: string;    
  8. }    
  9. class CreditCard {    
  10.  Number: string;    
  11.  Name: string;    
  12.  ExpiryDate: Date;    
  13. }    
  14. class Super {    
  15.  Name: string;    
  16.  Code: string;    
  17. }     

Validation rules

  1. /* Install npm package ts.validator.fluent and then import like below */    
  2. import {    
  3.  IValidator,    
  4.  Validator,    
  5.  ValidationResult    
  6. } from 'ts.validator.fluent/dist';    
  7. let validateSuperRules = (validator: IValidator < Super > ): ValidationResult => {    
  8.  return validator    
  9.   .NotNull(m => m.Name, "Should not be null""Super.Name.Null")    
  10.   .NotNull(m => m.Code, "Should not be null""Super.Code.Null")    
  11.   .If(m => m.Name != null && m.Code != null, validator => validator    
  12.    .NotEmpty(m => m.Name, "Should not be empty""Super.Name.Empty")    
  13.    .Matches(m => m.Code, "^[a-zA-Z]{2}\\d{4}$""Should not be invalid""Super.Code.Invalid")    
  14.    .ToResult())    
  15.   .ToResult();    
  16. };    
  17. let validateCreditCardRules = (validator: IValidator < CreditCard > ): ValidationResult => {    
  18.  return validator    
  19.   .NotNull(m => m.Name, "Should not be null""CreditCard.Name.Null")    
  20.   .NotNull(m => m.Number, "Should not be null""CreditCard.Number.Null")    
  21.   .NotNull(m => m.ExpiryDate, "Should not be null""CreditCard.ExpiryDate.Null")    
  22.   .If(m => m.Name != null && m.ExpiryDate != null, validator => validator    
  23.    .NotEmpty(m => m.Name, "Should not be empty""CreditCard.Name.Empty")    
  24.    .IsCreditCard(m => m.Number, "Should not be invalid""CreditCard.Number.Invalid")    
  25.    .IsDateOnOrAfter(m => m.ExpiryDate, new Date(), "Should be on or after today's date""CreditCard.ExpiryDate.Invalid")    
  26.    .ToResult())    
  27.   .ToResult();    
  28. };     
  1. let validateEmployeeRules = (validator: IValidator < Employee > ): ValidationResult => {    
  2.  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();  
  3. };    

Populate models

  1. let model = new Employee();    
  2. model.Name = "John Doe";    
  3. model.Password = "sD4A3";    
  4. model.PreviousPasswords = new Array < string > ()    
  5. model.PreviousPasswords.push("sD4A");    
  6. model.PreviousPasswords.push("sD4A1");    
  7. model.PreviousPasswords.push("sD4A2");    
  8. var expiryDate = new Date();    
  9. model.CreditCards = new Array < CreditCard > ();    
  10. var masterCard = new CreditCard();    
  11. masterCard.Number = "5105105105105100";    
  12. masterCard.Name = "John Doe"    
  13. masterCard.ExpiryDate = expiryDate;    
  14. var amexCard = new CreditCard();    
  15. amexCard.Number = "371449635398431";    
  16. amexCard.Name = "John Doe"    
  17. amexCard.ExpiryDate = expiryDate;    
  18. model.CreditCards.push(masterCard);    
  19. model.CreditCards.push(amexCard);    
  20. model.Super = new Super();    
  21. model.Super.Name = "XYZ Super Fund";    
  22. model.Super.Code = "XY1234";    
  23. model.Email = "john.doe@xyx.com";     

Synchronous validation

  1. let validationResult = new Validator(model).Validate(validateEmployeeRules);  

Asynchronous validation 

  1. let validationResult = await new Validator(model).ValidateAsync(validateEmployeeRules);  

Validation result 

  1. let isValid = validationResult.IsValid;  
  2. let allErrors = validationResult.Errors;  
  3. let employeeNameError = validationResult.Identifier("Employee.Name.Empty");  
  4. 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 
  1. class Accountant extends Employee {  
  2.  Code: string;  
  3. }  

Validation rules 

  1. let validateAccountantRules = (validator: IValidator < Accountant > ): ValidationResult => {  
  2.  return validator.NotEmpty(m => m.Code, "Should not be empty").ToResult();  
  3. };   

Populate models 

  1. let accountant = new Accountant();    
  2. accountant.Code = "ACC001";    
  3. accountant.Name = "John Doe";    
  4. accountant.Password = "sD4A3";    
  5. accountant.PreviousPasswords = new Array < string > ()    
  6. accountant.PreviousPasswords.push("sD4A");    
  7. accountant.PreviousPasswords.push("sD4A1");    
  8. accountant.PreviousPasswords.push("sD4A2");    
  9. var expiryDate = new Date();    
  10. accountant.CreditCards = new Array < CreditCard > ();    
  11. var masterCard = new CreditCard();    
  12. masterCard.Number = "5105105105105100";    
  13. masterCard.Name = "John Doe"    
  14. masterCard.ExpiryDate = expiryDate;    
  15. var amexCard = new CreditCard();    
  16. amexCard.Number = "371449635398431";    
  17. amexCard.Name = "John Doe";    
  18. amexCard.ExpiryDate = expiryDate;    
  19. accountant.CreditCards.push(masterCard);    
  20. accountant.CreditCards.push(amexCard);    
  21. accountant.Super = new Super();    
  22. accountant.Super.Name = "XYZ Super Fund";    
  23. accountant.Super.Code = "XY1234";    
  24. accountant.Email = "john.doe@xyx.com";   

Synchronous validation 

  1. let validationResult = new Validator(accountant).ValidateBase(validateEmployeeRules).Validate(validateAccountantRules);   

Asynchronous validation 

  1. 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
  1. import {    
  2.  Injectable    
  3. } from '@angular/core';    
  4. import {    
  5.  IValidator,    
  6.  Validator,    
  7.  ValidationResult    
  8. } from 'ts.validator.fluent/dist';    
  9. import {    
  10.  IValidationService    
  11. } from './ivalidation-service';    
  12. import {    
  13.  User,    
  14.  RegisterUser,    
  15.  AgeGroupEnum    
  16. } from '../models/models.component';    
  17. @Injectable()    
  18. export class ValidationService implements IValidationService {    
  19.  validateUser(model: User): ValidationResult {    
  20.   return new Validator(model).Validate(this.validateUserRules);    
  21.  }    
  22.  async validateUserAsync(model: User): Promise < ValidationResult > {    
  23.   return await new Validator(model).ValidateAsync(this.validateUserRules);    
  24.  }    
  25.  validateRegisterUser(model: RegisterUser): ValidationResult {    
  26.   return new Validator(model).Validate(this.validateRegisterUserRules);    
  27.  }    
  28.  async validateRegisterUserAsync(model: RegisterUser): Promise < ValidationResult > {    
  29.   return await new Validator(model).ValidateAsync(this.validateRegisterUserRules);    
  30.  }    
  31.  validateUserRules = (validator: IValidator < User > ): ValidationResult => {    
  32.   return validator    
  33.    .NotEmpty(m => m.Id, "Id cannot be empty""Id")    
  34.    .NotEmpty(m => m.Pwd, "Pwd cannot be empty""Pwd")    
  35.    .ToResult();    
  36.  };    
  37.  validateRegisterUserRules = (validator: IValidator < RegisterUser > ): ValidationResult => {    
  38.   return validator    
  39.    .NotEmpty(m => m.Name, "Name cannot be empty""Name:Empty")    
  40.    .NotEmpty(m => m.CreditCardNo, "Credit Card Number cannot be empty""CreditCardNo:Empty")    
  41.    .If(m => m.CreditCardNo != "", validator =>    
  42.     validator.ForStringProperty(m => m.CreditCardNo, creditCardValidator =>    
  43.      creditCardValidator.Length(13, 19, "Credit Card Number length is invalid""CreditCardNo:LengthInvalid")    
  44.      .IsCreditCard("Credit Card Number is invalid""CreditCardNo:Invalid")    
  45.      .ToResult()    
  46.     )    
  47.     .ToResult())    
  48.    .NotEmpty(m => m.Id, "Id cannot be empty""Id:Empty")    
  49.    .NotEmpty(m => m.Email, "Email cannot be empty""Email:Empty")    
  50.    .If(m => m.Email != "", validator =>    
  51.     validator.Email(m => m.Email, "Email is invalid""Email:Invalid")    
  52.     .ToResult())    
  53.    .NotEmpty(m => m.Password, "Pwd cannot be empty""Password:Empty")    
  54.    .NotEmpty(m => m.ConfirmPassword, "Confirm Pwd cannot be empty""ConfirmPassword:Empty")    
  55.    .If(m => m.Password != "", validator =>    
  56.     validator.ForStringProperty(m => m.Password, passwordValidator =>    
  57.      passwordValidator.Matches("(?=.*?[0-9])(?=.*?[a-z])(?=.*?[A-Z])""Password strength is not valid""Password:InvalidStrength")    
  58.      .Required((m, pwd) => pwd.length > 3, "Password length should be greater than 3""Password:LengthInvalid")    
  59.      .Required((m, pwd) => pwd == m.ConfirmPassword, "Password and Confirm Password are not the same""Password:ConfirmNotSame")    
  60.      .ToResult()    
  61.     )    
  62.     .ToResult())    
  63.    .For(m => m.AgeGroup, ageGroupValidator => ageGroupValidator    
  64.     .Required((m, ageGroup) => ageGroup != AgeGroupEnum.None, "Select Age Group""AgeGroup.Empty")    
  65.     .Required((m, ageGroup) => ageGroup == AgeGroupEnum.Minor ? m.IsParentalSupervisionProvided : true"Check Parental Supervision""IsParentalSupervisionProvided:Required")   
  66.     .ToResult())    
  67.    .ToResult();    
  68.  };    
  69. }     
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. 
    1. async validateFormAsync(service: (validationService: ValidationService) => Promise < ValidationResult > ): Promise < boolean > {    
    2.  this.validationResult = await service(this.validationService);this.validationResult.IsValid ? alert("Congrats! Validation passed.") : this.showValidationTooltips();    
    3.  return this.validationResult.IsValid;    
    4. }     

Login component

  1. async login() {  
  2.  var isValidModel = await this.validateFormAsync(validationService => validationService.validateUserAsync(this.loginUser));  
  3.  if (isValidModel) {}  
  4. }   

Register component

  1. async register() {  
  2.  var isValidModel = await this.validateFormAsync(validationService => validationService.validateRegisterUserAsync(this.registerUser));  
  3.  if (isValidModel) {}  
  4. }   
  • Html markup of an input element.
  • Using Tooltip to display the errors.
    1. < 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    
    2. type = "text"    
    3. id = "Id"    
    4. class = "form-control"    
    5. name = "Id" [(ngModel)] = "loginUser.Id"    
    6.  (ngModelChange) = "!validateMe('Id') ? t.open() : t.close()" [ngbTooltip] = "tipContent"#    
    7. t = "ngbTooltip"    
    8. 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.
  1. it('User should have 2 validation errors - Async', async () => {    
  2.  var model = new User("""");    
  3.  validationService = TestBed.get(ValidationService);    
  4.  var validationResult = await validationService.validateUserAsync(model);    
  5.  expect(validationResult.IsValid).toBeFalsy();    
  6.  expect(validationResult.Errors.length == 2).toBeTruthy();    
  7.  expect(validationResult.Errors[0].Value == "").toBeTruthy();    
  8.  expect(validationResult.Errors[0].Identifier == "Id").toBeTruthy();    
  9.  expect(validationResult.Errors[0].Message == "Id cannot be empty").toBeTruthy();    
  10.  expect(validationResult.Errors[1].Value == "").toBeTruthy();    
  11.  expect(validationResult.Errors[1].Identifier == "Pwd").toBeTruthy();    
  12.  expect(validationResult.Errors[1].Message == "Pwd cannot be empty").toBeTruthy();    
  13. });    
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.