module Savon # == Savon::Service # # Savon::Service is a SOAP client library to enjoy. The goal is to minimize # the overhead of working with SOAP services and provide a lightweight # alternative to other libraries. # # ==== Example # # proxy = Savon::Service.new("http://example.com/ExampleService?wsdl") # response = proxy.find_user_by_id(:id => 123) class Service # Supported SOAP versions. SOAPVersions = [1, 2] # Content-Types by SOAP version. ContentType = { 1 => "text/xml", 2 => "application/soap+xml" } # Initializer expects an +endpoint+ URI and takes an optional SOAP +version+. def initialize(endpoint, version = 1) raise ArgumentError, "Invalid endpoint: #{endpoint}" unless /^http.+/ === endpoint raise ArgumentError, "Invalid version: #{version}" unless SOAPVersions.include? version @endpoint = URI(endpoint) @version = version end # Returns an instance of Savon::WSDL. def wsdl @wsdl ||= WSDL.new(@endpoint, http) end private # Dispatches a SOAP request, handles any HTTP errors and SOAP faults # and returns the SOAP response. def dispatch(soap_action, soap_body, response_xpath) ApricotEatsGorilla.nodes_to_namespace = { :wsdl => wsdl.choice_elements } headers, body = build_request_parameters(soap_action, soap_body) Savon.log("SOAP request: #{@endpoint}") Savon.log(headers.map { |k, v| "#{k}: #{v}" }.join(", ")) Savon.log(body) response = http.request_post(@endpoint.path, body, headers) Savon.log("SOAP response (status #{response.code})") Savon.log(response.body) soap_fault = ApricotEatsGorilla[response.body, "//soap:Fault"] raise_soap_fault(soap_fault) if soap_fault && !soap_fault.empty? raise_http_error(response) if response.code.to_i >= 300 ApricotEatsGorilla[response.body, response_xpath] end # Expects the requested +soap_action+ and +soap_body+ and builds and # returns the request header and body to dispatch a SOAP request. def build_request_parameters(soap_action, soap_body) headers = { "Content-Type" => ContentType[@version], "SOAPAction" => soap_action } namespaces = { :wsdl => wsdl.namespace_uri } body = ApricotEatsGorilla.soap_envelope(namespaces, @version) do ApricotEatsGorilla["wsdl:#{soap_action}" => soap_body] end [headers, body] end # Expects a Hash containing information about a SOAP fault and raises # a Savon::SOAPFault. def raise_soap_fault(soap_fault) message = case @version when 1 "#{soap_fault[:faultcode]}: #{soap_fault[:faultstring]}" else "#{soap_fault[:code][:value]}: #{soap_fault[:reason][:text]}" end raise SOAPFault, message end # Expects a Net::HTTPResponse and raises a Savon::HTTPError. def raise_http_error(response) raise HTTPError, "#{response.message} (#{response.code}): #{response.body}" end # Returns a Net::HTTP instance. def http @http ||= Net::HTTP.new(@endpoint.host, @endpoint.port) end # Catches calls to SOAP actions, checks if the method called was found in # the WSDL and dispatches the SOAP action if it's valid. Takes an optional # Hash of options to be passed to the SOAP action and an optional XPath- # Expression to define a custom XML root node to start parsing the SOAP # response at. def method_missing(method, *args) soap_action = camelize(method) super unless wsdl.soap_actions.include? soap_action soap_body = args[0] || {} response_xpath = args[1] || "//return" dispatch(soap_action, soap_body, response_xpath) end # Converts a given +string+ from snake_case to lowerCamelCase. def camelize(string) string.to_s.gsub(/_(.)/) { $1.upcase } if string end end end