In last weeks puzzler you started your new job as Head IT Geek/Elf of Santa's workshop. Your initial task was simple, but now things are getting complex, and with Christmas fast approaching, you don't have much time to finish the task. (Gee, thank goodness this is just pretend and not like real life.)
In today's puzzler, you need to help Santa shop for toy parts for the workshop. Santa works with various providers across the world and while they have been on the Net for sometime now - Santa is only just catching up now.
Your task is to build an interface to 3 toy part producers to check their availability and price. You will need to provide a report to present to Santa so he can make a final call. Each producer has a different API, and guess what - it isn't really published. So you will need to play around a bit with the services to see what they return. Here are your three services:
Alpha Toy Shop's API may be found here: http://ray.camdenfamily.com/demos/santa/alpha.cfc?wsdl
Beta Toy Shop's API may be found here: http://ray.camdenfamily.com/demos/santa/beta.cfc?wsdl
Charlie Toy Shop's API may be found here: http://ray.camdenfamily.com/demos/santa/charlie.cfm
Note that Charlie is not using a web service. To make it easy, each of these services requires a product ID, but it doesn't matter what you send - the result will be the same.
Here are some things to consider: Your code will need to integrate with three different services to get the data needed for the report. Can you abstract the calls to those services so it is easy to update without changing the code to handle the report? Can you handle a case where a remote API fails? Outside of this puzzle - how do you deal with remote data in general? In particular, remember my Flex post on the topic as an example.
As always - your time limit is short - 10 minutes or so (I don't want to get anyone fired) and your reward will be great (ok, nothing, but ego counts, right?). Enjoy!
Archived Comments
Please tell me there is an easier way to abstract out the method and argument names from the webservice?
<cfset aServ = structNew()>
<cfset aServ.url = "http://ray.camdenfamily.com...">
<cfset aServ.method = "checkAvailability">
<cfset aServ.argName = "productid">
<cfset bServ = structNew()>
<cfset bServ.url = "http://ray.camdenfamily.com...">
<cfset bServ.method = "chkAvailability">
<cfset bServ.argName = "product">
<cfset cServ = structNew()>
<cfset cServ.url = "http://ray.camdenfamily.com...">
<cfset cServ.method = "">
<cfset cServ.argName = "productId">
<cfset variables.mailTo = "todd@cfsilence.com">
<cfset variables.mailFrom = "todd@cfsilence.com">
<cfset variables.productId = 123>
<cffunction name="errorHandler" access="private" output="false" hint="i handle remote errors" returntype="void">
<cfargument name="catch" required="true">
<cfargument name="args" type="struct" required="true">
<cfmail to="#variables.mailTo#" from="#variables.mailFrom#" subject="error"><cfdump var="#arguments#"></cfmail>
</cffunction>
<cffunction name="getService" access="public" output="false" hint="i get the service for santa" returntype="any">
<cfargument name="serviceStruct" hint="take a wild guess" type="struct" required="true">
<cfargument name="productId" type="string" required="true">
<cfset var results = structNew()>
<cfset var unavail = serviceStruct.url & ": Service Unavailable">
<cftry>
<cfif serviceStruct.url contains "wsdl">
<cfinvoke webservice="#serviceStruct.url#" method="#serviceStruct.method#" returnvariable="service">
<cfinvokeargument name="#serviceStruct.argName#" value="#arguments.productId#"/>
</cfinvoke>
<cfset results = service>
<cfelse>
<cfhttp url="#serviceStruct.url#" throwonerror="true" timeout="30">
<cfhttpparam type="url" name="#serviceStruct.argName#" value="#arguments.productId#">
</cfhttp>
<cfset results.available = XMLParse(cfhttp.FileContent).availability.quantity.XMLText>
<cfset results.price = XMLParse(cfhttp.FileContent).availability.price.XMLText>
</cfif>
<cfcatch type="any">
<cfset errorHandler(cfcatch,arguments)>
<!--- if this was for Flex i'd <cfrethrow> and use the flex error handler - but i still want to know about the error --->
<cfreturn unavail />
</cfcatch>
</cftry>
<cfreturn results />
</cffunction>
<cfdump var="#getService(aServ,variables.productId)#">
<cfdump var="#getService(bServ,variables.productId)#">
<cfdump var="#getService(cServ,variables.productId)#">
I should say 'easy' - not 'easier' - since I hardcoded it.
Interesting. One thing I'd probably change is not looking for WSDL in the url, but storing at creation type a 'type' attribute.
I originally had an isWSDL key in the struct, but thought this would be more dynamic.
Todd, I think you got the right idea here, but I would actually structure this a little differently. Ignoring the wsdl change for now, I would make the service and method each a separate argument and then have an arguments structure to pass. Like so:
<cffunction name="getService" access="public" output="false" hint="i get the service for santa" returntype="any">
<cfargument name="url" type="string" required="true">
<cfargument name="method" type="string" required="false" default="">
<cfargument name="args" type="structure" required="false">
<cfset var thisArg = "" />
<cfset var results = structNew()>
<cfset var unavail = arguments.url & ": Service Unavailable">
<cftry>
<cfif arguments.url contains "wsdl">
<cfinvoke webservice="#arguments.url#" method="#arguments.method#" returnvariable="service">
<cfloop collection="#arguments.args#" item="thisArg">
<cfinvokeargument name="#thisArg#" value="#arguments.args[thisArg]#"/>
</cfloop>
</cfinvoke>
<cfset results = service>
<cfelse>
<cfhttp url="#arguments.url#" throwonerror="true" timeout="30">
<cfloop collection="#arguments.args#" item="thisArg">
<cfhttpparam type="url" name="#thisArg#" value="#arguments.args[thisArg]#">
</cfloop>
</cfhttp>
<cfset results.available = XMLParse(cfhttp.FileContent).availability.quantity.XMLText>
<cfset results.price = XMLParse(cfhttp.FileContent).availability.price.XMLText>
</cfif>
<cfcatch type="any">
<cfset errorHandler(cfcatch,arguments)>
<cfreturn unavail />
</cfcatch>
</cftry>
<cfreturn results />
</cffunction>
The reasons for this are: 1) It reduces errors because the API is a little clearer on what arguments are necessary; 2) If the product API for any one of them were to be expanded or changed, it wouldn't require us to modify the code as it is designed to handle any number of potential arguments.
Just to note, I didn't actually run this...so there could very well be a typo/mistake in the code :)