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.

I have built a TypeScript based generic, fluent validation framework. Also an Angular 6 CLI app to demo 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 upper case.
 IsMixedcase  Used to test if a string is mixed case.
 IsNumeric  Used to test if a string is a numeric.
 IsAlpha  Used to test if a string is a alpha.
 IsAlphaNumeric  Used to test if a string is a alpha numeric.
 IsGuid  Used to test if a string is a guid/uuid.
 IsBase64  Used to test if a string is base64.
 IsUrl  Used to test if a string is an url.
 IsCountryCode  Used to test if a string is a 2 letter country code.
 Contains Used to test if a sub string 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 the 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.   
  10. class CreditCard {  
  11.     Number: string;  
  12.     Name: string;  
  13.     ExpiryDate: Date;  
  14. }  
  15.   
  16. class Super {  
  17.     Name: string;  
  18.     Code: string;  
  19. }  
Validation rules
  1. /* Install npm package ts.validator.fluent and then import like below */  
  2. import { IValidator, Validator, ValidationResult } from 'ts.validator.fluent/dist';   
  1. let validateSuperRules =  (validator: IValidator<Super>) : ValidationResult => {  
  2.   return validator  
  3.            .NotNull(m => m.Name, "Should not be null""Super.Name.Null")  
  4.            .NotNull(m => m.Code, "Should not be null""Super.Code.Null")  
  5.            .If(m => m.Name != null && m.Code != null, validator => validator   
  6.                                                          .NotEmpty(m => m.Name, "Should not be empty""Super.Name.Empty")  
  7.                                                          .Matches(m => m.Code, "^[a-zA-Z]{2}\\d{4}$""Should not be invalid""Super.Code.Invalid")  
  8.                                                      .ToResult())  
  9.         .ToResult();  
  10. };  
  1. let validateCreditCardRules =  (validator: IValidator<CreditCard>) : ValidationResult => {    
  2.  return validator  
  3.            .NotNull(m => m.Name, "Should not be null""CreditCard.Name.Null")  
  4.            .NotNull(m => m.Number, "Should not be null""CreditCard.Number.Null")  
  5.            .NotNull(m => m.ExpiryDate, "Should not be null""CreditCard.ExpiryDate.Null")  
  6.            .If(m => m.Name != null && m.ExpiryDate != null, validator => validator   
  7.                                                        .NotEmpty(m => m.Name, "Should not be empty""CreditCard.Name.Empty")  
  8.                                                        .IsCreditCard(m => m.Number, "Should not be invalid""CreditCard.Number.Invalid")  
  9.                                                        .IsDateOnOrAfter(m => m.ExpiryDate, new Date(), "Should be on or after today's date""CreditCard.ExpiryDate.Invalid")  
  10.                                                    .ToResult())  
  11.        .ToResult();  
  12. };  
  1. let validateEmployeeRules = (validator: IValidator<Employee>) : ValidationResult => {  
  2.    return validator                                
  3.          .NotEmpty(m => m.Name, "Should not be empty""Employee.Name.Empty")  
  4.          .NotNull(m => m.CreditCards, "Should not be null""CreditCard.Null")  
  5.          .NotNull(m => m.Super, "Should not be null""Super.Null")  
  6.          .NotEmpty(m => m.Email, "Should not be empty""Employee.Email.Empty")  
  7.          .If(m => m.Super != null, validator => validator.ForType(m => m.Super, validateSuperRules).ToResult())  
  8.          .If(m => m.Email != '', validator =>   
  9.                                              validator.Email(m => m.Email, "Should not be invalid""Employee.Email.Invalid")  
  10.                                  .ToResult())    
  11.          .Required(m => m.CreditCards, (m, creditCards) => creditCards.length > 0, "Must have atleast 1 credit card""Employee.CreditCards.Required")  
  12.          .If(m => m.CreditCards != null && m.CreditCards.length > 0,   
  13.                      validator => validator  
  14.                                    .ForEach(m => m.CreditCards, validateCreditCardRules)  
  15.                            .ToResult())  
  16.          .If(m => m.Password != '', validator => validator  
  17.                                    .ForStringProperty(m => m.Password, passwordValidator => passwordValidator  
  18.                                            .Matches("(?=.*?[0-9])(?=.*?[a-z])(?=.*?[A-Z])""Password strength is not valid""Employee.Password.Strength")  
  19.                                            .Required((m, pwd) => pwd.length > 3, "Password length should be greater than 3""Employee.Password.Length")  
  20.                                            .Required((m, pwd) => !m.PreviousPasswords.some(prevPwd => prevPwd == pwd), "Password is already used""Employee.Password.AlreadyUsed")  
  21.                                        .ToResult())  
  22.                                .ToResult())                                                                                                                      
  23.    .ToResult();  
  24. };  
Populate models
  1. let model = new Employee();  
  2. model.Name = "John Doe";  
  3.   
  4. model.Password = "sD4A3";  
  5. model.PreviousPasswords = new Array<string>()       
  6. model.PreviousPasswords.push("sD4A");  
  7. model.PreviousPasswords.push("sD4A1");  
  8. model.PreviousPasswords.push("sD4A2");  
  9.   
  10. var expiryDate = new Date();  
  11.   
  12. model.CreditCards = new Array<CreditCard>();  
  13. var masterCard = new CreditCard();  
  14. masterCard.Number = "5105105105105100";  
  15. masterCard.Name = "John Doe"  
  16. masterCard.ExpiryDate = expiryDate;  
  17. var amexCard = new CreditCard();  
  18. amexCard.Number = "371449635398431";  
  19. amexCard.Name = "John Doe"  
  20. amexCard.ExpiryDate = expiryDate;  
  21. model.CreditCards.push(masterCard);  
  22. model.CreditCards.push(amexCard);  
  23.   
  24. model.Super = new Super();  
  25. model.Super.Name = "XYZ Super Fund";  
  26. model.Super.Code = "XY1234";  
  27.   
  28. 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. //Check if the model is valid.  
  2. let isValid = validationResult.IsValid;  
  3.   
  4. //Get all errors.  
  5. let allErrors = validationResult.Errors;  
  6.   
  7. //Get error for a particular identifier  
  8. let employeeNameError = validationResult.Identifier("Employee.Name.Empty");  
  9.   
  10. //Get all errors which start with some identifier string.   
  11. //Below code will return Super.Code.Empty and Super.Code.Invalid errors  
  12. 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 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 the 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 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  
  3.             .NotEmpty(m => m.Code, "Should not be empty")  
  4.         .ToResult();  
  5. };  
Populate models 
  1. let accountant = new Accountant();  
  2. accountant.Code = "ACC001";  
  3.   
  4. //Employee data  
  5. accountant.Name = "John Doe";  
  6.   
  7. accountant.Password = "sD4A3";  
  8. accountant.PreviousPasswords = new Array<string>()       
  9. accountant.PreviousPasswords.push("sD4A");  
  10. accountant.PreviousPasswords.push("sD4A1");  
  11. accountant.PreviousPasswords.push("sD4A2");  
  12.   
  13. var expiryDate = new Date();  
  14.   
  15. accountant.CreditCards = new Array<CreditCard>();  
  16. var masterCard = new CreditCard();  
  17. masterCard.Number = "5105105105105100";  
  18. masterCard.Name = "John Doe"  
  19. masterCard.ExpiryDate = expiryDate;  
  20. var amexCard = new CreditCard();  
  21. amexCard.Number = "371449635398431";  
  22. amexCard.Name = "John Doe";  
  23. amexCard.ExpiryDate = expiryDate;  
  24. accountant.CreditCards.push(masterCard);  
  25. accountant.CreditCards.push(amexCard);  
  26.   
  27. accountant.Super = new Super();  
  28. accountant.Super.Name = "XYZ Super Fund";  
  29. accountant.Super.Code = "XY1234";  
  30.   
  31. accountant.Email = "john.doe@xyx.com";  
Synchronous validation 
  1. let validationResult = new Validator(accountant).ValidateBase(validateEmployeeRules)  
  2.                                                 .Validate(validateAccountantRules);   

Asynchronous validation 

  1. let validationResult = await new Validator(accountant).ValidateBaseAsync(validateEmployeeRules)  
  2.                                                       .ValidateAsync(validateAccountantRules);  

Summary of above code snippets

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

Register component

  1.  async register() {  
  2.    var isValidModel = await this.validateFormAsync(validationService => validationService.validateRegisterUserAsync(this.registerUser));   
  3.      
  4.    if (isValidModel)  
  5.    {  
  6.      //do processing  
  7.    }  
  8. }  
  • Html markup of an input element.
  • Using Tooltip to display the errors.
  1. <div class="form-group">  
  2.     <label class="col-md-4">Id</label>  
  3.     <ng-template #tipContent>    
  4.         <ul class="tooltip-inner">  
  5.             <li *ngFor="let error of validationResult?.IdentifierStartsWith('Id')">  
  6.                 {{error.Message}}  
  7.             </li>  
  8.         </ul>                                  
  9.     </ng-template>  
  10.     <input   
  11.             type="text"   
  12.             id="Id"   
  13.             class="form-control"   
  14.             name="Id"                       
  15.             [(ngModel)]="loginUser.Id"   
  16.             (ngModelChange)="!validateMe('Id') ? t.open() : t.close()"   
  17.             [ngbTooltip]="tipContent"     
  18.             #t="ngbTooltip"   
  19.             placeholder="Id" />                    
  20. </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 RegisterUser.
  • 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 validateUser 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 any how 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.     
  4.   //Get Validation Service  
  5.   validationService = TestBed.get(ValidationService);  
  6.   
  7.   var validationResult = await validationService.validateUserAsync(model);  
  8.     
  9.   expect(validationResult.IsValid).toBeFalsy();  
  10.   expect(validationResult.Errors.length == 2).toBeTruthy();  
  11.   
  12.   //Errors  
  13.   expect(validationResult.Errors[0].Value == "").toBeTruthy();  
  14.   expect(validationResult.Errors[0].Identifier == "Id").toBeTruthy();  
  15.   expect(validationResult.Errors[0].Message == "Id cannot be empty").toBeTruthy();  
  16.     
  17.   expect(validationResult.Errors[1].Value == "").toBeTruthy();  
  18.   expect(validationResult.Errors[1].Identifier == "Pwd").toBeTruthy();  
  19.   expect(validationResult.Errors[1].Message == "Pwd cannot be empty").toBeTruthy();  
  20. });   

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.