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?
Archived Comments
I would love to tackle this if I wasn't so busy. I'll take a shot later today if no one has answered!
I cant promise this is perfect, but I wrote this very thing a few months ago to do cfc testing. This will get your dot notation cfc, retrieve arguments, and create the form to accept inputs, then it will execute the cfc and try and dump the results below. Its not perfect, but it works!
(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 //--->
Justice - how does your form handle argument inputs? Does it handle different types? I was thinking of something that evaluates data types and creates an appropriate form element per that type (I.E. Checkboxes for booleans...)
Yea, I was thinking the same thing as I was looking through my code again, and a lot like you, I dont have a ton of time today to go through it.
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.
This one definitely has my wheels spinning. It's gonna be tough to handle some data types. For structs I was thinking of a textarea that allows for key=value pairs to be input on separate lines. That could be easily parsed into a struct and passed. For queries, maybe a text area that allows an sql statement that is parsed into a query with a helper method and then passed? One dimensional arrays could be handled like structs, but what about multi-dimensional? What about XML, etc. I can picture a bunch of helper functions that parse the data, but some things can be difficult to accept in a form. Maybe for xml we allow for an upload of an external xml file to be passed?
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.
I am game for it of course....ah man....I'm working :-(
To expand on todd's thoughts of handling different argument types, what about handling types like 'any' or a component instance?
I would say don't worry about it Jalpino - I'm imaging this tool being used for more simple testing.
this seems like it would be a very useful utility... like maybe an entire contest instead of just a one-day thing
Hey, all power to you or anyone who wants to do this. As I'm in the middle of a contest now I can't start a new one. ;)
You could probably do this all remotely using AJAX, Flash CFForm or Flex using a facade cfc. Using the facade would mean that it didn't matter if the requested component and method didn't have remote access.
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....