Skip to content

Instantly share code, notes, and snippets.

@bengillies
Created November 5, 2011 01:09
Show Gist options
  • Save bengillies/1340921 to your computer and use it in GitHub Desktop.
Save bengillies/1340921 to your computer and use it in GitHub Desktop.
extract.js - extract objects from an argument list
A library to parse arguments from functions based on their type.
/*
* A library to parse arguments from functions based on their type
*
*/
var extract = (function() {
var typeList = [];
function NoNameError(obj) {
return {
name: 'NoNameError',
msg: 'Object type has no name attribute',
value: obj
};
}
function TypeNotFoundError(obj) {
return {
name: 'TypeNotFoundError',
msg: 'Type "' + obj + '" not found',
value: obj
};
}
// Default to Array.forEach or make our own
var forEach = Array.prototype.forEach || function(callback) {
var i, l;
for (i = 0, l = this.length; i < l; i++) {
callback.call(this, this[i], i);
}
};
// Extend target with extra properties
function _extend(target) {
forEach.call(Array.prototype.slice.call(arguments, 1), function(obj) {
var prop;
for (prop in obj) {
if (obj.hasOwnProperty(prop)) {
target[prop] = obj[prop];
}
}
});
return target;
}
// define a base Type type
function Type(name, obj) {
var title = (typeof name === 'string') ? name :
(name && name.name) ? name.name : null,
typeObj = (typeof name !== 'string') ? name :
(obj || {});
if (!title) {
throw NoNameError(typeObj);
}
_extend(this, typeObj);
this.name = title;
}
_extend(Type.prototype, {
parse: function(obj) {
return obj;
},
test: function() {
return true;
},
list: false,
defaultValue: null,
extend: function(name, obj) {
var newType = function() {};
newType.prototype = _extend(new Type('default'), this);
var extension = new Type(name, obj);
return _extend(new newType(), extension);
}
});
// define a default type (name is mandatory, so not included here)
var defaultType = new Type('default');
// add a new type to the type list
function addType() {
var args = extract(
{ name: 'name', test: function(o) { return typeof o === 'string'; } },
{ name: 'type', parse: function(o) {
return (o instanceof Type) ? o : new Type(this.name, o);
} }
).from(arguments);
typeList.push(args.type);
}
function getType(name) {
var result = {};
forEach.call(typeList, function(type) {
if (type.name === name) {
result = type;
}
});
return result;
}
function listTypes() {
return _extend([], typeList);
}
function clearTypes() {
typeList = [];
}
/*
* from: turn a list of function arguments into a predictable object
*/
function from(args) {
var result = {},
types = this.types || [],
set = function(type, value) {
if (type.list) {
result[type.name] = result[type.name] || [];
result[type.name].push(value);
} else {
result[type.name] = result[type.name] || value;
}
};
args = Array.prototype.slice.call(args); // args is likely arguments so not a proper array
forEach.call(types, function(obj, pos) {
var type = (typeof obj === 'string') ? getType(obj) : new Type(obj),
index = 0,
arg;
if (type.name) {
while (index < args.length) {
arg = args[index];
if (type.test.call(result, arg)) {
set(type, type.parse.call(result, arg));
args.splice(index, 1);
if (!type.list) {
break;
}
} else {
index++;
}
}
if (!result[type.name]) {
set(type, type.defaultValue);
}
} else {
throw (type.get) ? NoNameError(type) : TypeNotFoundError(obj);
}
});
return result;
}
/*
* Pass in some types that describe objects you would like returned.
* You then get a "from" function that you can pass an array into to extract
* those objects from.
*
* e.g.:
*
* var args = extract(
* { name: 'tiddler', parse: function(t) { return store.get(t); }, default: null },
* { name: 'callback', pos: 0, test: function(f) { return typeof f === 'function'; } },
* { name: 'sub', test: function(s) { return s instanceof Sub; } },
* { name: 'msg', pos: 1, test: function(s) { return typeof s === 'string'; } },
* { name: 'renderFlag', pos: 2, parse: function(r) { return (r) ? true : false; } },
* { name: 'rest', list: true, test: function(a) { return typeof a === 'number'; } }
* ).from(arguments);
*
*/
function extract() {
var types = Array.prototype.slice.call(arguments);
return {
types: types,
from: from
};
}
return _extend(extract, {
Type: Type,
defaultType: defaultType,
addType: addType,
getType: getType,
listTypes: listTypes,
clearTypes: clearTypes
});
}());
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test Suite</title>
<link rel="stylesheet" type="text/css" href="lib/qunit.css">
</head>
<body>
<h1 id="qunit-header">Test Suite</h1>
<h2 id="qunit-banner"></h2>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<!--load QUnit and dependencies-->
<script src="lib/qunit.js" type="text/javascript"></script>
<script src="lib/jquery.js" type="text/javascript"></script>
<!--load your scripts in here-->
<script src="file:///Users/bengillies/tmp/typeargs/extract.js" type="text/javascript"></script>
<script src="fixtures.js" type="text/javascript"></script>
<!--load tests-->
<script src="test_main.js" type="text/javascript"></script>
<script src="test_types.js" type="text/javascript"></script>
</body>
</html>
.PHONY: test
test:
qunit test/index.html
module('extract/from', {});
test('Default Options return args in order', function() {
var args = extract({name: 'foo'}, {name: 'bar'}).from(['arg1', 'arg2']);
strictEqual(args.foo, 'arg1', 'arg1 is the first argument, so associates with foo');
strictEqual(args.bar, 'arg2', 'arg2 is the second argument, so associates with bar');
});
test('left over args lost', function() {
var args = extract({name: 'foo'}, {name: 'bar'}).from(['arg1', 'arg2', 'arg3']);
expect(2);
$.each(args, function(i, value) {
strictEqual(value !== 'arg3', true, 'the left over argument should be lost');
});
});
test('list holds an array', function() {
var args = extract({name: 'foo'}, {name: 'rest', list: true})
.from(['arg1', 'arg2', 'arg3']);
strictEqual(args.foo, 'arg1', 'args before the list should still exist');
strictEqual(args.rest.length, 2, 'rest should be a list');
strictEqual(args.rest[0], 'arg2', 'the first item should be arg2');
strictEqual(args.rest[1], 'arg3', 'the second item should be arg3');
});
test('"test" function sorts parameters', function() {
var args = extract(
{ name: 'foo', test: function(o) { return typeof o === 'string'; } },
{ name: 'bar', test: function(o) { return typeof o === 'function'; } },
{ name: 'baz', test: function(o) { return typeof o === 'number'; }, list: true }
).from([10, function() { return 'bar'; }, 20, "foo"]);
strictEqual(args.foo, 'foo', 'foo should return the string');
strictEqual(args.bar(), 'bar', 'bar should return the function');
strictEqual(args.baz.length, 2, 'bar should be a list of 2 numbers');
strictEqual(args.baz[0], 10, 'baz[0] should be the first number');
strictEqual(args.baz[1], 20, 'baz[1] should be the second number');
});
test('"test" function returns only the first item when list is not set', function() {
var args = extract({ name: 'foo', test: function(o) { return typeof o === 'string'; } })
.from([10, 'arg1', 20, 'arg2']);
strictEqual(args.foo, 'arg1', 'foo returns the first string. All others are ignored');
});
test('"parse" function modifies arguments', function() {
var args = extract({ name: 'foo', parse: function(o) { return o + 'bar'; } })
.from(['foo']);
strictEqual(args.foo, 'foobar', 'the original argument should be modified');
});
test('"parse" function allows object decomposition', function() {
var args = extract({
name: 'foo',
parse: function(o) { this.bar = o.bar; this.baz = o.baz; return o; }
}).from([{ bar: 'bar', baz: 'baz'}]);
strictEqual(typeof args.foo === 'object', true, 'foo becomes the whole object');
strictEqual(args.foo.bar, 'bar', 'foo contains bar');
strictEqual(args.foo.baz, 'baz', 'foo contains baz');
strictEqual(args.bar, 'bar', 'bar becomes the "bar" part of foo');
strictEqual(args.baz, 'baz', 'baz becomes the "baz" part of foo');
});
test('strings as types', function() {
extract.addType('foo', { test: function(o) { return o === 'foo'; },
parse: function(o) { return o + 'bar'; } });
var args = extract('foo').from([10, 'foo', 20]);
strictEqual(args.foo, 'foobar', 'the string "foo" should return the "foo" type');
});
module('Type', {
teardown: function() {
extract.clearTypes();
}
});
test('New Type', function() {
var t = new extract.Type({
name: 'foo',
parse: function(o) {
return o + 'bar';
}
});
strictEqual(t instanceof extract.Type, true, 't is a Type');
strictEqual(t.parse('foo'), 'foobar', 'it gains a new parse method');
strictEqual(t.name, 'foo', 'it is called foo');
strictEqual(t.test('qwerty'), true, 'it uses the default test function');
});
test('extend Type', function() {
var t = new extract.Type({
name: 'foo',
parse: function(o) {
return o + 'bar';
}
});
var newT = t.extend('bar', { test: function(o) { return o === 'bar'; } });
strictEqual(newT instanceof extract.Type, true, 'newT is also a Type');
strictEqual(t.test('qwerty'), true, 't still uses the default test');
strictEqual(newT.test('qwerty'), false, 'newT does not pass the default test');
strictEqual(newT.test('bar'), true, 'newT uses its own test method');
});
test('store types', function() {
var t = new extract.Type({
name: 'foo',
parse: function(o) {
return o + 'bar';
}
});
var newT = t.extend('bar', { test: function(o) { return o === 'bar'; } });
extract.addType(t);
extract.addType(newT);
var types = extract.listTypes();
strictEqual(types.length, 2, 'there are two types');
strictEqual(types[0].name, 'foo', 'the first type is foo');
strictEqual(types[1].name, 'bar', 'the second type is bar');
var getType = extract.getType('foo');
strictEqual(getType.name, 'foo', 'We can retrieve the foo type by name');
strictEqual(getType.parse('foo'), 'foobar', 'it is actually the foo type');
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment