Javascript Reflection & Introspection? huh
Javascript is such a cool language because it is so flexible. A friend challenged me to get JavaScript to support a feature that is commonly used in C#. In C# you can have custom attributes that can be applied to class, functions, arguments, packages, etc. This is very useful if you want to add metadata or declarative information to the code. I have built many things using this feature in C# like a REST API that uses the attributes to define documentation, ACLs, field validation, etc. This is very powerful because the code is very clean and declarative.
For exampl, lets say you want to make a generic REST API library so people can easily expose functions to be called throw a RESTful API. The standard way of doing this is to require the developer to explicitly add each function they want exposed to the REST interface by defining a configuration that maps a function to a RESTful endpoint. This setup and configuration code is separate from the code that has all the logic. Alternatively if you wanted a way to add metadata to the code that would not effect the way the function executes and stores the configuration details you could comment the code and use reflection to retrieve the metadata. C# and Java support this feature as a first class citizen but I do not know of a way to do this in JavaScript. So how can we do this?
Let’s enter into the world of JavaScript and why I love it! JavaScript is interpreted at runtime so we can just ask for the code and parse out the metadata. So how do we do this its easy! I will walk you through the code below.
function parseMetaTags(metadata) {
var tags = metadata.match(/\[@(.*?)\]/g)
, restult = {};
if(!tags) {
return false;
}
for(var i in tags) {
var str = tags[i]
, len = str.indexOf('{')
, tag = str.substring(str.indexOf('@') + 1,
(len != -1 ? len : str.length - 1))
, details = (len == -1 ? true :
eval('(' + str.substring(len, str.length - 1) + ')'));
restult[tag] = details;
}
return restult;
}
Function.prototype.reflect = exports.reflect = function(obj) {
obj = obj || this;
if(typeof obj === 'function') {
if(obj.args) {
return false;
}
var fnStr = obj.toString().replace(/[\n\t]/g,' ')
, argStr = fnStr.match(/\(.+?\)/)
, len = (argStr ? (argStr.index + argStr[0].length) : 11)
, fnBodyStr = fnStr.substring(len)
, metaData = {func:[], args:{}};
if(argStr) {
argStr = fnStr.substring(argStr.index + 1, len - 1);
var argList = argStr.match(
/(([$\s\w]+)?(\/\*.+?\*\/)?([$\s\w]+)?)+/g);
for(var i in argList) {
if(argList[i] != '') {
var meta = argList[i].match(
/(\/\*.+?\*\/)|([a-zA-Z_$][0-9a-zA-Z_$]*)/g);
var key = false
, val = [];
for(var j in meta) {
if(meta[j][0] == '/') {
var details = parseMetaTags(meta[j]);
if(details) {
val.push(details);
}
}
else key = meta[j];
}
metaData.args[key] = val;
}
}
}
if(fnBodyStr) {
var bodyList = fnBodyStr.match(/(\/\*.+?\*\/)/g);
for(var i in bodyList) {
if(bodyList[i] != '') {
var details = parseMetaTags(bodyList[i]);
if(details) {
metaData.func.push(details);
}
}
}
}
obj.args = metaData.args;
obj.func = metaData.func;
return true;
} else if(typeof obj === 'object') {
for(var i in obj) {
exports.reflect(obj[i]);
}
return true;
}
return false;
}
/* #### EXAMPLE #### */
function exampleAddUserToDB(
arg1 /* [@REST{postName:'name', type:'string'}] */
, arg2 /* [@REST{postName:'age', min:16, max:100}] */
) {
/* [@REST{type:'POST', access:'public'}] */
return true;
}
exampleAddUserToDB.reflect();
console.log(exampleAddUserToDB.args);
console.log(exampleAddUserToDB.func);
So what is going on in the code is very basic. The idea is we want to look for comments in the code that are formatted like /* [@attribute{json}]*/ and parse the comments out and turn them into metadata that can be attached to the function or its arguments. Because we want to add reflection to everything we add a prototype to Function. This makes it possible for any function to have reflect capabilities. The reflect function will call the toString() on itself. This gets the string representation of the function including all the comments so its just a matter of parsing out the tags in the code. After getting all the tags, we add the parsed tags onto the function itself so its easy to ask the function for its metadata. The function that parses the tags is parseMetaTags so you could build any kind of parser that would return any type of tags i.e. string, function, object, numbers.
This lets you add any kind of metadata to your code via comments that follow a standard format. Than at any time, you can call reflect that will parse out all the metadata and return an object that your code can query against. Lets take an example of having a RESTFul service. We now can comment in our code to let the RESTFul controller query to find out if the function should be called using GET, POST, what the endpoint URL is, rules around required arguments, inline documentation that the RESTFul service can return, etc. Basicly anything you can dream up. When you call the exampleAddUserToDB.reflect() it parses out the metadata so it can be accessed by exampleAddUserToDB.args and exampleAddUserToDB.reflect.func. Below is the output of the example above. As you can see all the metadata has been pulled out of the code. You can now build any function you want and add metadata to it and use reflection to retrieve it. What is more amazing is this is not apart of the JavaScript language but we still can support it in just a few lines. That is what I love about JavaScript it is so powerful.
{ arg1: [ { REST: { postName: 'name', type: 'string' } } ],
arg2: [ { REST: { postName: 'age', min: 16, max: 100 } } ] }
[ { REST: { type: 'POST', access: 'public' } } ]
I am not saying this is a good way to solve a problem but it is cool that we can do stuff like this. I love many things about C#, ObjC and Java each have their strengths and weaknesses but JavaScript seems to be more terse and flexible. This is both a good and bad thing! I really thing JavaScript is very very powerful, exciting, and fun to use and I hope sharing my weekend playground. I hope this with inspire you and your creativity to do other crazy cool things with JavaScript that it was not designed to do.
You can git (get) the code a github: demetriusj/js-reflection
4 Notes/ Hide
- wementallioncom liked this
- demetriusj-blog posted this