require 'open-uri'
require 'uri'
require 'cgi'
require 'nokogiri'
# TODO: inline builders, if they are only ever used in one place
class Builders
def initialize(xsd)
@xsd = xsd
@builders = {}
end
def add(type)
@builders[type] = false unless @builders[type]
end
def each
results = []
while builder = @builders.find { |builder,is_rendered| !is_rendered }
results << yield(@xsd.get_complex_type(builder[0]))
@builders[builder[0]] = true
end
results.join("")
end
end
class HandsoapGenerator < Rails::Generator::NamedBase
attr_reader :wsdl
def initialize(runtime_args, runtime_options = {})
super
# Wsdl argument is required.
usage if @args.empty?
@wsdl_uri = @args.shift
end
def banner
"WARNING: This generator is rather incomplete and buggy. Use at your own risk." +
"\n" + "Usage: #{$0} #{spec.name} name URI [options]" +
"\n" + " name Basename of the service class" +
"\n" + " URI URI of the WSDL to generate from"
end
def manifest
record do |m|
@wsdl = Handsoap::Wsdl.new(@wsdl_uri)
@wsdl.parse!
@xsd = Handsoap::XsdSpider.new(@wsdl_uri)
@xsd.process!
m.directory "app"
m.directory "app/models"
@builders = Builders.new(@xsd)
m.template "gateway.rbt", "app/models/#{file_name}_service.rb"
end
end
def builders
@builders
end
def render_build(context_name, message_type, varname = nil, indentation = ' ')
if varname.nil?
ruby_name = message_type.ruby_name
else
ruby_name = "#{varname}[:#{message_type.ruby_name}]"
end
# message_type.namespaces
if message_type.attribute?
"#{context_name}.set_attr " + '"' + message_type.name + '", ' + ruby_name
elsif message_type.boolean?
"#{context_name}.add " + '"' + message_type.name + '", bool_to_str(' + ruby_name + ')'
elsif message_type.primitive?
"#{context_name}.add " + '"' + message_type.name + '", ' + ruby_name
elsif message_type.list?
list_type = @xsd.get_complex_type(message_type.type)
builders.add(list_type.type)
# TODO: a naming conflict waiting to happen hereabout
# TODO: indentation
"#{varname}.each do |#{message_type.ruby_name}|" + "\n" + indentation +
" build_#{list_type.ruby_type}!(#{context_name}, #{message_type.ruby_name})" + "\n" + indentation +
"end"
else
builders.add(message_type.type)
"build_#{message_type.ruby_type}!(#{context_name}, " + ruby_name + ")"
end
end
end
module Handsoap
class Wsdl
attr_reader :uri, :soap_actions, :soap_ports, :target_namespace
def initialize(uri)
@uri = uri
end
def parse!
wsdl = Nokogiri.XML(Kernel.open(@uri).read)
@target_namespace = wsdl.namespaces['xmlns:tns'] || wsdl.namespaces['xmlns']
@soap_actions = []
@soap_ports = []
messages = {}
wsdl.xpath('//wsdl:message').each do |message|
message_name = message['name']
messages[message_name] = message.xpath('wsdl:part').map { |part| MessageType::Part.new(part['type'] || 'xs:element', part['name']) }
end
wsdl.xpath('//*[name()="soap:operation"]').each do |operation|
operation_name = operation.parent['name']
operation_spec = wsdl.xpath('//wsdl:operation[@name="' + operation_name + '"]').first
raise RuntimeError, "Couldn't find wsdl:operation node for #{operation_name}" if operation_spec.nil?
msg_type_in = operation_spec.xpath('./wsdl:input').first["message"]
raise RuntimeError, "Couldn't find wsdl:input node for #{operation_name}" if msg_type_in.nil?
raise RuntimeError, "Invalid message type #{msg_type_in} for #{operation_name}" if messages[msg_type_in].nil?
msg_type_out = operation_spec.xpath('./wsdl:output').first["message"]
raise RuntimeError, "Couldn't find wsdl:output node for #{operation_name}" if msg_type_out.nil?
raise RuntimeError, "Invalid message type #{msg_type_out} for #{operation_name}" if messages[msg_type_out].nil?
@soap_actions << SoapAction.new(operation, messages[msg_type_in], messages[msg_type_out])
end
raise RuntimeError, "Could not parse WSDL" if soap_actions.empty?
wsdl.xpath('//wsdl:port', {"xmlns:wsdl" => 'http://schemas.xmlsoap.org/wsdl/'}).each do |port|
name = port['name'].underscore
location = port.xpath('./*[@location]').first['location']
@soap_ports << { :name => name, :soap_name => port['name'], :location => location }
end
end
end
class SoapAction
attr_reader :input_type, :output_type
def initialize(xml_node, input_type, output_type)
@xml_node = xml_node
@input_type = input_type
@output_type = output_type
end
def name
@xml_node.parent['name'].underscore
end
def soap_name
@xml_node.parent['name']
end
def href
@xml_node['soapAction']
end
end
module MessageType
# complex-type is a spec (class), not an element ... (object)
#
#
# The element specifies a user
#
#
#
class ComplexType
def initialize(xml_node)
@xml_node = xml_node
end
def type
@xml_node['name']
end
def ruby_type
type.gsub(/^.*:/, "").underscore.gsub(/-/, '_')
end
def elements
@xml_node.xpath('./xs:attribute|./xs:all/xs:element|./xs:sequence').map do |node|
case
when node.node_name == 'attribute'
Attribute.new(node['type'], node['name'])
when node.node_name == 'element'
Element.new(node['type'], node['name'], []) # TODO: elements.elements
when node.node_name == 'sequence'
choice_node = node.xpath('./xs:choice').first
if choice_node
# TODO
Attribute.new('xs:choice', 'todo')
else
entity_node = node.xpath('./xs:element').first
Sequence.new(entity_node['type'], entity_node['name'])
end
else
puts node
raise "Unknown type #{node.node_name}"
end
end
end
end
class Base
attr_reader :type, :name
def initialize(type, name)
raise "'type' can't be nil" if type.nil?
raise "'name' can't be nil" if name.nil?
@type = type
@name = name
end
def ruby_type
type.gsub(/^.*:/, "").underscore.gsub(/-/, '_')
end
def ruby_name
name.underscore.gsub(/-/, '_')
end
def attribute?
false
end
def primitive?
/^xs:/.match type
end
def boolean?
type == "xs:boolean"
end
def list?
false
end
end
# Parts are shallow elements
#
class Part < Base
end
#
#
class Element < Base
attr_reader :elements
def initialize(type, name, elements = [])
super(type, name)
@elements = elements
end
end
#
class Attribute < Base
def primitive?
true
end
def attribute?
true
end
end
#
#
#
class Sequence < Base
def list?
true
end
end
end
end
module Handsoap
class XsdSpider
def initialize(uri)
@queue = []
@wsdl_uri = uri
end
def results
@queue.map { |element| element[:data] }
end
def get_complex_type(name)
# TODO namespace
short_name = name.gsub(/^.*:/, "")
results.each do |data|
search = data[:document].xpath('//xs:complexType[@name="' + short_name + '"]')
if search.any?
return MessageType::ComplexType.new(search.first)
end
end
raise "Didn't find '#{name}' (short name #{short_name})"
end
def process!
spider_href(@wsdl_uri, nil)
while process_next do end
end
private
def add_href(href, namespace)
unless @queue.find { |element| element[:href] == href }
@queue << { :href => href, :namespace => namespace, :state => :new, :data => {} }
end
end
def process_next
next_element = @queue.find { |element| element[:state] == :new }
if next_element
next_element[:data] = spider_href(next_element[:href], next_element[:namespace])
next_element[:state] = :done
return true
end
return false
end
def spider_href(href, namespace)
raise "'href' must be a String" if href.nil?
xsd = Nokogiri.XML(Kernel.open(href).read)
#
#
xsd.xpath('//*[@schemaLocation]').each do |inc|
add_href(inc['schemaLocation'], inc['namespace'] || namespace)
end
{ :document => xsd, :namespace => namespace }
end
end
end