# Loosely Coupled SOAP/WSDL WOA/Client in Ruby # Author: Dion Hinchcliffe (Hinchcliffe & Company) # E-mail: dion@hinchcliffeandco.com # Date: June 21st, 2005 # # Usage: oc OrderID Name Amount Price DeliveryDate # Description: Will check the contract at the WSDL URL and invoke the placeOrder operation described therein, # then wait ten (10) seconds and retrieve the order and print the order to the console # Note: Only the relevant location, binding, operations, and dependent schemas are checked # # IMPORTANT: Requires Ruby 1.8 or higher to work correctly with XML/XPATH libraries # # Updated: August 12th, 2006: - Required changes since XSL was breaking with newest version of Ruby # - Also added command-line documentation for XSD date format, or you can submit orders easily require 'net/http' require 'rexml/document' include REXML # Check the number of parameters and print usage if needed if ARGV.length !=6 raise "\noc.rb:\nOrder Client Usage: oc OrderID Name Amount Price DeliveryDate\nNote: DeliveryDate parameter must be in XSD datetime format - YYYY-MM-DDTHH:MM:SS; so, for example, December 14 1984 12:14:37 would be 1984-12-14T12:14:37." end # Fetch the WSDL at the URL provided puts "Get the Machine Readable Service Description" h = Net::HTTP.new('hinchcliffe.org', 80) resp, sd = h.get('/order.asmx?WSDL', nil ) #puts "Code = #{resp.code}" #puts "Message = #{resp.message}" #resp.each {|key, val| printf "%-14s = %-40.40s\n", key, val } doc = Document.new sd puts "Checking the contract..." # OK, we want to place an order, and we know it's WSDL, so let's check the contract # Is there an Order Service described? q = XPath.first( doc, "/wsdl:definitions/wsdl:service/@name[.='OrderService']") if q == nil raise "UNACCEPTABLE CONTRACT CHANGE: No OrderService present" end # The service is there, so lets check out the particulars, like if there's a SOAP binding q = XPath.first( doc, "/wsdl:definitions/wsdl:service/wsdl:port[@name='OrderServiceSoap' and @binding='tns:OrderServiceSoap']") if q == nil raise "UNACCEPTABLE CONTRACT CHANGE: No SOAP binding" end # There is a SOAP binding for OrderService, verify the details including the operations q = XPath.first( doc, "/wsdl:definitions/wsdl:binding[@name='OrderServiceSoap' and @type='tns:OrderServiceSoap' and " + "soap:binding[@transport = 'http://schemas.xmlsoap.org/soap/http' and @style = 'document'] and " + "wsdl:operation[@name = 'placeOrder' and soap:operation[@soapAction = 'http://www.hinchcliffe.org/placeOrder']] and "+ "wsdl:operation[@name = 'getOrderStatus' and soap:operation[@soapAction = 'http://www.hinchcliffe.org/getOrderStatus']]]") if q == nil raise "UNACCEPTABLE CONTRACT CHANGE: Check of the OrderService binding failed" end # OK, do we actually have SOAP ports for the operations we want q = XPath.first(doc, "/wsdl:definitions/wsdl:portType[@name='OrderServiceSoap' and " + "wsdl:operation[@name='placeOrder' and wsdl:input[@message='tns:placeOrderSoapIn'] and wsdl:output[@message='tns:placeOrderSoapOut']] and " + "wsdl:operation[@name='getOrderStatus' and wsdl:input[@message='tns:getOrderStatusSoapIn'] and wsdl:output[@message='tns:getOrderStatusSoapOut']]]") if q == nil raise "UNACCEPTABLE CONTRACT CHANGE: No placeOrder() operation binding" end # Now, let's check the input and output schemas of the SOAP messages, and if it all # matches, it's a wrap: the service contract is good # Step 1: Make sure the placeOrder() operation takes an order schema q = XPath.first(doc, "/wsdl:definitions/wsdl:types/s:schema[@targetNamespace='http://www.hinchcliffe.org/']/s:element[@name='placeOrder']/s:complexType/s:sequence/s:element[@minOccurs='1' and @ref='order']") if q == nil raise "UNACCEPTABLE CONTRACT CHANGE: placeOrder() doesn't take an order structure" end # Step 2a: Make sure the order structure still has the orderID field we need and their types haven't changed q = XPath.first(doc, "/wsdl:definitions/wsdl:types/s:schema/s:complexType[@name='Order']/s:sequence"+ "[s:element[@name = 'orderID' and @type='s:int']]") if q == nil raise"UNACCEPTABLE CONTRACT CHANGE: order structure doesn't have orderID (of type s:int) required for nominal operation" end # Step 2b: Make sure the order structure still has the name field we need and their types haven't changed q = XPath.first(doc, "/wsdl:definitions/wsdl:types/s:schema/s:complexType[@name='Order']/s:sequence"+ "[s:element[@name = 'name' and @type='s:string']]") if q == nil raise"UNACCEPTABLE CONTRACT CHANGE: order structure doesn't have name (of type s:string) required for nominal operation" end # Step 2c: Make sure the order structure still has the amount field we need and their types haven't changed q = XPath.first(doc, "/wsdl:definitions/wsdl:types/s:schema/s:complexType[@name='Order']/s:sequence"+ "[s:element[@name = 'amount' and @type='s:int']]") if q == nil raise"UNACCEPTABLE CONTRACT CHANGE: order structure doesn't have amount (of type s:int) required for nominal operation" end # Step 2d: Make sure the order structure still has the price field we need and their types haven't changed q = XPath.first(doc, "/wsdl:definitions/wsdl:types/s:schema/s:complexType[@name='Order']/s:sequence"+ "[s:element[@name = 'price' and @type='s:double']]") if q == nil raise"UNACCEPTABLE CONTRACT CHANGE: order structure doesn't have price(of type s:double) required for nominal operation" end # Step 2e: Make sure the order structure still has the deliverydate field we need and their types haven't changed q = XPath.first(doc, "/wsdl:definitions/wsdl:types/s:schema/s:complexType[@name='Order']/s:sequence"+ "[s:element[@name = 'deliverydate' and @type='s:dateTime']]") if q == nil raise"UNACCEPTABLE CONTRACT CHANGE: order structure doesn't have deliverydate (of type s:dateTime) required for nominal operation" end if q == nil raise"UNACCEPTABLE CONTRACT CHANGE: order structure doesn't have name (of type s: string) required for nominal operation" end # Step 3a: Last, check the in-sies and outies of getOrderStatus() since we need to make sure we get an order structure back q = XPath.first(doc, "/wsdl:definitions/wsdl:types/s:schema/s:element[@name='getOrderStatus']/s:complexType/s:sequence/s:element[@minOccurs='0' and @name='oid' and @type='s:string']") if q == nil raise "UNACCEPTABLE CONTRACT CHANGE: getOrderStatus doesn't take at least a string parameter" end # Step 3b: Does getOrderStatus response at least return an order structure q = XPath.first(doc, "/wsdl:definitions/wsdl:types/s:schema/s:element[@name='getOrderStatusResponse']/s:complexType/s:sequence/s:element[@minOccurs='1' and @ref='getOrderStatusResult']") if q == nil raise "UNACCEPTABLE CONTRACT CHANGE: getOrderStatus doesn't return the structure type expected" end # Step 3c: Does getOrderStatus response actually have the type of 'order'? q = XPath.first(doc, "/wsdl:definitions/wsdl:types/s:schema/s:element[@name='getOrderStatusResult' and @type='Order']") if q == nil raise "UNACCEPTABLE CONTRACT CHANGE: getOrderStatus doesn't have the right structure mapped to the result" end # mustUnderstand present? If there is any in this service, then we can't use it q = XPath.first(doc, "/wsdl:definitions/wsdl:types/@mustUnderstand") if q != nil raise "UNACCEPTABLE CONTRACT CHANGE: A mustUnderstand header has been added to the contract, which we can't handle" end # Checking is done, build the message according to the contractual format and send it soapOrderRequest = ""+ ""+ ""+ ""+ ""+ ""+ ARGV[1] + ""+ ""+ ARGV[2] + ""+ ""+ ARGV[3] + ""+ ""+ARGV[4]+""+ ""+ARGV[5]+""+ ""+ ""+ ""+ "" puts "Invoke placeOrder SOAP service" headers = Hash["Host","hinchcliffe.org","Content-Type","text/xml; charset=utf-8", "SOAPAction", "http://www.hinchcliffe.org/placeOrder"] httpresp, soapresp = h.post("/order.asmx", soapOrderRequest, headers) doc = Document.new soapresp puts("Request complete, checking for SOAP faults"); # Check for success q = XPath.first(doc, "//soap:Fault") if q != nil raise "SOAP FAULT: " + soapresp else puts "SUCCESS!" end # Wait 10 seconds # Build the getOrderStatus() request soapOrderRequest = ""+ ""+ ""+ ""+ ""+ARGV[1]+""+ ""+ ""+ "" puts "Invoke getOrder SOAP service" headers = Hash["Host","hinchcliffe.org","Content-Type","text/xml; charset=utf-8", "SOAPAction", "http://www.hinchcliffe.org/getOrderStatus"] httpresp, soapresp = h.post("/order.asmx", soapOrderRequest, headers) doc = Document.new soapresp puts("Request complete, checking for SOAP faults"); # Check for success q = XPath.first(doc, "//soap:Fault") if q != nil raise "SOAP FAULT: " + soapresp else puts "SUCCESS!" end puts soapresp