require "builder"
require "gyoku"
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.
SchemaTypes = {
"xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
"xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
}
# Accepts an +endpoint+, an +input+ tag and a SOAP +body+.
def initialize(config, endpoint = nil, input = nil, body = nil)
self.config = config
self.endpoint = endpoint if endpoint
self.input = input if input
self.body = body if body
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
# 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
@xml ||= tag(builder, :Envelope, complete_namespaces) do |xml|
tag(xml, :Header) { xml << header_for_xml } unless header_for_xml.empty?
if input.nil?
tag(xml, :Body)
else
tag(xml, :Body) { 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 = SchemaTypes.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
Gyoku.xml add_namespaces_to_body(body), :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
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
end
end
end