21 September 2014

logo

I would like to demonstrate 4 ways how to define some validation rules. I use as an example the validation rules for the hobbies form.

  1. non-formal textual representation - typically requirements from customer
  2. declarative - JSON schema with validation keywords JSON Schema Validation
  3. declarative - raw JSON data annotated with meta data - using keywords from JQuery validation plugin
  4. imperative - validation API

Next i would like to show how all these representation are implemented in business rules engine.

You can play with these examples here:

1. Non-formal textual representation:

  • person
    • first name + last name are required and both have maximal 15 characters
    • email is required, maximal length is 100 characters and it is a valid Internet email address as defined by RFC 5322, section 3.4.1
  • hobbies
    • each person must give at least 2 hobbies, maximum hobbies is 4
    • each hobby must have name name that is maximal 100 characters long
    • each hobby can have optional information about
      • How often do you participate in this hobby
      • Is a paid hobby
      • Would recommend this hobby to a friend

2. JSON schema validation

JSON schema with validation keywords JSON Schema Validation

angular.module('myApp').factory('businessRules',function(){
    var json = {
         Person: {
             type: "object",
             properties: {
                 FirstName: { type: "string", title: "First name", required: true, maxLength: 15 },
                 LastName: { type: "string", title: "Last name", required: true, maxLength: 15 },
                 Contact: {
                     type: "object",
                     properties: {
                         Email: {
                             type: "string", title: "Email",
                             required: true,
                             maxLength: 100,
                             email: true
                         }
                     }
                 }
             }
         },
         Hobbies: {
             type: "array",
             items: {
                 type: "object",
                 properties: {
                     HobbyName: { type: "string", title: "HobbyName", required: true, maxLength: 100 },
                     Frequency: { type: "string", title: "Frequency", enum: ["Daily", "Weekly", "Monthly"] },
                     Paid: { type: "boolean", title: "Paid" },
                     Recommedation: { type: "boolean", title: "Recommedation" }
                 }
             },
             maxItems: 4,
             minItems: 2
         }
     };
     return new FormSchema.JsonSchemaRuleFactory(json).CreateRule("Main");
 })

3. JSON data annotated with meta data

raw JSON data annotated with meta data - using keywords from JQuery validation plugin

angular.module('myApp').factory('businessRules',function(){
   var json = {
        
        Person: {
            FirstName: {
                rules: {required: true, maxlength: 15}
            },
            LastName: {
                rules: { required: true, maxlength: 15 }
            },
            Contact: {
                Email: {
                    rules: {
                        required: true,
                        maxlength: 100,
                        email: true
                    }
                }
            }
        },
        Hobbies: [
            {
                HobbyName: {
                    rules: { required: true, maxlength: 100 }
                },
                Frequency: {
                    rules: { enum: ["Daily", "Weekly", "Monthly"]
                    }
                }},
            { maxItems: 4, minItems: 2}
        ]
    };
    
    return new FormSchema.JQueryValidationRuleFactory(json).CreateRule("Main");
})

4. Validation API

validation API

angular.module('myApp').factory('businessRules',function(){
    var BusinessRules = (function () {
       
          function BusinessRules() {
              
              this.MainValidator = this.createMainValidator().CreateRule("Main");
          }
         
         
         BusinessRules.prototype.createMainValidator = function () {
              //create custom validator
              var validator = new Validation.AbstractValidator();
  
              validator.ValidatorFor("Person", this.createPersonValidator());
              validator.ValidatorFor("Hobbies", this.createItemValidator(), true);
  
              var hobbiesCountFce = function (args) {
                  args.HasError = false;
                  args.ErrorMessage = "";
  
                  if (this.Hobbies === undefined || this.Hobbies.length < 2) {
                      args.HasError = true;
                      args.ErrorMessage = "Come on, speak up. Tell us at least two things you enjoy doing";
                      args.TranslateArgs = { TranslateId: 'HobbiesCountMin' };
                      return;
                  }
                  if (this.Hobbies.length > 4) {
                      args.HasError = true;
                      args.ErrorMessage = "'Do not be greedy. Four hobbies are probably enough!'";
                      args.TranslateArgs = { TranslateId: 'HobbiesCountMax'};
                      return;
                  }
              };
  
              validator.Validation({ Name: "Hobbies", ValidationFce: hobbiesCountFce });
  
              return validator;
          };
          BusinessRules.prototype.createPersonValidator = function () {
              //create custom composite validator
              var personValidator = new Validation.AbstractValidator();
  
              //create validators
              var required = new Validators.RequiredValidator();
              var maxLength = new Validators.MaxLengthValidator(15);
  
              //assign validators to properties
              personValidator.RuleFor("FirstName", required);
              personValidator.RuleFor("FirstName", maxLength);
  
              personValidator.RuleFor("LastName", required);
              personValidator.RuleFor("LastName", maxLength);
  
              personValidator.ValidatorFor("Contact", this.createContactValidator());
  
              return personValidator;
          };
          BusinessRules.prototype.createContactValidator = function () {
              //create custom validator
              var validator = new Validation.AbstractValidator();
              validator.RuleFor("Email", new Validators.RequiredValidator());
              validator.RuleFor("Email", new Validators.MaxLengthValidator(100));
              validator.RuleFor("Email", new Validators.EmailValidator());
              return validator;
          };
          BusinessRules.prototype.createItemValidator = function () {
              //create custom validator
              var validator = new Validation.AbstractValidator();
              validator.RuleFor("HobbyName", new Validators.RequiredValidator());
              return validator;
          };
          return BusinessRules;
      })();
      return new BusinessRules().MainValidator;
  })

Optional for typescript you can define data structure to ensure strong-type support when coding.

/**
     * Data structure for hobbies data.
     */
    export interface IHobbiesData {

        /**
         * Person identification
         */
        Person?:Shared.IPerson

        /**
         *  The things you enjoy doing.
         */
        Hobbies?:Array<IHobby>;
    }
    
    
    /**
     * The things you enjoy doing.
     */
    export interface IHobby
    {
        /**
         * Description of your hobby.
         */
        HobbyName?:string;

        /**
         * How often do you participate in this hobby.
         */
        Frequency?:HobbyFrequency;

        /**
         * Return true if this is a paid hobby, otherwise false.
         */
        Paid?:boolean;


        /**
         * Return true if you would recommend this hobby to a friend, otherwise false.
         */
        Recommedation?:boolean;

    }
    /**
     * How often do you participate in this hobby.
     */
    export enum HobbyFrequency {
        Daily, Weekly, Monthly

    }

Going deep

To add declarative support in business rules engine i use validation API. I share common validators for basic build-in constrains.

I write one factory for each syntax

/**
     * It represents the JSON schema factory for creating validation rules based on JSON form schema.
     * It uses constraints keywords from JSON Schema Validation specification.
     */
    export class JsonSchemaRuleFactory implements IValidationRuleFactory{

        /**
         * Default constructor
         * @param jsonSchema JSON schema for business rules.
         */
        constructor(private jsonSchema:any){
        }

        /**
         * Return concrete validation rule structured according to JSON schema.
         * @param name validation rule name
         * @returns {IAbstractValidationRule<any>} return validation rule
         */
        public CreateRule(name:string):Validation.IAbstractValidationRule<any>{
            return this.ParseAbstractRule(this.jsonSchema).CreateRule(name);
        }


        /**
         * Returns an concrete validation rules structured according to JSON schema.
         */
        private ParseAbstractRule(formSchema:any):Validation.IAbstractValidator<any> {

            var rule = new Validation.AbstractValidator<any>();

            for (var key in formSchema) {
                var item = formSchema[key];
                var type = item[Util.TYPE_KEY];
                if (type === "object") {
                    rule.ValidatorFor(key, this.ParseAbstractRule(item[Util.PROPERTIES_KEY]));
                }
                else if (type === "array") {
                    _.each(this.ParseValidationAttribute(item),function(validator){ rule.RuleFor(key,validator)});
                    rule.ValidatorFor(key, this.ParseAbstractRule(item[Util.ARRAY_KEY][Util.PROPERTIES_KEY]), true);
                }
                else {
                    _.each(this.ParseValidationAttribute(item),function(validator){ rule.RuleFor(key,validator)});
                }
            }
            return rule;
        }
        /**
         * Return list of property validators that corresponds json items for JSON form validation tags.
         * See keywords specifications -> http://json-schema.org/latest/json-schema-validation.html
         */
        private ParseValidationAttribute(item:any):Array<Validation.IPropertyValidator> {

            var validators = new Array<Validation.IPropertyValidator>();
            if (item === undefined) return validators;

            //5.  Validation keywords sorted by instance types
            //http://json-schema.org/latest/json-schema-validation.html

            //5.1. - Validation keywords for numeric instances (number and integer)
            // multipleOf validation
            validation = item["multipleOf"];
            if (validation !== undefined) {
                validators.push(new Validators.MultipleOfValidator(validation));
            }

            // maximum validation
            validation = item["maximum"];
            if (validation !== undefined) {
                validators.push(new Validators.MaxValidator(validation,item["exclusiveMaximum"]));
            }
            ...
            ...
            ...

            //7.3.2 email
            validation = item["email"];
            if (validation !== undefined) {
                validators.push(new Validators.EmailValidator())
            }

            //7.3.6 url
            validation = item["uri"];
            if (validation !== undefined) {
                validators.push(new Validators.UrlValidator())
            }

            //TODO: allOf,anyOf,oneOf,not,definitions

            return validators;
        }
    }
/**
     * It represents the JSON schema factory for creating validation rules based on raw JSON data annotated by validation rules.
     * It uses constraints keywords from JQuery validation plugin.
     */
    export class JQueryValidationRuleFactory implements IValidationRuleFactory  {

       static RULES_KEY = "rules";
       static DEFAULT_KEY = "default";

        /**
         * Default constructor
         * @param metaData -  raw JSON data annotated by validation rules
         */
        constructor(private metaData:any){
        }

        /**
         * Return an concrete validation rule by traversing raw JSON data annotated by validation rules.
         * @param name validation rule name
         * @returns {IAbstractValidationRule<any>} return validation rule
         */
        public CreateRule(name:string):Validation.IAbstractValidationRule<any>{
            return this.ParseAbstractRule(this.metaData).CreateRule(name);
        }

        /**
         * Returns an concrete validation rule structured according to JSON schema.
         */
        private ParseAbstractRule(metaData:any):Validation.IAbstractValidator<any> {

            var rule = new Validation.AbstractValidator<any>();

            for (var key in metaData) {
                var item = metaData[key];
                var rules = item[JQueryValidationRuleFactory.RULES_KEY];

                if ( _.isArray(item)) {
                    if (item[1] !== undefined) {
                        _.each(this.ParseValidationAttribute(item[1]), function (validator) {
                            rule.RuleFor(key, validator)
                        });
                    }
                    rule.ValidatorFor(key, this.ParseAbstractRule(item[0]), true);
                }
                else if (rules !== undefined) {
                    _.each(this.ParseValidationAttribute(rules),function(validator){ rule.RuleFor(key,validator)})
                }
                else {
                    rule.ValidatorFor(key, this.ParseAbstractRule(item));
                }
            }
            return rule;
        }

        /**
         * Return list of property validators that corresponds json items for JQuery validation pluging tags.
         * See specification - http://jqueryvalidation.org/documentation/
         */
        private ParseValidationAttribute(item:any):Array<Validation.IPropertyValidator> {

           var validators = new Array<Validation.IPropertyValidator>();
           if (item === undefined) return validators;

           var validation = item["required"];
           if (validation !== undefined && validation) {
               validators.push(new Validators.RequiredValidator());
           }

            var validation = item["remote"];
            if (validation !== undefined && validation) {
                validators.push(new Validators.RemoteValidator(validation));
            }

           // maxLength validation
           validation = item["maxlength"];
           if (validation !== undefined) {
               validators.push(new Validators.MaxLengthValidator(validation))
           }

            ...
            ...
            ...

            // uniqueItems validation
            validation = item["uniqueItems"];
            if (validation !== undefined) {
                validators.push( new Validators.UniqItemsValidator(validation))
            }

            // enum validation
            validation = item["enum"];
            if (validation !== undefined) {
                validators.push(new Validators.EnumValidator(validation))
            }


           return validators;
       }
    }

Summary

I am not a big fan of declarative validation. It works really nice for simple example, but for real-world examples problems start to encounter. There are a lot of small, tiny details to address that are not possible to express in declarative way. There is always many exception against typical scenario. This exceptional business rule scenario is typically not covered by declarative syntax that is restrictive by its nature. It is difficult to describe a lot of possibilities that can be found in business in real world. The reuse of declarative validation rule is also limited.



blog comments powered by Disqus