module Soaspec # Produce test content from a WSDL module WsdlGenerator include ExeHelpers # Generate from WSDL def generate_from_wsdl(options) auth_name, auth_password = enter_auth_details if options[:auth] == 'basic' @virtual = false savon_options = { wsdl: options[:wsdl] } savon_options[:basic_auth] = [auth_name, auth_password] if options[:auth] == 'basic' wsdl_doc = Savon.client(**savon_options).wsdl @wsdl_schemas = wsdl_doc.parser.schemas # Create basic project files create_files %w[Rakefile Gemfile README.md spec/spec_helper.rb], ignore_if_present: true create_file(filename: '.rspec') create_file(filename: '.travis.yml') if options[:ci] == 'travis' create_folder 'logs' create_file filename: "lib/#{options[:name].snakecase}.rb", content: class_content # Files according to WSDL wsdl_doc.operations.each do |operation, op_details| puts "Creating files for operation: #{operation}" @content = "default:\n" @use_camel_case = false puts 'Message params: ' + op_details.to_s # From namespace identifier, find namespace, and for that find schemaLocation xsd and use that to build request if op_details[:parameters] op_details[:parameters].each do |element, details| @use_camel_case = true unless /[[:upper:]]/.match(element.to_s[0]).nil? @content += " #{element.to_s.snakecase}: #{fill_in_field_from_type(details[:type])} # #{details[:type]} \n" # TODO: If details is a Hash need to loop again end end wsdl_to_yaml_for root_elements_for(op_details) params = [] params << 'convert_request_keys_to: :camelcase' if @use_camel_case params_string = params == [] ? '' : ', ' + params.join(', ') @class_params = "'#{camel_case(operation)}'#{params_string}" create_file(filename: "config/data/#{operation}.yml", content: @content) create_file(filename: "spec/#{operation}_spec.rb", content: generated_soap_spec_for(operation)) end end # Attempt to calculate values of enumeration by looking up type in Schema # @param [String] type Try out filling enumeration for type. Return Custom Type if can't be done def try_enum_for(type) raise "'@wsdl_schemas' must be defined" if @wsdl_schemas.nil? custom_type = @wsdl_schemas.xpath("//*[@name='#{type}']") if enumeration? custom_type prefix = custom_type.first.namespace.prefix enumerations = custom_type.xpath("//#{prefix}:enumeration") return 'Custom Type' if enumerations.empty? @enums_values = [] enumerations.each { |enum_value| @enums_values << "'#{enum_value['value']}'" } "~randomize [#{@enums_values.join(', ')}]" else 'Custom Type' end end # @param [Nokogiri::XML::NodeSet] type WSDL element type # @return [Boolean] Whether WSDL type is an enumeration def enumeration?(type) return false unless type.first !type.xpath("*/#{type.first.namespace.prefix}:enumeration").empty? end # Return value of string after a namespace # @param [String] string String to parse for part after namespace # @return [String] Part after the namespace, demonstrated by ':' def value_after_namespace(string) string.split(':').last end # Based on WSDL type return a valid value # @param [String] type Type without the WSDL # @return [Object] Value representing type to fill in def fill_in_field_from_type(type) case type when 'string' then options[:string_default] # 'test string' when 'int' then 2 when 'boolean' then true when 'double' then '1.5' else try_enum_for type end end # @param [Nokogiri::XML::Element] element Element to check # @return [Boolean] True if Nokogiri element is a complex type, that is, has a complexType element underneath itself def complex_type?(element) element.children.any? { |child| child.name == 'complexType' } end # @param [String, Symbol] underscore_separated Snakecase value to be converted to camel case # @return [String] CamelCased value def camel_case(underscore_separated) underscore_separated.to_s.split('_').collect(&:capitalize).join end # @param [Hash] op_details Hash with details from WSDL for an operation # @return [Nokogiri::XML::NodeSet] List of the root elements in the SOAP body def root_elements_for(op_details) raise "'@wsdl_schemas' must be defined" if @wsdl_schemas.nil? root_element = @wsdl_schemas.at_xpath("//*[@name='#{op_details[:input]}']") raise 'Operation has no input defined' if root_element.nil? schema_namespace = root_element.namespace.prefix root_type = root_element['type'] if root_type @wsdl_schemas.xpath("//*[@name='#{root_type.split(':').last}']//#{schema_namespace}:element") else return [] unless complex_type? root_element # Empty Array if there are no root elements complex_type = root_element.children.find { |c| c.name == 'complexType' } sequence = complex_type.children.find { |c| c.name == 'sequence' } sequence.xpath("#{schema_namespace}:element") end end # Adds documentation content for all children of XML element # @param [Nokogiri::XML::Element] element Type to document for # @param [Integer] depth How many times to iterate depth for def document_type_for(element, depth = 1) # raise "Too far deep for #{element}" unless depth < 6 return unless depth < 6 if complex_type? element complex_type = element.children.find { |c| c.name == 'complexType' } sequence = complex_type.children.find { |c| c.name == 'sequence' } sequence.children.select { |node| node.class == Nokogiri::XML::Element }.each do |sub_element| document_type_for sub_element, depth += 1 end else return "No type seen for #{element}, #{element.class}" unless element['type'] name = element['name'] type = value_after_namespace(element['type']) @use_camel_case = true unless /[[:upper:]]/.match(name[0]).nil? spaces = ' ' * depth @content += "#{spaces}#{name.snakecase}: #{fill_in_field_from_type(type)} # #{type} \n" end end # Makes a yaml string in a '@content' instance variable # @param [Nokogiri::XML::NodeSet] list List to convert to YAML def wsdl_to_yaml_for(list) raise "'@content' string must be set" if @content.nil? list.each { |element| document_type_for element } end # Prompt user for wsdl def ask_wsdl prompt = <<-WSDL_LOC Enter WSDL: WSDL_LOC print prompt.chop @wsdl = $stdin.gets.strip puts end # Prompt user for Service name for wsdl def name_of_wsdl prompt = <<-WSDL_NAME Enter what you would like to name WSDL (CamelCase): WSDL_NAME print prompt.chop @name = $stdin.gets.strip puts end # Prompt user to enter basic auth details # @return [Array] Array with Basic auth username and password entered def enter_auth_details prompt = <<-AUTH_PROMPT User Name: AUTH_PROMPT print prompt.chop auth_name = $stdin.gets.strip puts prompt = <<-PASSWORD User Password: PASSWORD print prompt.chop auth_password = $stdin.gets.strip puts [auth_name, auth_password] end end end