require "httpi/request" require "akami" require "savon/wasabi/document" require "savon/soap/xml" require "savon/soap/request" require "savon/soap/response" module Savon # = Savon::Client # # Savon::Client is the main object for connecting to a SOAP service. class Client # Initializes the Savon::Client for a SOAP service. Accepts a +block+ which is evaluated in the # context of this object to let you access the +wsdl+, +http+, and +wsse+ methods. # # == Examples # # # Using a remote WSDL # client = Savon::Client.new("http://example.com/UserService?wsdl") # # # Using a local WSDL # client = Savon::Client.new File.expand_path("../wsdl/service.xml", __FILE__) # # # Directly accessing a SOAP endpoint # client = Savon::Client.new do # wsdl.endpoint = "http://example.com/UserService" # wsdl.namespace = "http://users.example.com" # end def initialize(wsdl_document = nil, &block) self.config = Savon.config.clone wsdl.document = wsdl_document if wsdl_document process 1, &block if block wsdl.request = http end # Accessor for the Savon::Config. attr_accessor :config # Returns the Savon::Wasabi::Document. def wsdl @wsdl ||= Wasabi::Document.new end # Returns the HTTPI::Request. def http @http ||= HTTPI::Request.new end # Returns the Akami::WSSE object. def wsse @wsse ||= Akami.wsse end # Returns the Savon::SOAP::XML object. Please notice, that this object is only available # in a block given to Savon::Client#request. A new instance of this object is created # per SOAP request. attr_reader :soap # Executes a SOAP request for a given SOAP action. Accepts a +block+ which is evaluated in the # context of this object to let you access the +soap+, +wsdl+, +http+ and +wsse+ methods. # # == Examples # # # Calls a "getUser" SOAP action with the payload of "123" # client.request(:get_user) { soap.body = { :user_id => 123 } } # # # Prefixes the SOAP input tag with a given namespace: "..." # client.request(:wsdl, "GetUser") { soap.body = { :user_id => 123 } } # # # SOAP input tag with attributes: ..." # client.request(:get_user, "xmlns:wsdl" => "http://example.com") def request(*args, &block) raise ArgumentError, "Savon::Client#request requires at least one argument" if args.empty? self.soap = SOAP::XML.new(config) preconfigure extract_options(args) process &block if block soap.wsse = wsse response = SOAP::Request.new(config, http, soap).response set_cookie response.http.headers if wsse.verify_response WSSE::VerifySignature.new(response.http.body).verify! end response end private # Writer for the Savon::SOAP::XML object. attr_writer :soap # Accessor for the original self of a given block. attr_accessor :original_self # Passes a cookie from the last request +headers+ to the next one. def set_cookie(headers) if headers["Set-Cookie"] @cookies ||= {} #handle single or multiple Set-Cookie Headers as returned by Rack::Utils::HeaderHash in HTTPI set_cookies = [headers["Set-Cookie"]].flatten set_cookies.each do |set_cookie| # use the cookie name as the key to the hash to allow for cookie updates and seperation # set the value to name=value (for easy joining), stopping when we hit the Cookie options @cookies[set_cookie.split('=').first] = set_cookie.split(';').first end http.headers["Cookie"] = @cookies.values.join(';') end end # Expects an Array of +args+ and returns an Array containing the namespace (might be +nil+), # the SOAP input and a Hash of attributes for the input tag (which might be empty). def extract_options(args) attributes = Hash === args.last ? args.pop : {} namespace = args.size > 1 ? args.shift.to_sym : nil input = args.first [namespace, input, attributes] end # Expects an Array of +args+ to preconfigure the system. def preconfigure(args) soap.endpoint = wsdl.endpoint soap.element_form_default = wsdl.element_form_default body = args[2].delete(:body) soap.body = body if body wsdl.type_namespaces.each do |path, uri| soap.use_namespace(path, uri) end wsdl.type_definitions.each do |path, type| soap.types[path] = type end soap_action = args[2].delete(:soap_action) || args[1] set_soap_action soap_action if wsdl.document? && (operation = wsdl.operations[args[1]]) && operation[:namespace_identifier] soap.namespace_identifier = operation[:namespace_identifier].to_sym soap.namespace = wsdl.parser.namespaces[soap.namespace_identifier.to_s] # Override nil namespace with one specified in WSDL args[0] = soap.namespace_identifier unless args[0] else soap.namespace_identifier = args[0] soap.namespace = wsdl.namespace end set_soap_input *args end # Expects an +input+ and sets the +SOAPAction+ HTTP headers. def set_soap_action(input_tag) soap_action = wsdl.soap_action(input_tag.to_sym) if wsdl.document? soap_action ||= Gyoku::XMLKey.create(input_tag).to_sym http.headers["SOAPAction"] = %{"#{soap_action}"} end # Expects a +namespace+, +input+ and +attributes+ and sets the SOAP input. def set_soap_input(namespace, input, attributes) new_input_tag = wsdl.soap_input(input.to_sym) if wsdl.document? new_input_tag ||= Gyoku::XMLKey.create(input) soap.input = [namespace, new_input_tag.to_sym, attributes] end # Processes a given +block+. Yields objects if the block expects any arguments. # Otherwise evaluates the block in the context of this object. def process(offset = 0, &block) block.arity > 0 ? yield_objects(offset, &block) : evaluate(&block) end # Yields a number of objects to a given +block+ depending on how many arguments # the block is expecting. def yield_objects(offset, &block) yield *[soap, wsdl, http, wsse][offset, block.arity] end # Evaluates a given +block+ inside this object. Stores the original block binding. def evaluate(&block) self.original_self = eval "self", block.binding instance_eval &block end # Handles calls to undefined methods by delegating to the original block binding. def method_missing(method, *args, &block) super unless original_self original_self.send method, *args, &block end end end