Friday Puzzler: Automatic CFC Method Tester
One of the cooler features of ColdFusion components is their metadata. This lets you dig into the CFC via CFML and can enable some pretty powerful features. (See Canvas or the new rendering engine in BlogCFC 5.5 for examples.) Building on the fact that CF lets you grab this metadata easily, let me see how you build the following:
Write code that accepts as input both a CFC (dotted notation path) and a method (yes, you could grab this via metadata, but I'm trying to keep the contest short). Your code will then generate a form with inputs for all method arguments.
The code can assume nicely documented CFCs. (Ie, the method arguments all have type attributes.)
If you are really eager, don't prompt for the method, but just the CFC and follow it up with a prompt for the method where you provide the methods for the user.
The code should be a self posting form and should invoke the data sent by the user using cfinvoke. This will let you build the tester for methods that are not remote.
Anyone game for this?
Comments
(sorry its split into multi pages!)
Any feedback or refinement would be appreciated!
<!---// File index.cfm //--->
<script language="javascript">
var xmlhttp=false;
/*@cc_on @*/
/*@if (@_jscript_version >= 5)
// JScript gives us Conditional compilation, we can cope with old IE versions.
// and security blocked creation of the objects.
try {
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
} catch (E) {
xmlhttp = false;
}
}
@end @*/
if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
xmlhttp = new XMLHttpRequest();
}
// function getfile will perform our request using our created xmlhttp object //
function doAction(div,action,urlvars) {
var Current = action+"?RandomKey=" + Math.random() * Date.parse(new Date());
if (urlvars.charAt(1) != " ") {
var Current = Current+urlvars;
}
xmlhttp.open("GET", Current, true);
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4) {
document.getElementById(div).innerHTML = xmlhttp.responseText;
}
}
xmlhttp.send(null);
}
/* Function to parse through and create a string from a form of all form values */
function parseform(obj) {
var getstr = "&";
for(i=0; i<document.forms[0].elements.length; i++){
if(document.forms[0].elements[i].type == "checkbox") {
getstr += document.forms[0].elements[i].name + '=' + document.forms[0].elements[i].checked + '&';
}
else {
getstr += document.forms[0].elements[i].name + '=' + document.forms[0].elements[i].value + '&';
}
}
/* finish up the url string so anything further appended is correct */
getstr += '1=1';
return getstr;
}
</script>
<BR><BR><BR><BR>
<form name="cfc" action="javascript:void(0);" method="post" onsubmit="doAction('rslt','results.cfm',parseform(cfc)); return false;">
Full CFC Name: <input name="cfcname" onblur="doAction('opt','getMethods.cfm','&cfc=' + this.value);"> <a href="javascript:void(0);"
onclick="doAction('opt','getMethods.cfm','&cfc=' + document.cfc.cfcname.value);">Get Methods</a> <BR>
<div id="opt">
CFC Method to call:
<select name="method">
<option value="">Enter a CFC first</option>
</select>
<a href="javascript:void(0);" onclick="doAction('args','getArguments.cfm','&cfc=' + document.cfc.cfcname.value + '&method=' + document.cfc.method[document.cfc.method.selectedIndex].value)">Get Arguments</a><BR>
</div>
<div id="args">
Select a method to populate arguments
</div>
<button type="submit" value="Submit">Submit</button>
</form>
<div id="rslt">
</div>
<!---// End file index.cfm //--->
<!---// Begin file getMethods.cfm //--->
<cfsilent>
<cfset messageCFC = createObject("component", "#URL.CFC#")>
<cfset methods = getMetaData(messageCFC).functions>
<cfset methodList = ' ' />
<cfloop from="1" to="#arrayLen(methods)#" index="i">
<cfif i eq arrayLen(methods)>
<cfset methodList = methodList & methods[i].name />
<cfelse>
<cfset methodList = methodList & methods[i].name & ',' />
</cfif>
</cfloop>
<cfset sortedList = listSort(methodList, "textnocase", "asc", ',') />
</cfsilent>
<cfoutput>
CFC Method to call:
<select name="method">
<option value="">Select Method</option>
<cfloop from="1" to="#listLen(sortedList, ",")#" index="i">
<option value="#listGetAt(sortedList, i)#">#listGetAt(sortedList, i)#</option>
</cfloop>
</select><a href="javascript:void(0);" onclick="doAction('args','getArguments.cfm','&cfc=' + document.cfc.cfcname.value + '&method=' + document.cfc.method[document.cfc.method.selectedIndex].value)">Get Arguments</a><BR>
</cfoutput>
<!---// End file getMethods.cfm //--->
<!---// Begin file getAtguments.cfm //--->
<cfsilent>
<cfset messageCFC = createObject("component", "#URL.CFC#")>
<cfset arguments = getMetaData(messageCFC[URL.Method]).parameters>
</cfsilent>
<cfoutput>
<cfloop from="1" to="#ArrayLen(arguments)#" index="i">
#Arguments[i].Name# <input name="Arg_#arguments[i].Name#"> <cfif arguments[i].required><font color="red">Required</font></cfif><BR>
</cfloop>
</cfoutput>
<!---// End file getArguments //--->
<!---// Begin file results.cfm //--->
<!---// Look at the URL scope for variables starting with ARG_ //--->
<cfset arguments = 'blank' />
<cfset values = '0' />
<cfloop collection="#URL#" item="URLVar">
<cfif Left(URLVar, 4) eq 'ARG_'>
<cfset arguments = arguments & ',' & Replace(URLVar, 'ARG_', '') />
<cfset values = values & ',' & URL[URLVar] />
</cfif>
</cfloop>
<!---// Delete our seed records from the lists //--->
<cfset arguments = listDeleteAt(arguments, 1, ',') />
<cfset values = listDeleteAt(values, 1, ',') />
<BR>
Request Time: <cfdump var="#TimeFormat(now(), "HH:MM:SS")#"><BR>
Arguments: <cfdump var="#Arguments#"><BR>
Values: <cfdump var="#Values#"><BR><BR>
Return:<BR>
<cfinvoke component="#url.cfcname#" method="#url.method#" returnvariable="Return">
<cfloop from="1" to="#ListLen(Arguments, ",")#" index="i">
<cfinvokeargument name="#listGetAt(arguments, i, ",")#" value="#listGetAt(values, i, ",")#">
</cfloop>
</cfinvoke>
<cfdump var="#return#">
<!---// End file results.cfm //--->
Right now, my code wants the user to enter the right data types. =) It definitly needs some refinement, and I think it would be pretty slick build the whole thing into a flex or cfform with all the code in a cfc using flash remoting. Then we could build either a form or a data-grid asking for arguments and enforce the typing there.
I will work up a new one and see what I can do, maybe this weekend.
Am I overthinking? This could get complex. I'd love to collaborate with you on this Justice, since I think a Flex version would kick ass. I think this could be a good open source tool. I'll be on vacation next week, but if you want some help the following week let me know.
The facade cfc would take the component name and pass back a set of methods that it knows can be rendered by the form eg. simple datatypes; bool, date, guid, numeric, string, uuid and possibly xml. That list of methods would populate a drop down which would send another request for the parameters of the user selected method.
Using the params you could probably relatively easily generate a set of form fields. Completing the generated form would return you a dump of the result set. I guess the "dump" would vary depending on the returntype of the method.
I don't think you could simply automatically post the generated form. Randomising strings and dates, while they might be ok for testing invalid data, doesn't really allow for submitting valid test cases.
Anyway... just toying with ideas....

