Hyrule, by Dan Vega, is pretty cool. Using some simple metadata in your ORM Entities or CFCs you can enable Hyrule validation. It is quite an elegant object data validator and I have switched to it on my newest project. If you are using ColdFusion 9 and rapidly developing applications with the new ORM Hybernate features then I highly recommend you take advantage of Dan’s generosity and adopt use of Hyrule.
When evaluating the code I discovered two minor bugs and sent the corrections to the author, Dan Vega (who has some sweet projects). The biggest issue was that the validation results where stored in the instance of Hyrule which made it unsafe to persist in a non request specific scope. Since I am a big ColdBox Framework fan, which makes caching objects so much fun, I just can’t allow a utility like like Hyrule to exist and not be a singleton. So, I made some changes and e-mailed them to Dan Vega two weeks ago.
Tonight, I made some additional changes to Hyrule. This time, I added some recursive crawling to the object being validated to support extended properties (the second bug I found). Hyrule can now validate the object passed into it and the properties set into the instance of the extended objects. I have yet to hear back from Dan Vega from my code suggestion a few weeks ago so I thought I’d post the code here for consumption and for safe keeping in case I suffer some SVN meltdown.
ValidatorResults.cfc
This is a new file which must be passed into the Hyrule Validate method.
/**
* @displayname Hyrule Validator Errors
* @hint I am the thread safe error component. I am intended to be created in a thread safe scope such as local or request
* @accessors true
* @output false
*/
component {
/**
* @hint The array which will contain errors found by Hyrule
*/
property array errors;
public function init(){
setErrors([]);
return this;
}
/**
* @hint A way to see if the validate method produced any errors.
*/
public boolean function hasErrors(){
return arrayLen( getErrors() ) > 0;
}
/**
* @hint adds an error to the errors array
*/
public void function addError(String property,String Message,String display){
var s = {property=arguments.property,message=arguments.message,display=arguments.display};
arrayAppend(getErrors(),s);
}
}
Validator.cfc
This is the primary method for Hyrule
/**
* @displayname Hyrule Validator
* @hint I am the main component for the Hyrule Validation Framework
* @accessors true
* @output false
*/
component {
/**
* @hint I am the name of the properties file that contains our default messages
*/
property string resourceBundle;
/**
* @hint I am the ValidatorMessage Component - I handle the loading and retrieval of messages.
*/
property ValidatorMessage validatorMessage;
public Validator function init(String rb="DefaultValidatorMessages",Boolean isAbsolutePath=false){
this.setResourceBundle(arguments.rb);
this.setValidatorMessage(new ValidatorMessage(arguments.rb));
return this;
}
public void function validate(dto, validatorResults){
var props = getPropertiesRecusivly( arguments.dto );
/* LOCAL SCOPE */
var i = 0;
var key = '';
// for each property in the array
for(i=1; i <= arrayLen(props); ++i) {
// the current property struct
var prop = props[i];
// the name of the current property
var name = prop["name"];
// the value of the property
//var val = arguments.dto.getPropertyValue(name);
var val = evaluate("arguments.dto." & "get#name#()");
// the display name of the property
var display = getDisplay(prop);
if(structKeyExists(prop,"message")){
var perPropertyMessage = prop.message;
}
// loop over each key/pair value in the property
for(key in prop){
// based on the type we can grab our default message template from
// our properties file that was loaded
var message = isDefined("perPropertyMessage") ? perPropertyMessage : getValidatorMessage().getMessageByType(key);
switch(key){
case "NOTNULL" : {
var notNullValidator = new validator.NotNullValidator();
if( isNull(val) ){
// if notnull != true then we are just making sure its not null
validatorResults.addError(name,notNullValidator.getMessage(prop,message),display);
break;
}
break;
}
case "NOTEMPTY" : {
var notEmptyValidator = new validator.NotEmptyValidator();
if(!notEmptyValidator.isValid(prop,val)){
validatorResults.addError(name,notEmptyValidator.getMessage(prop,message),display);
break;
}
break;
}
case "MIN" : {
var minValidator = new validator.MinValidator();
if( !minValidator.isValid(prop,val) ){
validatorResults.addError(name,minValidator.getMessage(prop,message),display);
break;
}
break;
}
case "MAX" : {
var maxValidator = new validator.MaxValidator();
if( !maxValidator.isValid(prop,val) ){
validatorResults.addError(name,maxValidator.getMessage(prop,message),display);
break;
}
break;
}
case "RANGE" : {
var rangeValidator = new validator.rangeValidator();
if( !rangeValidator.isValid(prop,val) ){
validatorResults.addError(name,rangeValidator.getMessage(prop,message),display);
break;
}
break;
}
case "SIZE" : {
var sizeValidator = new validator.sizeValidator();
if( !sizeValidator.isValid(prop,val) ){
validatorResults.addError(name,sizeValidator.getMessage(prop,message),display);
break;
}
break;
}
case "INLIST" : {
var inListValidator = new validator.InListValidator();
if( !inListValidator.isValid(prop,val) ){
validatorResults.addError(name,inListValidator.getMessage(prop,message),display);
break;
}
break;
}
case "NOTINLIST" : {
var notInListValidator = new validator.NotInListValidator();
if( !notInListValidator.isValid(prop,val) ){
validatorResults.addError(name,notInListValidator.getMessage(prop,message),display);
break;
}
break;
}
case "ISMATCH" : {
var compareto = prop.isMatch;
//var comparetoValue = arguments.dto.getPropertyValue(prop.isMatch);
var comparetoValue = evaluate("arguments.dto." & "get#prop.isMatch#()");
prop.comparetoValue = comparetoValue;
var isMatchValidator = new validator.isMatchValidator();
if( !isMatchValidator.isValid(prop,val) ){
validatorResults.addError(name,isMatchValidator.getMessage(prop,message),display);
break;
}
break;
}
case "PAST" : {
var pastValidator = new validator.PastValidator();
if( !pastValidator.isValid(prop,val) ){
validatorResults.addError(name,pastValidator.getMessage(prop,message),display);
break;
}
break;
}
case "FUTURE" : {
var futureValidator = new validator.FutureValidator();
if( !futureValidator.isValid(prop,val) ){
validatorResults.addError(name,futureValidator.getMessage(prop,message),display);
break;
}
break;
}
case "PATTERN" : {
var patternValidator = new validator.PatternValidator();
if( !patternValidator.isValid(prop,val) ){
validatorResults.addError(name,patternValidator.getMessage(prop,message),display);
break;
}
break;
}
case "ASSERTTRUE" : {
var assertTrueValidator = new validator.AssertTrueValidator();
if( !assertTrueValidator.isValid(prop,val) ){
validatorResults.addError(name,assertTrueValidator.getMessage(prop,message),display);
break;
}
break;
}
case "ASSERTFALSE" : {
var assertFalseValidator = new validator.AssertFalseValidator();
if( !assertFalseValidator.isValid(prop,val) ){
validatorResults.addError(name,assertFalseValidator.getMessage(prop,message),display);
break;
}
break;
}
case "UPPERCASE" : {
var uppercaseValidator = new validator.UpperCaseValidator();
if( !uppercaseValidator.isValid(prop,val) ){
validatorResults.addError(name,uppercaseValidator.getMessage(prop,message),display);
break;
}
break;
}
case "LOWERCASE" : {
var lowercaseValidator = new validator.LowerCaseValidator();
if( !lowercaseValidator.isValid(prop,val) ){
validatorResults.addError(name,lowercaseValidator.getMessage(prop,message),display);
break;
}
break;
}
case "PASSWORD" : {
var passwordValidator = new validator.passwordValidator();
if( !passwordValidator.isValid(prop,val) ){
validatorResults.addError(name,passwordValidator.getMessage(prop,message),display);
break;
}
break;
}
// the following all validate using isValid but are broken into custom validators
// in case you want to overwrite or extend thme to provide customer functionality
case "EMAIL" : {
var emailValidator = new validator.emailValidator();
if( !emailValidator.isValid(prop,val) ){
validatorResults.addError(name,emailValidator.getMessage(prop,message),display);
break;
}
break;
}
case "CREDITCARD" : {
var creditCardNumberValidator = new validator.CreditCardNumberValidator();
if( !creditCardNumberValidator.isValid(prop,val) ){
validatorResults.addError(name,creditCardNumberValidator.getMessage(prop,message),display);
break;
}
break;
}
case "SSN" : {
var ssnValidator = new validator.SSNValidator();
if( !ssnValidator.isValid(prop,val) ){
validatorResults.addError(name,ssnValidator.getMessage(prop,message),display);
break;
}
break;
}
case "PHONE" : {
var phoneValidator = new validator.PhoneValidator();
if( !phoneValidator.isValid(prop,val) ){
validatorResults.addError(name,phoneValidator.getMessage(prop,message),display);
break;
}
break;
}
case "ZIPCODE" : {
var zipCodeValidator = new validator.ZipCodeValidator();
if( !zipCodeValidator.isValid(prop,val) ){
validatorResults.addError(name,zipCodeValidator.getMessage(prop,message),display);
break;
}
break;
}
case "DATE" : {
var dateValidator = new validator.DateValidator();
if( !dateValidator.isValid(prop,val) ){
validatorResults.addError(name,dateValidator.getMessage(prop,message),display);
break;
}
break;
}
case "ARRAY" : {
var arrayValidator = new validator.ArrayValidator();
if( !arrayValidator.isValid(prop,val) ){
validatorResults.addError(name,arrayValidator.getMessage(prop,message),display);
break;
}
break;
}
case "STRUCT" : {
var structValidator = new validator.StructValidator();
if( !structValidator.isValid(prop,val) ){
validatorResults.addError(name,structValidator.getMessage(prop,message),display);
break;
}
break;
}
case "BOOLEAN" : {
var booleanValidator = new validator.BooleanValidator();
if( !booleanValidator.isValid(prop,val) ){
validatorResults.addError(name,booleanValidator.getMessage(prop,message),display);
break;
}
break;
}
case "QUERY" : {
var queryValidator = new validator.QueryValidator();
if( !queryValidator.isValid(prop,val) ){
validatorResults.addError(name,queryValidator.getMessage(prop,message),display);
break;
}
break;
}
case "URL" : {
var urlValidator = new validator.URLValidator();
if( !urlValidator.isValid(prop,val) ){
validatorResults.addError(name,urlValidator.getMessage(prop,message),display);
break;
}
break;
}
case "UUID" : {
var uuidValidator = new validator.UUIDValidator();
if( !uuidValidator.isValid(prop,val) ){
validatorResults.addError(name,uuidValidator.getMessage(prop,message),display);
break;
}
break;
}
case "GUID" : {
var guidValidator = new validator.GUIDValidator();
if( !guidValidator.isValid(prop,val) ){
validatorResults.addError(name,guidValidator.getMessage(prop,message),display);
break;
}
break;
}
case "BINARY" : {
var binaryValidator = new validator.binaryValidator();
if( !binaryValidator.isValid(prop,val) ){
validatorResults.addError(name,binaryValidator.getMessage(prop,message),display);
break;
}
break;
}
case "NUMERIC" : {
var numericValidator = new validator.numericValidator();
if( !numericValidator.isValid(prop,val) ){
validatorResults.addError(name,numericValidator.getMessage(prop,message),display);
break;
}
break;
}
case "STRING" : {
var stringValidator = new validator.stringValidator();
if( !stringValidator.isValid(prop,val) ){
validatorResults.addError(name,stringValidator.getMessage(prop,message),display);
break;
}
break;
}
case "CUSTOM" : {
// TODO: We should throw a custom error here if the component was not found
var customValidator = createObject("component","#prop.custom#");
if( !customValidator.isValid(prop,val) ){
validatorResults.addError(name,customValidator.getMessage(prop,message),display);
break;
}
break;
}
}
}
}
}
/**
* @hint This utility function will look at a property to determine what the display name should be.
If a display attribute is provided it will use that if not it just uses the property name.
*/
private string function getDisplay(Struct prop){
var display = "";
if(structKeyExists(arguments.prop,"display")){
display = arguments.prop.display;
} else {
display = arguments.prop.name;
}
return display;
}
private array function getPropertiesRecusivly(required dto) {
var metadata = getMetaData(arguments.dto);
var properties = [];
/* Create an array of the first level object properties. We need to create our own array too allow us to append
* it in extended objects. The array returned from the getMetaData method does not support growth */
var p = getMetaData(arguments.dto).properties;
for (LOCAL.i=1; LOCAL.i <= arrayLen(p); LOCAL.i++) {
arrayAppend(properties, p[LOCAL.i]);
}
activeMetadata = metadata;
do {
extensionExists = structKeyExists(activeMetadata, 'extends') AND activeMetadata.extends.name != 'WEB-INF.cftags.component';
if ( extensionExists ) {
// Recursivly reassign the active metadata struct
activeMetadata = activeMetadata.extends;
for (LOCAL.i = 1; LOCAL.i <= arrayLen(activeMetadata.properties); LOCAL.i++) {
arrayAppend(properties, activeMetadata.properties[LOCAL.i]);
}
}
} while (extensionExists);
return properties;
}
}
Hi Aaron! I’m interested in using Hyrule in a current project, obviously the bugs above are a concern- has Dan integrated your findings into the working repos, and if not, is it easy to throw your work and his together?
Cheers,
Andy
@Andy Dan e-mailed me back and said he would bring the code in. I know he was hoping to find a solution that did not involve an additional object, which, could be accomplished if the Validate method also returned any messages–but–thats a pretty big API change so I am not sure what he decided.
I have the code in Production now and can send you a copy if you wish.
Thanks.
Just dawned on me- is the email I submit these comments under not visible to you?
@Aaron Thanks for the prompt reply- I’d really appreciate a copy
thanks amigo- good catch on the thread safe stuff, btw!