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

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