Skip to content

Instantly share code, notes, and snippets.

@raix
Last active December 27, 2015 18:59
Show Gist options
  • Save raix/7374295 to your computer and use it in GitHub Desktop.
Save raix/7374295 to your computer and use it in GitHub Desktop.
An idea for parsing arguments in Meteor.js With this code declaring interfaces and validating input just got a bit easier..
typeNames = function(type) {
if (Match.test(type, [Match.Any])) return 'array';
if (type === Object) return 'object';
if (type === String) return 'string';
if (type === Number) return 'number';
if (type === Boolean) return 'boolean';
if (type === Function) return 'function';
return typeof type;
};
// If arguments are correctly parsed then return the object
// otherwice we return the new Error() object - the user can then throw this
// if relevant
parseArguments = function(args, names, types) {
// Names are array of strings or string in array
check(names, [Match.OneOf(String, [String])]);
check(types, [Match.Any]);
check(args, [Match.Any]);
// Check lengths, we throw this since this function needs this
if (names.length !== types.length) {
throw new RangeError('names and types dont match');
}
// Make sure we dont have too many arguments to begin with, return parse error
if (args.length > names.length) {
return new Error('too many arguments');
}
var requiredLength = 0;
// Count required arguments
for (var i = 0; i < names.length; i++) {
if (names[i] === ''+names[i]) {
requiredLength++;
}
}
// Make sure we have enough arguments
if (args.length < requiredLength) {
return new Error('not enough arguments');
}
// The returning result object
var result = {};
// Argument index
var a = 0;
// Init number of allowed optionals left
var optionalsLeft = args.length - requiredLength;
// Go find a match
for (var i = 0; i < types.length; i++) {
// If name is a string then argument is required
var isRequired = names[i] === ''+names[i];
var name = (isRequired)? names[i] : names[i][0];
// Check to see if we have a type match
if (Match.test(args[a], types[i])) {
if (isRequired || optionalsLeft > 0) {
// If not required then decrease number of optionals left
isRequired || optionalsLeft--;
// Check if we are about to overwrite an existing key
if (typeof result[name] !== 'undefined') {
throw new Error('duplicate argument names "' + name + '"');
}
// Set key and value on result
result[name] = args[a]; // could use args[a++]
// Goto next argument
a++;
}
} else {
// We are not allowed to skip over isRequired arguments
if (isRequired) {
return new TypeError('type (' + typeNames(args[a]) +
') did not match (' + typeNames(types[i]) +
') for required argument "' + names[i] + '"');
} // If not isRequired we skip to the next
// If more arguments left than interface allows then we are
// out of bounds...
if (args.length - a > types.length - i) {
return new TypeError('type (' + typeNames(args[a]) +
') did not match remaining arguments.' +
' eg.: "' + names[i-1] + '" (' + typeNames(types[i-1]) + ')');
}
}
}
return result;
};
Test = function(/* name, [options], [nr], nr2, [on], callback */) {
var self = this;
var result = parseArguments(arguments,
['name', ['options'], ['nr'], 'nr2', ['on'], 'callback' ],
[String, Object, Number, Number, Boolean, Function]
);
var self.options = {
test: 'default'
};
// If the parseArgument returned an instance of Error we can
// throw it or test with another parseArguments if we also
// accept a complete different input pattern
if (result instanceof Error) {
throw result;
} else {
// Finally the user got it right, we can now trust the
// input
console.log('GOT:');
console.log(result);
// Extend and overwrite options - we could use a more
// specific match than using Object
_.extend(self.options, result.options || {});
// Maybe use the callback
result.callback('We do have a callback since its required');
}
};
@aldeed
Copy link

aldeed commented Nov 13, 2013

@raix, I'm using this for a few of the CFS methods, and there's an issue with optional arguments. If all arguments are optional and you pass undefined or null for some of the optional arguments, it throws the "did not match remaining arguments" error. I can't focus enough to figure out the correct way to fix this at the moment.

@aldeed
Copy link

aldeed commented Nov 13, 2013

Maybe just need to check for null or undefined like this before throwing that error? I'm not sure if that will cause other issues.

if (args.length - a > types.length - i && args[a] !== void 0 && args[a] !== null) { }

@aldeed
Copy link

aldeed commented Nov 13, 2013

I rewrote it as follows and it seems to work now.

parseArguments = function(args, names, types) {
  // Names are array of strings or string in array
  check(names, [Match.OneOf(String, [String])]);
  check(types, [Match.Any]);
  check(args, [Match.Any]);
  // Check lengths, we throw this since this function needs this
  if (names.length !== types.length) {
    throw new RangeError("Names and types don't match");
  }

  // The returning result object
  var result = {}, t = 0, found, arg, type, name, argIsRequired;

  for (var a = 0; a < args.length; a++) {
    arg = args[a];
    found = false;
    while (!found && t < types.length) {
      type = types[t];
      name = names[t];
      argIsRequired = name === '' + name;
      if (Match.test(arg, type)) {
        if (typeof result[name] !== 'undefined') {
          throw new Error('Duplicate argument name: "' + name + '"');
        }
        // Set key and value on result
        result[name] = arg;
        found = true;
      } else {
        if (argIsRequired) {
          return new TypeError('type (' + typeNames(arg) +
                  ') did not match (' + typeNames(type) +
                  ') for required argument "' + name + '"');
        } else if (arg === null) {
          // It's OK for an optional argument to be null
          if (typeof result[name] !== 'undefined') {
            throw new Error('Duplicate argument name: "' + name + '"');
          }
          // Set key and value on result
          result[name] = arg;
          found = true;
        } else if (arg === void 0) {
          // It's OK for an optional argument to be undefined
          found = true;
        }
      }
      t++;
    }
  }

  // Done looping through supplied arguments.
  // Now check any remaining expected arguments to make sure none are required.
  while (t < types.length) {
    name = names[t];
    argIsRequired = name === '' + name;
    if (argIsRequired) {
      return new TypeError('required argument "' + name + '" is undefined');
    }
    t++;
  }

  return result;
};

@aldeed
Copy link

aldeed commented Nov 13, 2013

Here are some tests I did with it:

var at = function(/* name, [options], [nr], nr2, [on], callback */) {
  var result = parseArguments(arguments,
            ['name', ['options'], ['nr'], 'nr2', ['on'], 'callback'],
            [String, Object, Number, Number, Boolean, Function]
            );

  // If the parseArgument returned an instance of Error we can
  // throw it or test with another parseArguments if we also
  // accept a complete different input pattern
  if (result instanceof Error) {
    console.log(result.message);
  } else {
    console.log('GOT:', result);
  }
};

at();
at(function() {});
at("myName", 200);
at("myName", {});
at("myName", 200, function() {});
at("myName", {}, 200, function() {});
at("myName", {}, 100, 200, function() {});
at("myName", 100, 200, function() {});
at("myName", 100, 200, true, function() {});
at("myName", 100, 200, "true", function() {});
at("myName", {}, 100, 200, true, function() {});
at("myName", null, null, 200, null, function() {});
at("myName", void 0, void 0, 200, void 0, function() {});
at("myName", 200, true, function() {});

@raix
Copy link
Author

raix commented Nov 19, 2013

Thanks, nice :) yeah null is a bitch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment