views:

211

answers:

7

I'm developing a JavaScript library that will be used by 3rd party developers. The API includes methods with this signature:

function doSomething(arg1, arg2, options)

  • arg1, arg2 are 'required' simple type arguments.
  • options is a hash object containing optional arguments.

Would you recommend to validate that: - argument types are valid? - options attributes are correct? For example: that the developer didn't pass by mistake onSucces instead of onSuccess?

  • why do popular libraries like prototype.js do not validate?
+2  A: 

Don't validate. More code is more code the user has to download, so it's a very real cost on the user and production systems. Argument errors are easy enough to catch by the developer; don't burden the user with such.

Stefan Kendall
I disagree that this approach is always best. If the API is complex enough, no validation can lead to annoying errors. For example, there have been plenty of times when using the ExtJS library that I'd wished they had done more validation of incoming parameters, saving me hours of headache.
jvenema
Which would you rather have? Hours of headache, or no users because your site takes too long to load? The choice is obvious.
Stefan Kendall
Having neither is an option here. A little more work up front to make your compressor and/or build process a bit "smart" means no headache down the road, and nice speedy load times for your users.
jvenema
+3  A: 

You have the right to decide whether to make a "defensive" vs. a "contractual" API. In many cases, reading the manual of a library can make it clear to it's user that he should provide arguments of this or that type that obey these and those constraints.

If you intend to make a very intuitive, user friendly, API, it would be nice to validate your arguments, at least in debug mode. However, validation costs time (and source code => space), so it may also be nice to leave it out.

It's up to you.

xtofl
A: 

It depends. How big this library would be? It is said that typed languages are better for big projects with complex API. Since JS is to some extent hybrid, you can choose.

About validation - I don't like defensive programming, the user of the function shall be obliged to pass valid arguments. And in JS size of code matters.

doc
+3  A: 

Validate as much as you can and print useful error messages which help people to track down problems quickly and easily.

Quote this validation code with some special comments (like //+++VALIDATE and //--VALIDATE) so you can easily remove it with a tool for a high-speed, compressed production version.

Aaron Digulla
A: 

When I've developed APIs like these in the past, I've validated anything that I feel is a "major" requirement - in your example, I'd verify the first two arguments.

As long as you specify sensible defaults, it should be pretty simple for your user to determine that "optional" arguments aren't specified correctly, since it won't make any change to the application, but everything will still work properly.

If the API is complex, I'd suggest following Aaron's advice - add comments that can be parsed by a compressor around your validation so the developers get the benefit of validation, but can extract the extra dead weight when pushing the code into production.

EDIT:

Here're some examples of what I like to do in the cases where validation is necessary. This particular case is pretty simple; I probably wouldn't bother with validation for it, since it really is trivial. Depending on your needs, sometimes attempting to force types would be better than validation, as demonstrated with the integer value.

Assume extend() is a function that merges objects, and the helper functions exist:

    var f = function(args){
      args = extend({
        foo: 1,
        bar: function(){},
        biz: 'hello'
      }, args || {});

      // ensure foo is an int.
      args.foo = parseInt(args.foo);

      //<validation>
      if(!isNumeric(args.foo) || args.foo > 10 || args.foo < 0){
        throw new Error('foo must be a number between 0 and 10');
      }

      if(!isFunction(args.bar)){
        throw new Error('bar must be a valid function');
      }

      if(!isString(args.biz) || args.biz.length == 0){
        throw new Error('biz must be a string, and cannot be empty');
      }
      //</validation>
    };

EDIT 2:

If you want to avoid common misspellings, you can either 1) accept and re-assign them or 2) validate the argument count. Option 1 is easy, option 2 could be done like this, although I'd definitely refactor it into its own method, something like Object.extendStrict() (example code works w/ prototype):

var args = {
  ar: ''
};
var base = {
  foo: 1,
  bar: function(){},
  biz: 'hello'
};
// save the original length
var length = Object.keys(base).length;
// extend
args = Object.extend(base, args || {});
// detect if there're any extras
if(Object.keys(args).length != length){
  throw new Error('Invalid argument specified. Please check the options.')
}
jvenema
The optional arguments in some cases are harder to detect, for example I noticed that if timeout value was a string '60' - no timeout was set (the value was passed down to prototype.js..). Is it always possible to validate the optional attributes at all? I noticed prototype.js sometimes add additional methods, so how can differentiate wrong-named methods that were set by the developer?
Totach
Absolutely, they can definitely be harder to detect; all the more reason to validate. I've updated my answer to give some examples.
jvenema
A: 

An intermediate way would be to return a reasonable default value (e.g. null) when required arguments are missing. In this way, the user's code will fail, not yours. And it will probably be easier for them to figure out what is the issue in their code rather than in yours.

Eric Bréchemier
+1  A: 

Thanks for the detailed answers.

Below is my solution - a utility object for validations that can easily be extended to validate basically anything... The code is still short enough so that I dont need to parse it out in production.

WL.Validators = {

/*
 * Validates each argument in the array with the matching validator.
 * @Param array - a JavaScript array.
 * @Param validators - an array of validators - a validator can be a function or 
 *                     a simple JavaScript type (string).
 */
validateArray : function (array, validators){
 if (! WL.Utils.isDevelopmentMode()){
  return;
 }
 for (var i = 0; i < array.length; ++i ){      
  WL.Validators.validateArgument(array[i], validators[i]);
 }
},

/*
 * Validates a single argument.
 * @Param arg - an argument of any type.
 * @Param validator - a function or a simple JavaScript type (string).
 */
validateArgument : function (arg, validator){
 switch (typeof validator){
     // Case validation function.
     case 'function':
          validator.call(this, arg);
          break;             
     // Case direct type. 
        case 'string':
         if (typeof arg !== validator){
          throw new Error("Invalid argument '" + Object.toJSON(arg) + "' expected type " + validator);
         }
      break;
 }      
}, 

/*
 * Validates that each option attribute in the given options has a valid name and type.
 * @Param options - the options to validate.
 * @Param validOptions - the valid options hash with their validators:
 * validOptions = {
 *     onSuccess : 'function',
 *     timeout : function(value){...}
 * }
 */
validateOptions : function (validOptions, options){
 if (! WL.Utils.isDevelopmentMode() || typeof options === 'undefined'){
  return;
 }
    for (var att in options){
     if (! validOptions[att]){
            throw new Error("Invalid options attribute '" + att + "', valid attributes: " + Object.toJSON(validOptions));
        }
        try {
         WL.Validators.validateArgument(options[att], validOptions[att]);
        }
        catch (e){
         throw new Error("Invalid options attribute '" + att + "'");
        }
    } 
},

};

Heres a few examples of how I use it:

isUserAuthenticated : function(realm) {
WL.Validators.validateArgument(realm, 'string');



getLocation: function(options) {            
    WL.Validators.validateOptions{
        onSuccess: 'function', 
        onFailure: 'function'}, options);


makeRequest : function(url, options) {
    WL.Validators.validateArray(arguments, ['string', 
        WL.Validators.validateOptions.carry({
        onSuccess : 'function', 
        onFailure : 'function',
        timeout   : 'number'})]);
Totach