Wednesday, June 30, 2010

hijacking javascript functions part 2 - Enforce Parameters Type

We've seen in the last entry how we could hijack functions at distance and add some checking logic to the parameters in one place. We saved some typos, some possible mistakes and probably saved hours of debugging.But we needed to know the function parameters in order to check even some basic data types. Could not it be easier if like strongly typed languages, javascript could do this for us ?

We will see in this entry how using some naming conventions could allow us to check parameters automagically at distance and save us even more typos and ease debugging!

Getting javascript function code dynamically

The very nature of javascript is that it is dynamic. So dynamic that you can even access to functions code, it is not hard:

function doSomething(param1,param2,param3) {
    return param1;
}

now, to get the code of this very useful function, it is a one liner:
doSomething.toString(); // returns the above function code as a string

What is even nicer in Safari/FireFox/Chrome is that they clean up the function layout for us before giving it back to us.
so the following function:
function doSomething(param1 /* String */,
                                       param2 /* Number */,
                                       param3 /* Array */) {
     return param1;
}
will be returned as a string as:

function doSomething(param1,param2,param3) {
    return param1;

}

Obviously, IE does return the function as written.
Why does it matter will you ask?
Let's see what we intend to implement so that it becomes clear...

Using Naming Conventions

Using common conventions when programming is useful and in our case will become primordial to our purpose.
The above function was using param1 and param2 as parameters which does not really help. Even you in 6 months when you will be looking back to your code, perplex...
We will change the function so that it uses a naming convention.
Let's use one:

- String Object will all start with str + [A-Z]
so let's say that param1 is a String, we will change it to strParam1

- Number Object will start with num + [A-Z]
so let's say that param2 is a Number, we will change it to numParam2

- Array Object will start with arr + [A-Z]
so let's say that param3 is an Array, we will change it to arrParam3

Many conventions exists and this is just an example. Just change to what fit best your programming methods and don't hesitate to add more types.

so our first function will be changed to:


function doSomething(strParam1,numParam2,arrParam3) {
    return param1;

}

You will see that in the end this will help for reading and maintenance purpose anyway.

Parsing the function code to get the parameters

This is where things get dirty...
As IE does not send back a clean up version of the function, we shall look into many ways to parse the function but we will make it easy for our purpose.
We will assume that all functions put there parameters on one line, only separated by commas and with no space in between. Like so:


function doSomething(strParam1,numParam2,arrParam3) {
    return param1;

}

We could certainly handle different patterns but the code will get ugly. The concept is the same so we will not look too much into it.
So let's write a simple helper function to get the parameters:

DataController = {

    _getParametersName : function (code) {

        //if we did not get something just return
        if(code===undefined || !code) return [];

        //split the parameters
        var matches      = code.match(/\((.*\))/);

        // return them or an empty array
        return (matches[1]) ? matches[1].split(',') : [];
    }
};

As you can see here, we shall look into more checking as to if the code parameters is really a string or not but let's pretend you will DIY.
How do we use it?Easy as pie:
var paramNames = DataController.getParametersName(doSomething.toString());
alert(paramNames);//should display strParam1,numParam2,arrParam3

So we have our parameter names now with the extra information about their type!
Let's move on to the next step then...

Hijacking function revisited

First a reminder with the above _getParametersName added:

var DataController = {


    _getParametersName : function (code) {

        //if we did not get something just return
        if(code===undefined || !code) return [];

        //split the parameters
        var matches      = code.match(/\((.*\))/);

        // return them or an empty array
        return (matches[1]) ? matches[1].split(',') : [];
    },

    hijack : function(oPackage,fName,fConstraint) {

        var fCode = oPackage[fName];
        if(typeof fCode !=='function') return false;

        oPackage[fName] = function() {
            var parameters = DataController.toArray(arguments);
            var ret = fConstraint(fName,parameters);
            if(!ret) return false;
            return fCode.apply(oPackage,parameters);
        }
    },

    addConstraint : function(oPackage, fConstraint) {
        for(var prop in oPackage) {
            DataController.hijack(oPackage,prop,fConstraint);
        }
        return true;
    },
    toArray : function (arg){

      var ret =[];
      for(var i=0,ln=arg.length;i < ln;i++){
          ret.push(arg);
      }
      return ret;
   },
   _isString : function(str){
        if(str===undefined) return false;
        if(str.constructor===String)
            return true;
        return false;
    }
};
Look at the previous entry for further details of the what, how and why.
Basically what we are going to do is add some default checking in the hijack functions.
It will not only use the callback but also use its own default parameter checking based on pre defined type. the default checking function will look like this:

DataController = {

_checkDefaultType : function(func,parameters) {


        // get the parameters name
        var awaitedVals  = DataController._getParametersName(func.toString());

       //loop through all the parameters found
        for(var i=0,ln=awaitedVals.length;i < ln; i++) {

            //if the parameter start with num followed by a capital letter
           //we check if it is a number

            if(awaitedVals[i].match(/^num[A-Z]/)){
                //if it's not we just log a message and return false
                if(!DataController._isNumber(parameters[i])){
                   console.log("parameter "+i+"should be a Number but got:"+parameters[i])
                   return false;
               }
            }

           //same thing but for Array
            if(awaitedVals[i].match(/^arr[A-Z]/)){
                if(!DataController._isArray(parameters[i]))
                   console.log("parameter "+i+"should be an Array but got:"+parameters[i])
                   return false;
            }

           //same thing but for String
            if(awaitedVals[i].match(/^str[A-Z]/)){
                if(!DataController._isString(parameters[i]))
                   console.log("parameter "+i+"should be a String but got:"+parameters[i])
                   return false;
            }
        }
        return true;
    }
};

This function uses some functions not listed yet they just check for the type of the data. You will find a basic implementation at the end of this entry.

Now that we have our default type checking function available, it is just a matter of adding it to the hijack function like so:

    hijack : function(oPackage,fName,fConstraint) {

        var fCode = oPackage[fName];
        if(typeof fCode !=='function') return false;

        oPackage[fName] = function() {
            var parameters = DataController.toArray(arguments);

            //here is the magic part
            var ret = DataController._checkDefaultType(fCode,parameters);
            if(!ret) return false;

            ret = fConstraint(fName,parameters);
            if(!ret) return false;
            return fCode.apply(oPackage,parameters);
        }
    }

That's it!!!
May seem a lot but it was quite easy (even if there is a lot of thing to do if you want to implement a more 'realistic' function parameter parser)

So now what do we have?
Basically, we can add some constraint to the a package like so:
var toolbox = {

    doSomething: function (strParam1,numParam2,arrParam3) {
         return param1;

   }
};
DataController.addConstraint(toolbox,function(){return true});

Here we return true by default as we do not add any other checking.
Thanks to this one line three parameters will checked automagically:

toolbox.doSomething("bonjour",2,[2,3,4]); // return bonjour
toolbox.doSomething(2,"bonjour",[2,3,4]); //oops, you inverted the parameters
// you will get a logged message in the console and false as a result

Conclusion

We've seen how using some naming conventions, the very dynamic of javascript could allow us to hijack functions and add default type checking for free!
We could add a flag:
DataController.mode="strict";
If set to strict the function could also check if the number of arguments meet the number of parameters.
if set to undefined, we could turn off the checking entirely too...
Possibilities are up to your imagination and your programming style!

Do you see any case where this kind of feature in javascript could have helped you??

Code

var DataController = {

      _checkDefaultType : function(func,parameters) {

        var awaitedVals  = DataController._getParametersName(func.toString());

        for(var i=0,ln=awaitedVals.length;i

            if(awaitedVals[i].match(/^num[A-Z]/)){

                if(!DataController._isNumber(parameters[i])){
                   console.log("parameter "+i+"should be a Number but got:"+parameters[i])
                   return false;
               }
            }

            if(awaitedVals[i].match(/^arr[A-Z]/)){
                if(!DataController._isArray(parameters[i]))
                   console.log("parameter "+i+"should be an Array but got:"+parameters[i])
                   return false;
            }

            if(awaitedVals[i].match(/^str[A-Z]/)){
                if(!DataController._isString(parameters[i]))
                   console.log("parameter "+i+"should be a String but got:"+parameters[i])
                   return false;
            }
        }
        return true;
    },

    hijack : function(oPackage,fName,fConstraint) {

        var fCode = oPackage[fName];
        if(typeof fCode !=='function') return false;

        oPackage[fName] = function() {
            var parameters = DataController.toArray(arguments);

            var ret = DataController._
checkDefaultType(fCode,parameters);
            if(!ret) return false;

            ret = fConstraint(fName,parameters);
            if(!ret) return false;

            return fCode.apply(oPackage,parameters);
        }
    },

    addConstraint : function(oPackage, fConstraint) {
        for(var prop in oPackage) {
            DataController.hijack(oPackage,prop,fConstraint);
        }
        return true;
    },
    toArray : function (arg){

      var ret =[];
      for(var i=0,ln=arg.length;i < ln;i++){
          ret.push(arg);
      }
      return ret;
   },
    _isNumber : function(str){
        if(str===undefined) return undefined;
        if(str.constructor===Number)
            return true;
        return false;
    },
    _isArray : function(ar){
        if(ar===undefined) return undefined;
        if(ar.constructor===Array)
            return true;
        return false;
    },
   _isString : function(str){
        if(str===undefined) return false;
        if(str.constructor===String)
            return true;
        return false;
    }
};

var StringToolBox = {

    camelize: function(strVal) {
        return str.replace(/-(.)/g, function(m, l){return l.toUpperCase()});
    },
    hyphenize : function(strVal) {
        return str.replace(/([A-Z])/g, function(m, l){return '-'+l.toLowerCase()});
    },
    firstToUpperCase : function(strVal) {
         return str.replace(/^([a-z])/, function(m, l){return l.toUpperCase()});
    },
    trim : function(strVal) {
         return str.replace(/^\s+|\s+$/g, '');
    }
};

DataController.addConstraint(StringToolBox,function(){return true;});


No comments: