Yesterday someone had an interesting use case for the cbvalidation library. I presented at ITB2020 about cbvalidation, and I’ve contributed some code so I thought it had no secrets anymore. But when trying to solve this case I discovered cbvalidation still had some hidden lines for me. When discussing this validation problem we tried to solve it with UDF validators, but -spoiler alert-finally we agreed it was not powerful enough. So time to build a CustomValidator, which is a lot easier than you might think.

The question

The question was if it was possible to use cbvalidation to validate a UserID and a ChildId field. Both had some model logic where is was easy to check if a UserId or ChildId was valid, let’s say myModel.isValidUserID(someValue) and myModel.isValidChildId(someValue). So this all looks easy, but now for the hard part: additional requirement was that there should be at least a UserID or ChildId present, and to make stuff more complex: this was exclusive, so if there is a UserId, the ChildID should not be present, and if there is a ChildId there should not be a UserId.

It is clear that none of the standard validators could perform this trick, especially because there is some relation between these two UserId and ChildId fields. So I thought we could solve this with UDF validators. A UDF validator is quite flexible and easy to understand and looks like this

var myConstraints={
  "UserId": { 
    udf: ( value, target ) => {
      return someBoolean;
    },
    udfMessage: "This is not a valid UserId"
}

What’s the target?

So a UDF has two parameters, a value and a target. If we were validating our input from the reques scope e.g

prc.validationResult=validate(target=rc, constraints=myConstraints);

I would expect this target would be what I entered in my validate function. A validate function can accept a model or a struct as target. Most of the time I am validating models and in that case my target in the UDF is not so surprisingly a model. But in this special case we were validating values from the rc, so a struct. To my surprise the target in the udf validator is still a model, and not a struct. Actually cbvalidation converts the target struct on the fly to a GenericObject which accepts a struct on initialization. It has some magic onMissingMethod to provide your values as getters, and there is a getMemento method. So if you want to get the value of one of your structKeys you just call target.getMyKey(). If the key does not exist it does not error but returns null. If you need a struct of all values you just call target.getMemento().

The UDF solution, but what’s the message?

UDF validators are quite flexible. You have the target in your parameters, so you can check values of other submitted parameters in the same validation action. So what should we check for UserId ?

  • if the UserId is null, the validator should return true only if the ChildId is not null. If both Id types are null the validator should return false.
  • if the UserId is not null, the ChildId has to be null. If not the validator should return false
  • finally, if all previous conditions are met we can lookup from some external model component if the UserId is a valid one.

If the UserId is not valid, a message should be returned. This can be done by using the UdfMessage property. For ChildId there is simular logic. So the code would look like this.

var myConstraints={
  "UserId": { 
    udf: ( value, target ) => {
      // value can be null, but only if childId is not null
      if ( isNull(value) ) return !isNull( target.getChildId() );
      // if value is present, ChildId should not be there.
      if ( !isNull( target.getChildId() ) ) return false
      // now check validity of the UserId
      return myModel.isValidUserId(value);
    },
    udfMessage: "This is not a valid UserId"
  },
  "ChildId": { 
    udf: ( value, target ) => {
      // value can be null, but only if UserId is not null
      if ( isNull(value) ) return !isNull( target.getUserId() );
      // if value is present, ChildId should not be there.
      if ( !isNull( target.getUserId() ) ) return false
      // now check validity of the ChildId
      return myModel.isValidUserId(value);
    },
    udfMessage: "This is not a valid ChildId"
  },
}
prc.validationResult = validate(target=rc, constraints=myConstraints);
event.setView("Demo/myValidationErrors")

Mission accomplished or…

Ok, so we now have two validators for both fields. They accept correct values and refuse them if any of these two does not follow the rules. So mission accomplished or not? Unfortunately not. Both fields can only give you one errormessage, but in these UDF validators we are checking three different things. It makes no sense to report “This is not a valid UserId” if there is also a ChildId present. And both fields deliver the same kind of wrong message if there are no values at all for both ChildId and UserId. So actually we need a way to change the message based on one of the three failed conditions. This is not possible with UDF validation, and there’s also no possibility to split your UDF in three different ones with their own messages, since a field can only accept ONE udf validator.

Custom validator

But there’s something more flexible and powerfull: a custom validator.
Advantages of a custom validator:

  • a field can have MANY custom validators
  • error messages can be customized based on validation conditions or for example language
  • a custom validator has the full validation context, including previous validation results, so you could decide to not show errors if they are already there in related fields
  • a custom validator can accept extra validationData, e.g
    myValidator = { param1="foo", param2="bar"} which make usage of this customValidator more flexible

A custom validator is a component which has a validate function with the following parameters:

  • validationResult so you can add errormessages to your result
  • target which is an object with all fields
  • field which is the name of the field you are validating
  • targetValue which is the exact value you are validating (make sure you check for nulls)
  • validationData which is all the stuff which you add after myValidator = …

So let’s rewrite our logic as a Customvalidator. I don’t want to write two differnt ones for almost the same logic, so here it is:

component accessors="true" singleton {
  //DI
  property name="myModel" inject;

  property name="name";

  CustomUserChildIdValidator function init(){
    variables.name = "CustomUserChildIdValidator";
    return this;
  }

  boolean function validate(
    required any validationResult,
    required any target,
    required string field,
    any targetValue,
    any validationData
  ){
    var result = true;
    // value can be null for UserId OR ChildId, not both
    if ( isNull(arguments.targetValue) ) {
      result = ( CompareNoCase(arguments.field,"UserId") == 0 ) 
        ? !isNull( target.getChildId() ) 
          : !isNull( target.getUserId() );
      var ErrorMessage = "Error validating #arguments.field#: Please enter a value for UserId OR ChildId";
    }
    // if Validation didn't fail yet ( result=true ) a non-NULL target value.
    // so make sure the other field is NOT present
    if ( result && !isNull( target.getUserId() ) && !isNull( target.getChildId()) ){
      var ErrorMessage = "Error validating #arguments.field#: Please enter UserId OR ChildId, you can't provide both";
      result = false;
    }
    // only continue validation when no errors, so result=true. 
    if ( result && !isNull(arguments.targetValue) ){
      result = ( CompareNoCase(arguments.field,"UserId") == 0 ) 
        ? myModel.isValidUserId(arguments.targetValue) 
          : myModel.isValidChildId(arguments.targetValue) ;
      var ErrorMessage = "#arguments.field# with value #arguments.targetValue# is not valid";
    }
    if ( result ) return true;

    var args = {
      message        : errorMessage,
      field          : arguments.field,
      validationType : getName(),
      rejectedValue  : ( isSimpleValue( arguments.targetValue ) ? 
                           arguments.targetValue : "" ),
      validationData : arguments.validationData
    };
    var error = validationResult.newError( argumentCollection = args )
      .setErrorMetadata( 
        { CustomUserChildIdValidator : arguments.validationData } 
      );
    validationResult.addError( error );
    return false;
  }

  /**
   * Get the name of the validator
   */
  string function getName(){
    return variables.name;
  }
}

A CustomValidator is quite simple. It has an init method which returns the validator, a getName function to return the Name of the validator and a validate function. You could have a look at some of the standard validators in the cbvalidate module, and the validate function always looks the same. Do some checking, save the result. If everything is OK return OK and else build some error, which you append to the validation result.

In this code, we check three different conditions and create an error message for each condition and save the result of a check. The next check will only be executed if there is no error yet, so we always have the correct errorMessage. In your errormessage you can add the name and value of the field you were validating to create a very specific errorMessage. And we non-native english speakers could easily add translation to our messages.

When you save your customValidator make sure you save it in some place accessible by Wirebox. In that case you can easily load your customValidator just by specifying the name in your constraints.

In my Demo handler it looks like this:

function submitResultCustomValidated( event, rc, prc){
  var myConstraints={
    "UserId": { 
      CustomUserChildIdValidator: {}
    },
    "ChildId": { 
      CustomUserChildIdValidator: {}
    }
  }
  prc.validationResult=validate(target=rc, constraints=myConstraints);
  event.setView("Demo/myValidationErrors")
}

As you can see in my CustomUserChildIdValidator there’s an empty struct behind it. If you want to make your validators even more flexible you can add extra parameters in this struct, for example like this where I add tableName and targetColumn to my Validator:

this.constraints = {
  "name" : {
    required         : true,
    QUniqueValidator : {
      tableName    : "users",
      targetColumn : "name"
    },
  },
  ....
}

In a future post I will explain how to write this Unique validator.