require "builder" require "gyoku" require "rexml/document" require "nori" require "savon/soap" Nori.configure do |config| config.strip_namespaces = true config.convert_tags_to { |tag| tag.snakecase.to_sym } end module Savon module SOAP # = Savon::SOAP::XML # # Represents the SOAP request XML. Contains various global and per request/instance settings # like the SOAP version, header, body and namespaces. class XML # XML Schema Type namespaces. SCHEMA_TYPES = { "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema", "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance" } # Expects a +config+ object. def initialize(config) self.config = config end attr_accessor :config # Accessor for the SOAP +input+ tag. attr_accessor :input # Accessor for the SOAP +endpoint+. attr_accessor :endpoint # Sets the SOAP +version+. def version=(version) raise ArgumentError, "Invalid SOAP version: #{version}" unless SOAP::VERSIONS.include? version @version = version end # Returns the SOAP +version+. Defaults to Savon.config.soap_version. def version @version ||= config.soap_version end # Sets the SOAP +header+ Hash. attr_writer :header # Returns the SOAP +header+. Defaults to an empty Hash. def header @header ||= config.soap_header.nil? ? {} : config.soap_header end # Sets the SOAP envelope namespace. attr_writer :env_namespace # Returns the SOAP envelope namespace. Uses the global namespace if set Defaults to :env. def env_namespace @env_namespace ||= config.env_namespace.nil? ? :env : config.env_namespace end # Sets the +namespaces+ Hash. attr_writer :namespaces # Returns the +namespaces+. Defaults to a Hash containing the SOAP envelope namespace. def namespaces @namespaces ||= begin key = ["xmlns"] key << env_namespace if env_namespace && env_namespace != "" { key.join(":") => SOAP::NAMESPACE[version] } end end def namespace_by_uri(uri) namespaces.each do |candidate_identifier, candidate_uri| return candidate_identifier.gsub(/^xmlns:/, '') if candidate_uri == uri end nil end def used_namespaces @used_namespaces ||= {} end def use_namespace(path, uri) @internal_namespace_count ||= 0 unless identifier = namespace_by_uri(uri) identifier = "ins#{@internal_namespace_count}" namespaces["xmlns:#{identifier}"] = uri @internal_namespace_count += 1 end used_namespaces[path] = identifier end def types @types ||= {} end # Sets the default namespace identifier. attr_writer :namespace_identifier # Returns the default namespace identifier. def namespace_identifier @namespace_identifier ||= :wsdl end # Accessor for whether all local elements should be namespaced. attr_accessor :element_form_default # Accessor for the default namespace URI. attr_accessor :namespace # Accessor for the Savon::WSSE object. attr_accessor :wsse def signature? wsse.respond_to?(:signature?) && wsse.signature? end # Returns the SOAP request encoding. Defaults to "UTF-8". def encoding @encoding ||= "UTF-8" end # Sets the SOAP request encoding. attr_writer :encoding # Accepts a +block+ and yields a Builder::XmlMarkup object to let you create # custom body XML. def body @body = yield builder(nil) if block_given? @body end # Sets the SOAP +body+. Expected to be a Hash that can be translated to XML via `Gyoku.xml` # or any other Object responding to to_s. attr_writer :body # Accepts a +block+ and yields a Builder::XmlMarkup object to let you create # a completely custom XML. def xml(directive_tag = :xml, attrs = {}) @xml = yield builder(directive_tag, attrs) if block_given? end # Accepts an XML String and lets you specify a completely custom request body. attr_writer :xml # Returns the XML for a SOAP request. def to_xml(clear_cache = false) if clear_cache @xml = nil @header_for_xml = nil end @xml ||= tag(builder, :Envelope, complete_namespaces) do |xml| tag(xml, :Header) { xml << header_for_xml } unless header_for_xml.empty? # TODO: Maybe there should be some sort of plugin architecture where # classes like WSSE::Signature can hook into this process. body_attributes = (signature? ? wsse.signature.body_attributes : {}) if input.nil? tag(xml, :Body, body_attributes) else tag(xml, :Body, body_attributes) { xml.tag!(*add_namespace_to_input) { xml << body_to_xml } } end end end private # Returns a new Builder::XmlMarkup object. def builder(directive_tag = :xml, attrs = { :encoding => encoding }) builder = Builder::XmlMarkup.new builder.instruct!(directive_tag, attrs) if directive_tag builder end # Expects a builder +xml+ instance, a tag +name+ and accepts optional +namespaces+ # and a block to create an XML tag. def tag(xml, name, namespaces = {}, &block) if env_namespace && env_namespace != "" xml.tag! env_namespace, name, namespaces, &block else xml.tag! name, namespaces, &block end end # Returns the complete Hash of namespaces. def complete_namespaces defaults = SCHEMA_TYPES.dup defaults["xmlns:#{namespace_identifier}"] = namespace if namespace defaults.merge namespaces end # Returns the SOAP header as an XML String. def header_for_xml @header_for_xml ||= (Hash === header ? Gyoku.xml(header) : header) + wsse_header end # Returns the WSSE header or an empty String in case WSSE was not set. def wsse_header wsse.respond_to?(:to_xml) ? wsse.to_xml : "" end # Returns the SOAP body as an XML String. def body_to_xml return body.to_s unless body.kind_of? Hash body_to_xml = element_form_default == :qualified ? add_namespaces_to_body(body) : body Gyoku.xml body_to_xml, :element_form_default => element_form_default, :namespace => namespace_identifier end def add_namespaces_to_body(hash, path = [input[1].to_s]) return unless hash return hash if hash.kind_of?(Array) return hash.to_s unless hash.kind_of? Hash hash.inject({}) do |newhash, (key, value)| camelcased_key = Gyoku::XMLKey.create(key) newpath = path + [camelcased_key] if used_namespaces[newpath] newhash.merge( "#{used_namespaces[newpath]}:#{camelcased_key}" => add_namespaces_to_body(value, types[newpath] ? [types[newpath]] : newpath) ) else add_namespaces_to_values(value, path) if key == :order! newhash.merge(key => value) end end end def add_namespace_to_input return input.compact unless used_namespaces[[input[1].to_s]] [used_namespaces[[input[1].to_s]], input[1], input[2]] end def add_namespaces_to_values(values, path) values.collect! { |value| camelcased_value = Gyoku::XMLKey.create(value) namespace_path = path + [camelcased_value.to_s] namespace = used_namespaces[namespace_path] "#{namespace.blank? ? '' : namespace + ":"}#{camelcased_value}" } end end end end