Last active
December 27, 2015 18:59
-
-
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..
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | |
} | |
}; |
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) { }
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;
};
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() {});
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
@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
ornull
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.