require 'nokogiri' require 'cgi' module LolSoap # @private class WSDLParser class Node attr_reader :parser, :node, :target_namespace, :name, :prefix def initialize(parser, node, target_namespace) @parser = parser @node = node @target_namespace = target_namespace @prefix, @name = prefix_and_name(node.attr('name')) end def name_with_prefix "#{prefix}:#{name}" end def prefix_and_name(string) parser.prefix_and_name(string, target_namespace) end end class Element < Node def type if complex_type = node.at_xpath('xs:complexType', parser.ns) type = Type.new(parser, complex_type, target_namespace) { :elements => type.elements, :attributes => type.attributes } else prefix_and_name(node.attr('type').to_s).join(':') end end def singular max_occurs.empty? || max_occurs == '1' end private def max_occurs @max_occurs ||= node.attribute('maxOccurs').to_s end end class Type < Node def elements parent_elements.merge(own_elements) end def attributes parent_attributes + own_attributes end def base_type @base_type ||= begin if extension = node.at_xpath('*/xs:extension/@base', parser.ns) parser.type(extension.to_s) end end end private def own_elements Hash[ element_nodes.map do |element| [ element.name, { :name => element.name, :prefix => element.prefix, :type => element.type, :singular => element.singular } ] end ] end def element_nodes node.xpath('*/xs:element | */*/xs:element | xs:complexContent/xs:extension/*/xs:element | xs:complexContent/xs:extension/*/*/xs:element', parser.ns).map { |el| Element.new(parser, el, target_namespace) } end def parent_elements base_type ? base_type.elements : {} end def own_attributes node.xpath('xs:attribute/@name | */xs:extension/xs:attribute/@name', parser.ns).map(&:text) end def parent_attributes base_type ? base_type.attributes : [] end end SOAP_1_1 = 'http://schemas.xmlsoap.org/wsdl/soap/' SOAP_1_2 = 'http://schemas.xmlsoap.org/wsdl/soap12/' attr_reader :doc def self.parse(raw) new(Nokogiri::XML::Document.parse(raw)) end def initialize(doc) @doc = doc end def namespaces @namespaces ||= begin namespaces = Hash[doc.collect_namespaces.map { |k, v| [k.sub(/^xmlns:/, ''), v] }] namespaces.delete('xmlns') namespaces end end # We invert the hash in a deterministic way so that the results are repeatable. def prefixes @prefixes ||= Hash[namespaces.sort_by { |k, v| k }.uniq { |k, v| v }].invert end def endpoint @endpoint ||= CGI.unescape(doc.at_xpath('/d:definitions/d:service/d:port/s:address/@location', ns).to_s) end def schemas doc.xpath('/d:definitions/d:types/xs:schema', ns) end def types @types ||= begin types = {} each_node('xs:complexType[not(@abstract="true")]') do |node, target_ns| type = Type.new(self, node, target_ns) types[type.name_with_prefix] = { :name => type.name, :prefix => type.prefix, :elements => type.elements, :attributes => type.attributes } end types end end def type(name) name = prefix_and_name(name).last if node = doc.at_xpath("//xs:complexType[@name='#{name}']", ns) target_namespace = node.at_xpath('parent::xs:schema/@targetNamespace', ns).to_s Type.new(self, node, target_namespace) end end def elements @elements ||= begin elements = {} each_node('xs:element') do |node, target_ns| element = Element.new(self, node, target_ns) elements[element.name_with_prefix] = { :name => element.name, :prefix => element.prefix, :type => element.type } end elements end end def messages @messages ||= Hash[ doc.xpath('/d:definitions/d:message', ns).map do |msg| element = msg.at_xpath('./d:part/@element', ns).to_s [msg.attribute('name').to_s, prefix_and_name(element).join(':')] end ] end def port_type_operations @port_type_operations ||= Hash[ doc.xpath('/d:definitions/d:portType/d:operation', ns).map do |op| input = op.at_xpath('./d:input/@message', ns).to_s.split(':').last output = op.at_xpath('./d:output/@message', ns).to_s.split(':').last name = op.attribute('name').to_s [name, { :input => messages.fetch(input), :output => messages.fetch(output) }] end ] end def operations @operations ||= begin binding = doc.at_xpath('/d:definitions/d:service/d:port/s:address/../@binding', ns).to_s.split(':').last Hash[ doc.xpath("/d:definitions/d:binding[@name='#{binding}']/d:operation", ns).map do |op| name = op.attribute('name').to_s action = op.at_xpath('./s:operation/@soapAction', ns).to_s [ name, { :action => action, :input => port_type_operations.fetch(name)[:input], :output => port_type_operations.fetch(name)[:output] } ] end ] end end def soap_version @soap_version ||= namespaces.values.include?(SOAP_1_2) ? '1.2' : '1.1' end def ns @ns ||= { 'd' => 'http://schemas.xmlsoap.org/wsdl/', 'xs' => 'http://www.w3.org/2001/XMLSchema', 's' => soap_version == '1.2' ? SOAP_1_2 : SOAP_1_1 } end def prefix_and_name(prefixed_name, default_namespace = nil) prefix, name = prefixed_name.to_s.split(':') if name # Ensure we always use the same prefix for a given namespace prefix = prefixes.fetch(namespaces.fetch(prefix)) else name = prefix prefix = prefixes.fetch(default_namespace) end [prefix, name] end def each_node(xpath) schemas.each do |schema| target_namespace = schema.attr('targetNamespace').to_s schema.xpath(xpath, ns).each do |node| yield node, target_namespace end end end end end