├── .gitignore ├── DEPLOY.markdown ├── README.markdown ├── Rakefile ├── VERSION.yml ├── generators └── handsoap │ ├── USAGE │ ├── handsoap_generator.rb │ └── templates │ └── DUMMY ├── handsoap.gemspec ├── lib ├── handsoap.rb └── handsoap │ ├── compiler.rb │ ├── deferred.rb │ ├── http.rb │ ├── http │ ├── drivers.rb │ ├── drivers │ │ ├── abstract_driver.rb │ │ ├── curb_driver.rb │ │ ├── event_machine_driver.rb │ │ ├── http_client_driver.rb │ │ ├── mock_driver.rb │ │ └── net_http_driver.rb │ ├── part.rb │ ├── request.rb │ └── response.rb │ ├── parser.rb │ ├── service.rb │ ├── xml_mason.rb │ └── xml_query_front.rb └── tests ├── GoogleSearch-soapui-project.xml ├── Weather.wsdl ├── WeatherSummary.wsdl ├── account_test.rb ├── benchmark_integration_test.rb ├── dispatch_test.rb ├── event_machine_test.rb ├── fault_test.rb ├── handsoap_generator_test.rb ├── http_test.rb ├── httpauth_integration_test.rb ├── httpauth_server.rb ├── parser_test.rb ├── service_integration_test.rb ├── service_test.rb ├── socket_server.rb ├── wstf-sc002-soapui-project.xml ├── xml_mason_test.rb └── xml_query_front_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.log 3 | *.pid 4 | *.sqlite3 5 | *.tmproj 6 | .DS_Store 7 | log/* 8 | pkg/* 9 | tests/rails_root/* 10 | *.swp 11 | *.orig 12 | -------------------------------------------------------------------------------- /DEPLOY.markdown: -------------------------------------------------------------------------------- 1 | This is mostly a note to my self, so I don't forget it. 2 | 3 | To make a release, do: 4 | 5 | rake version:bump:patch 6 | rake release 7 | 8 | You need `jeweler` and `gemcutter`, as well as login credentials for gemcutter. 9 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Handsoap 2 | === 3 | 4 | Install 5 | --- 6 | 7 | gem sources -a http://gemcutter.org 8 | sudo gem install handsoap curb nokogiri 9 | 10 | What 11 | --- 12 | Handsoap is a library for creating SOAP clients in Ruby. 13 | 14 | [Watch a tutorial](http://www.vimeo.com/4813848), showing how to use Handsoap. The final application can be found at: [http://github.com/troelskn/handsoap-example/tree/master](http://github.com/troelskn/handsoap-example/tree/master) 15 | 16 | API docs are at [http://rdoc.info/projects/unwire/handsoap/](http://rdoc.info/projects/unwire/handsoap/) 17 | 18 | Some usage information is to be found in [the wiki](http://wiki.github.com/unwire/handsoap). 19 | 20 | ![Handsoap](http://ny-image0.etsy.com/il_430xN.68558416.jpg) 21 | 22 | Why 23 | --- 24 | 25 | Ruby already has a SOAP-client library, [soap4r](http://dev.ctor.org/soap4r), so why create another one? 26 | 27 | > Let me summarize SOAP4R: it smells like Java code built on a Monday morning by an EJB coder. 28 | > 29 | > -- [Ruby In Practice: REST, SOAP, WebSphere MQ and SalesForce](http://blog.labnotes.org/2008/01/28/ruby-in-practice-rest-soap-websphere-mq-and-salesforce/) 30 | 31 | OK, not entirely fair, but soap4r has problems. It's incomplete and buggy. If you try to use it for any real-world services, you quickly run into compatibility issues. You can get around some of them, if you have control over the service, but you may not always be that lucky. In the end, even if you get it working, it has a bulky un-Rubyish feel to it. 32 | 33 | Handsoap tries to do better by taking a minimalistic approach. Instead of a full abstraction layer, it is more like a toolbox with which you can write SOAP bindings. You could think of it as a [ffi](http://c2.com/cgi/wiki?ForeignFunctionInterface) targeting SOAP. 34 | 35 | This means that you generally need to do more manual labor in the cases where soap4r would have automated the mapping. It also means that you need to get your hands dirty with wsdl, xsd and other heavyweight specifications. However, it does give you some tools to help you stay sane. 36 | 37 | There are several benefits of using Handsoap: 38 | 39 | * It supports the entire SOAP specification, all versions (because you have to implement it your self). 40 | * You actually get a sporting chance to debug and fix protocol level bugs. 41 | * It's much faster than soap4r, because it uses fast low-level libraries for xml-parsing and http-communication. 42 | 43 | To summarise, soap4r takes an optimistic approach, where Handsoap expects things to fail. If soap4r works for you today, it's probably the better choice. If you find your self strugling with it, Handsoap will offer a more smooth ride. It won't magically fix things for you though. 44 | 45 | Handsoap vs. soap4r benchmark 46 | --- 47 | 48 | Benchmarks are always unfair, but my experiments has placed Handsoap at being approximately double as fast as soap4r. I'd love any suggestions for a more precise measure. 49 | 50 | $ ruby tests/benchmark_test.rb 1000 51 | Benchmarking 1000 calls ... 52 | user system total real 53 | handsoap 0.750000 0.090000 0.840000 ( 1.992437) 54 | soap4r 2.240000 0.140000 2.380000 ( 3.605836) 55 | --------------- 56 | Legend: 57 | The user CPU time, system CPU time, the sum of the user and system CPU times, 58 | and the elapsed real time. The unit of time is seconds. 59 | 60 | SOAP basics 61 | --- 62 | 63 | SOAP is a protocol that is tunneled through XML over HTTP. Apart from using the technology for transportation, it doesn't have much to do with HTTP. Some times, it hasn't even got much to do with XML either. 64 | 65 | A SOAP client basically consists of three parts: 66 | 67 | * A http-connectivity layer, 68 | * a mechanism for marshalling native data types to XML, 69 | * and a mechanism for unmarshalling XML to native data types. 70 | 71 | The protocol also contains a large and unwieldy specification of how to do the (un)marshalling, which can be used as the basis for automatically mapping to a rich type model. This makes the protocol fitting for .net/Java, but is a huge overhead for a very dynamically typed language such as Ruby. Much of the complexity of clients such as soap4r, is in the parts that tries to use this specification. Handsoap expects you to manually write the code that marshals/unmarshals, thereby bypassing this complexity (or rather - pass it to the programmer) 72 | 73 | Handsoap only supports RPC-style SOAP. This seems to be the most common style. It's probably possible to add support for Document-style with little effort, but until I see the need I'm not going there. 74 | 75 | API documentation 76 | --- 77 | 78 | In addition to this guide, there's autogenerated API documentation available at [http://rdoc.info/projects/unwire/handsoap/](http://rdoc.info/projects/unwire/handsoap/) 79 | 80 | Getting started 81 | --- 82 | 83 | For getting started with Handsoap, you should read [the guide in the wiki](http://wiki.github.com/unwire/handsoap/recommendations). 84 | 85 | The toolbox 86 | --- 87 | 88 | The Handsoap toolbox consists of the following components. 89 | 90 | Handsoap can use either [curb](http://curb.rubyforge.org/), [Net::HTTP](http://www.ruby-doc.org/stdlib/libdoc/net/http/rdoc/index.html) or [httpclient](http://dev.ctor.org/http-access2) for HTTP-connectivity. The former is recommended, and default, but for portability you might choose one of the latter. You usually don't need to interact at the HTTP-level, but if you do (for example, if you have to use SSL), you can do so through a thin abstraction layer. 91 | 92 | For parsing XML, Handsoap defaults to use [Nokogiri](http://github.com/tenderlove/nokogiri/tree/master). Handsoap has an abstraction layer, so that you can switch between REXML, Nokogiri and ruby-libxml. Besides providing portability between these parsers, Handsoap also gives some helper functions that are meaningful when parsing SOAP envelopes. 93 | 94 | Finally, there is a library for generating XML, which you'll use when mapping from Ruby to SOAP. It's quite similar to [Builder](http://builder.rubyforge.org/), but is tailored towards being used for writing SOAP-messages. The name of this library is `XmlMason` and it is included/part of Handsoap. 95 | 96 | Maintainers & Contributors 97 | --- 98 | 99 | Handsoap is maintained by [Unwire A/S](http://www.unwire.dk), namely [Troels Knak-Nielsen](http://github.com/troelskn) and [Jimmi Westerberg](http://github.com/jimmiw), with the help of many other contributors. 100 | 101 | Use the git command below to see a list of them all. (GIT command was found at formtastic) 102 | 103 | git shortlog -n -s --no-merges 104 | 105 | License 106 | --- 107 | 108 | Copyright: [Unwire A/S](http://www.unwire.dk), 2009 109 | 110 | License: [Creative Commons Attribution 2.5 Denmark License](http://creativecommons.org/licenses/by/2.5/dk/deed.en_GB) 111 | or: [LGPL 3](http://www.gnu.org/copyleft/lesser.html) 112 | ___ 113 | 114 | troelskn@gmail.com - April, 2009 115 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | begin 3 | require 'jeweler' 4 | #require 'rubygems' 5 | #gem :jeweler 6 | Jeweler::Tasks.new do |gemspec| 7 | gemspec.name = "handsoap" 8 | gemspec.summary = "Handsoap is a library for creating SOAP clients in Ruby" 9 | gemspec.email = ["troelskn@gmail.com","frontend@unwire.dk"] 10 | gemspec.homepage = "http://github.com/unwire/handsoap" 11 | gemspec.description = gemspec.summary 12 | gemspec.authors = ["Troels Knak-Nielsen", "Jimmi Westerberg"] 13 | gemspec.requirements << "You need to install either \"curb\" or \"httpclient\", using one of:\n gem install curb\n gem install httpclient" 14 | gemspec.requirements << "It is recommended that you install either \"nokogiri\" or \"libxml-ruby\"" 15 | gemspec.files = FileList['lib/**/*.rb', 'generators/handsoap/templates', 'generators/**/*', '[A-Z]*.*'].to_a 16 | end 17 | Jeweler::GemcutterTasks.new 18 | rescue LoadError => err 19 | puts "Jeweler not available. Install it with: sudo gem install jeweler" 20 | p err 21 | end 22 | 23 | desc "Generates API documentation" 24 | task :rdoc do 25 | sh "rm -rf doc && rdoc lib" 26 | end 27 | 28 | require 'rake/testtask' 29 | namespace :test do 30 | desc "Remove temporary files generated by test case" 31 | task :cleanup do 32 | puts "Clearing temporary files" 33 | sh "rm -rf tests/rails_root" 34 | end 35 | Rake::TestTask.new :test do |test| 36 | # Rake::Task['test:cleanup'].invoke 37 | test.test_files = FileList.new('tests/**/*_test.rb') do |list| 38 | list.exclude 'tests/benchmark_integration_test.rb' 39 | list.exclude 'tests/service_integration_test.rb' 40 | list.exclude 'tests/httpauth_integration_test.rb' 41 | end 42 | test.libs << 'tests' 43 | test.verbose = true 44 | end 45 | end 46 | 47 | desc "Run tests and clean up afterwards" 48 | task :test => ["test:cleanup", "test:test", "test:cleanup"] 49 | 50 | task :default => [:test] 51 | 52 | -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 1 3 | :minor: 4 4 | :patch: 0 5 | :build: 6 | -------------------------------------------------------------------------------- /generators/handsoap/USAGE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unwire/handsoap/d5e3c7d4f1cd7559da947898e5464cba12587852/generators/handsoap/USAGE -------------------------------------------------------------------------------- /generators/handsoap/handsoap_generator.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require "#{File.dirname(__FILE__)}/../../lib/handsoap/parser.rb" 3 | require "#{File.dirname(__FILE__)}/../../lib/handsoap/compiler.rb" 4 | require 'pathname' 5 | 6 | # TODO 7 | # options: 8 | # soap_actions (true/false) 9 | # soap_version (1/2/auto) 10 | # basename 11 | class HandsoapGenerator < Rails::Generator::Base 12 | def initialize(runtime_args, runtime_options = {}) 13 | super 14 | # Wsdl argument is required. 15 | usage if @args.empty? 16 | @wsdl_uri = @args.shift 17 | @basename = @args.shift 18 | end 19 | 20 | # def add_options!(opt) 21 | # opt.on('--soap-actions') { |value| options[:soap_actions] = true } 22 | # opt.on('--no-soap-actions') { |value| options[:soap_actions] = false } 23 | # end 24 | 25 | def banner 26 | "Generates the scaffold for a Handsoap binding." + 27 | "\n" + "You still have to fill in most of the meat, but this gives you a head start." + 28 | "\n" + "Usage: #{$0} #{spec.name} URI [BASENAME] [OPTIONS]" + 29 | "\n" + " URI URI of the WSDL to generate from" + 30 | "\n" + " BASENAME The basename to use for the service. If omitted, the name will be deducted from the URL." + 31 | # "\n" + 32 | # "\n" + "The following options are available:" + 33 | # "\n" + " --soap-actions If set, stubs will be generated with soap-action parameters. (Default)" + 34 | # "\n" + " --no-soap-actions If set, stubs will be generated without soap-action parameters." + 35 | # "\n" + " --soap-version-1 If set, the generator will look for SOAP v 1.1 endpoints." + 36 | # "\n" + " --soap-version-2 If set, the generator will look for SOAP v 1.2 endpoints." + 37 | "" 38 | end 39 | 40 | def manifest 41 | wsdl = Handsoap::Parser::Wsdl.read(@wsdl_uri) 42 | compiler = Handsoap::Compiler.new(wsdl, @basename) 43 | protocol = wsdl.preferred_protocol 44 | file_name = compiler.service_basename 45 | record do |m| 46 | m.directory "app" 47 | m.directory "app/models" 48 | m.file_contents "app/models/#{file_name}_service.rb" do |file| 49 | file.write compiler.compile_service(protocol, :soap_actions) 50 | end 51 | m.directory "test" 52 | m.directory "test/integration" 53 | m.file_contents "test/integration/#{file_name}_service_test.rb" do |file| 54 | file.write compiler.compile_test(protocol) 55 | end 56 | # TODO 57 | # Ask user about which endpoints to use ? 58 | m.message do |out| 59 | out.puts "----" 60 | out.puts "Endpoints in WSDL" 61 | out.puts " You should copy these to the appropriate environment files." 62 | out.puts " (Eg. `config/environments/*.rb`)" 63 | out.puts "----" 64 | out.puts compiler.compile_endpoints(protocol) 65 | out.puts "----" 66 | end 67 | end 68 | end 69 | 70 | end 71 | 72 | module Handsoap #:nodoc: 73 | module Generator #:nodoc: 74 | module Commands #:nodoc: 75 | module Create 76 | def file_contents(relative_destination, &block) 77 | destination = destination_path(relative_destination) 78 | temp_file = Tempfile.new("handsoap_generator") 79 | canonical_path = Pathname.new(source_path("/.")).realpath.to_s 80 | temp_file_relative_path = relative_path(temp_file.path, canonical_path) 81 | begin 82 | yield temp_file 83 | temp_file.close 84 | return self.file(temp_file_relative_path, relative_destination) 85 | ensure 86 | temp_file.unlink 87 | end 88 | end 89 | 90 | def message(&block) 91 | yield $stdout unless logger.quiet 92 | end 93 | 94 | private 95 | 96 | # Convert the given absolute path into a path 97 | # relative to the second given absolute path. 98 | # http://www.justskins.com/forums/file-relative-path-handling-97116.html 99 | def relative_path(abspath, relative_to) 100 | path = abspath.split(File::SEPARATOR) 101 | rel = relative_to.split(File::SEPARATOR) 102 | while (path.length > 0) && (path.first == rel.first) 103 | path.shift 104 | rel.shift 105 | end 106 | ('..' + File::SEPARATOR) * rel.length + path.join(File::SEPARATOR) 107 | end 108 | end 109 | end 110 | end 111 | end 112 | 113 | Rails::Generator::Commands::Create.send :include, Handsoap::Generator::Commands::Create 114 | -------------------------------------------------------------------------------- /generators/handsoap/templates/DUMMY: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unwire/handsoap/d5e3c7d4f1cd7559da947898e5464cba12587852/generators/handsoap/templates/DUMMY -------------------------------------------------------------------------------- /handsoap.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "handsoap" 8 | s.version = "1.4.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Troels Knak-Nielsen", "Jimmi Westerberg"] 12 | s.date = "2013-11-06" 13 | s.description = "Handsoap is a library for creating SOAP clients in Ruby" 14 | s.email = ["troelskn@gmail.com", "frontend@unwire.dk"] 15 | s.extra_rdoc_files = [ 16 | "README.markdown" 17 | ] 18 | s.license = 'LGPL-3' 19 | s.files = [ 20 | "DEPLOY.markdown", 21 | "README.markdown", 22 | "VERSION.yml", 23 | "generators/handsoap/USAGE", 24 | "generators/handsoap/handsoap_generator.rb", 25 | "generators/handsoap/templates/DUMMY", 26 | "lib/handsoap.rb", 27 | "lib/handsoap/compiler.rb", 28 | "lib/handsoap/deferred.rb", 29 | "lib/handsoap/http.rb", 30 | "lib/handsoap/http/drivers.rb", 31 | "lib/handsoap/http/drivers/abstract_driver.rb", 32 | "lib/handsoap/http/drivers/curb_driver.rb", 33 | "lib/handsoap/http/drivers/event_machine_driver.rb", 34 | "lib/handsoap/http/drivers/http_client_driver.rb", 35 | "lib/handsoap/http/drivers/mock_driver.rb", 36 | "lib/handsoap/http/drivers/net_http_driver.rb", 37 | "lib/handsoap/http/part.rb", 38 | "lib/handsoap/http/request.rb", 39 | "lib/handsoap/http/response.rb", 40 | "lib/handsoap/parser.rb", 41 | "lib/handsoap/service.rb", 42 | "lib/handsoap/xml_mason.rb", 43 | "lib/handsoap/xml_query_front.rb" 44 | ] 45 | s.homepage = "http://github.com/unwire/handsoap" 46 | s.require_paths = ["lib"] 47 | s.requirements = ["You need to install either \"curb\" or \"httpclient\", using one of:\n gem install curb\n gem install httpclient", "It is recommended that you install either \"nokogiri\" or \"libxml-ruby\""] 48 | s.rubygems_version = "2.0.7" 49 | s.summary = "Handsoap is a library for creating SOAP clients in Ruby" 50 | end 51 | 52 | -------------------------------------------------------------------------------- /lib/handsoap.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'handsoap/xml_mason' 3 | require 'handsoap/xml_query_front' 4 | require 'handsoap/service' 5 | require 'bigdecimal' 6 | -------------------------------------------------------------------------------- /lib/handsoap/compiler.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | module Handsoap 3 | # Used internally to generate Ruby source code 4 | class CodeWriter #:nodoc: all 5 | 6 | def initialize 7 | @buffer = "" 8 | @indentation = 0 9 | end 10 | 11 | def begin(text) 12 | puts(text) 13 | indent 14 | end 15 | 16 | def end(str = "end") 17 | unindent 18 | puts(str) 19 | end 20 | 21 | def puts(text = "") 22 | @buffer << text.gsub(/^(.*)$/, (" " * @indentation) + "\\1") 23 | @buffer << "\n" # unless @buffer.match(/\n$/) 24 | end 25 | 26 | def indent 27 | @indentation = @indentation + 1 28 | end 29 | 30 | def unindent 31 | @indentation = @indentation - 1 32 | end 33 | 34 | def to_s 35 | @buffer 36 | end 37 | end 38 | 39 | # Used internally by the generator to generate a Service stub. 40 | class Compiler #:nodoc: all 41 | 42 | def initialize(wsdl, basename = nil) 43 | @wsdl = wsdl 44 | if basename 45 | @basename = basename.gsub(/[^a-zA-Z0-9]/, "_").gsub(/_+/, "_").gsub(/(^_+|_+$)/, '') 46 | else 47 | @basename = @wsdl.service 48 | end 49 | @basename = underscore(@basename).gsub(/_service$/, "") 50 | end 51 | 52 | def write 53 | writer = CodeWriter.new 54 | yield writer 55 | writer.to_s 56 | end 57 | 58 | def underscore(camel_cased_word) 59 | camel_cased_word.to_s.gsub(/::/, '/'). 60 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). 61 | gsub(/([a-z\d])([A-Z])/,'\1_\2'). 62 | tr("-", "_"). 63 | downcase 64 | end 65 | 66 | def camelize(lower_case_and_underscored_word) 67 | lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { 68 | "::" + $1.upcase 69 | }.gsub(/(^|_)(.)/) { 70 | $2.upcase 71 | } 72 | end 73 | 74 | def method_name(operation) 75 | if operation.name.match /^(get|find|select|fetch)/i 76 | "#{underscore(operation.name)}" 77 | else 78 | "#{underscore(operation.name)}!" 79 | end 80 | end 81 | 82 | def service_basename 83 | @basename 84 | end 85 | 86 | def service_name 87 | camelize(service_basename) + "Service" 88 | end 89 | 90 | def endpoint_name 91 | "#{service_basename.upcase}_SERVICE_ENDPOINT" 92 | end 93 | 94 | def detect_protocol 95 | if endpoints.select { |endpoint| endpoint.protocol == :soap12 }.any? 96 | :soap12 97 | elsif endpoints.select { |endpoint| endpoint.protocol == :soap11 }.any? 98 | :soap11 99 | else 100 | raise "Can't find any soap 1.1 or soap 1.2 endpoints" 101 | end 102 | end 103 | 104 | def compile_endpoints(protocol) 105 | version = protocol == :soap12 ? 2 : 1 106 | @wsdl.endpoints.select { |endpoint| endpoint.protocol == protocol }.map do |endpoint| 107 | write do |w| 108 | w.puts "# wsdl: #{@wsdl.url}" 109 | w.begin "#{endpoint_name} = {" 110 | w.puts ":uri => '#{endpoint.url}'," 111 | w.puts ":version => #{version}" 112 | w.end "}" 113 | end 114 | end 115 | end 116 | 117 | def compile_service(protocol, *options) 118 | binding = @wsdl.bindings.find { |b| b.protocol == protocol } 119 | raise "Can't find binding for requested protocol (#{protocol})" unless binding 120 | write do |w| 121 | w.puts "# -*- coding: utf-8 -*-" 122 | w.puts "require 'handsoap'" 123 | w.puts 124 | w.begin "class #{service_name} < Handsoap::Service" 125 | w.puts "endpoint #{endpoint_name}" 126 | w.begin "def on_create_document(doc)" 127 | w.puts "# register namespaces for the request" 128 | w.puts "doc.alias 'tns', '#{@wsdl.target_ns}'" 129 | w.end 130 | w.puts 131 | w.begin "def on_response_document(doc)" 132 | w.puts "# register namespaces for the response" 133 | w.puts "doc.add_namespace 'ns', '#{@wsdl.target_ns}'" 134 | w.end 135 | w.puts 136 | w.puts "# public methods" 137 | @wsdl.interface.operations.each do |operation| 138 | action = binding.actions.find { |a| a.name == operation.name } 139 | raise "Can't find action for operation #{operation.name}" unless action 140 | w.puts 141 | w.begin "def #{method_name(operation)}" 142 | # TODO allow :soap_action => :none 143 | if operation.name != action.soap_action && options.include?(:soap_actions) 144 | w.puts "soap_action = '#{action.soap_action}'" 145 | maybe_soap_action = ", soap_action" 146 | else 147 | maybe_soap_action = "" 148 | end 149 | w.begin((operation.output ? 'response = ' : '') + "invoke('tns:#{operation.name}'#{maybe_soap_action}) do |message|") 150 | w.puts 'raise "TODO"' 151 | w.end 152 | w.end 153 | end 154 | w.puts 155 | w.puts "private" 156 | w.puts "# helpers" 157 | w.puts "# TODO" 158 | w.end 159 | end 160 | end 161 | 162 | def compile_test(protocol) 163 | binding = @wsdl.bindings.find { |b| b.protocol == protocol } 164 | raise "Can't find binding for requested protocol (#{protocol})" unless binding 165 | write do |w| 166 | w.puts "# -*- coding: utf-8 -*-" 167 | w.puts "require 'test_helper'" 168 | w.puts 169 | w.puts "# #{service_name}.logger = $stdout" 170 | w.puts 171 | w.begin "class #{service_name}Test < Test::Unit::TestCase" 172 | @wsdl.interface.operations.each do |operation| 173 | w.puts 174 | w.begin "def test_#{underscore(operation.name)}" 175 | w.puts "result = #{service_name}.#{method_name(operation)}" 176 | w.end 177 | end 178 | w.end 179 | end 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/handsoap/deferred.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Handsoap 4 | class Deferred 5 | def initialize 6 | @callback = nil 7 | @callback_cache = nil 8 | @errback = nil 9 | @errback_cache = nil 10 | end 11 | def has_callback? 12 | !! @callback 13 | end 14 | def has_errback? 15 | !! @errback 16 | end 17 | def callback(&block) 18 | raise "Already assigned a block for callback" if @callback 19 | @callback = block 20 | if @callback_cache 21 | payload = @callback_cache 22 | trigger_callback(*payload) 23 | end 24 | self 25 | end 26 | def errback(&block) 27 | raise "Already assigned a block for errback" if @errback 28 | @errback = block 29 | if @errback_cache 30 | payload = @errback_cache 31 | trigger_errback(*payload) 32 | end 33 | self 34 | end 35 | def trigger_callback(*args) 36 | if @callback 37 | @callback.call(*args) 38 | else 39 | @callback_cache = args 40 | end 41 | end 42 | def trigger_errback(*args) 43 | if @errback 44 | @errback.call(*args) 45 | else 46 | @errback_cache = args 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/handsoap/http.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'handsoap/http/request' 3 | require 'handsoap/http/response' 4 | require 'handsoap/http/drivers' 5 | -------------------------------------------------------------------------------- /lib/handsoap/http/drivers.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'handsoap/http/drivers/abstract_driver' 3 | require 'handsoap/http/drivers/curb_driver' 4 | require 'handsoap/http/drivers/event_machine_driver' 5 | require 'handsoap/http/drivers/http_client_driver' 6 | require 'handsoap/http/drivers/net_http_driver' 7 | require 'handsoap/http/drivers/mock_driver' 8 | 9 | module Handsoap 10 | module Http 11 | @@drivers = { 12 | :curb => Drivers::CurbDriver, 13 | :em => Drivers::EventMachineDriver, 14 | :event_machine => Drivers::EventMachineDriver, 15 | :httpclient => Drivers::HttpClientDriver, 16 | :http_client => Drivers::HttpClientDriver, 17 | :net_http => Drivers::NetHttpDriver, 18 | } 19 | 20 | def self.drivers 21 | @@drivers 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/handsoap/http/drivers/abstract_driver.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Handsoap 4 | module Http 5 | module Drivers 6 | class AbstractDriver 7 | def self.load! 8 | end 9 | 10 | def initialize 11 | self.class.load! 12 | end 13 | 14 | # Parses a raw http response into a +Response+ or +Part+ object. 15 | def parse_http_part(headers, body, status = nil, content_type = nil) 16 | if headers.kind_of? String 17 | headers = parse_headers(headers) 18 | end 19 | headers = headers.inject({}) {|collect,item| collect[item[0].downcase] = item[1]; collect } 20 | if content_type.nil? && headers['content-type'] 21 | content_type = headers['content-type'].first 22 | end 23 | boundary = parse_multipart_boundary(content_type) 24 | parts = if boundary 25 | parse_multipart(boundary, body).map {|raw_part| parse_http_part(raw_part[:head], raw_part[:body]) } 26 | end 27 | if status.nil? 28 | Handsoap::Http::Part.new(headers, body, parts) 29 | else 30 | Handsoap::Http::Response.new(status, headers, body, parts) 31 | end 32 | end 33 | 34 | # Content-Type header string -> mime-boundary | nil 35 | def parse_multipart_boundary(content_type) 36 | if %r|\Amultipart.*boundary=\"?([^\";,]+)\"?|n.match(content_type) 37 | $1.dup 38 | end 39 | end 40 | 41 | # Parses a multipart http-response body into parts. 42 | # +boundary+ is a string of the boundary token. 43 | # +content_io+ is either a string or an IO. If it's an IO, then content_length must be specified. 44 | # +content_length+ (optional) is an integer, specifying the length of +content_io+ 45 | # 46 | # This code is lifted from cgi.rb 47 | # 48 | def parse_multipart(boundary, content_io, content_length = nil) 49 | if content_io.kind_of? String 50 | content_length = content_io.length 51 | content_io = StringIO.new(content_io, 'r') 52 | elsif !(content_io.kind_of? IO) || content_length.nil? 53 | raise "Second argument must be String or IO with content_length" 54 | end 55 | 56 | boundary = "--" + boundary 57 | quoted_boundary = Regexp.quote(boundary, "n") 58 | buf = "" 59 | bufsize = 10 * 1024 60 | boundary_end = "" 61 | 62 | # start multipart/form-data 63 | content_io.binmode if defined? content_io.binmode 64 | boundary_size = boundary.size + "\r\n".size 65 | content_length -= boundary_size 66 | status = content_io.read(boundary_size) 67 | 68 | if nil == status 69 | raise EOFError, "no content body" 70 | elsif "\r\n" + boundary == status 71 | extra = content_io.read("\r\n".size) 72 | unless extra == "\r\n" 73 | raise EOFError, "parse error while reading boundary" 74 | end 75 | elsif boundary + "\r\n" != status 76 | raise EOFError, "bad content body" 77 | end 78 | 79 | parts = [] 80 | 81 | loop do 82 | head = nil 83 | if 10240 < content_length 84 | require "tempfile" 85 | body = Tempfile.new("Handsoap") 86 | else 87 | begin 88 | require "stringio" 89 | body = StringIO.new 90 | rescue LoadError 91 | require "tempfile" 92 | body = Tempfile.new("Handsoap") 93 | end 94 | end 95 | body.binmode if defined? body.binmode 96 | 97 | until head and /#{quoted_boundary}(?:\r\n|--)/n.match(buf) 98 | 99 | if (not head) and /\r\n\r\n/n.match(buf) 100 | buf = buf.sub(/\A((?:.|\n)*?\r\n)\r\n/n) do 101 | head = $1.dup 102 | "" 103 | end 104 | next 105 | end 106 | 107 | if head and ( ("\r\n" + boundary + "\r\n").size < buf.size ) 108 | body.print buf[0 ... (buf.size - ("\r\n" + boundary + "\r\n").size)] 109 | buf[0 ... (buf.size - ("\r\n" + boundary + "\r\n").size)] = "" 110 | end 111 | 112 | c = if bufsize < content_length 113 | content_io.read(bufsize) 114 | else 115 | content_io.read(content_length) 116 | end 117 | if c.nil? || c.empty? 118 | raise EOFError, "bad content body" 119 | end 120 | buf.concat(c) 121 | content_length -= c.size 122 | end 123 | 124 | buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do 125 | body.print $1 126 | if "--" == $2 127 | content_length = -1 128 | end 129 | boundary_end = $2.dup 130 | "" 131 | end 132 | 133 | body.rewind 134 | parts << {:head => head, :body => body.read(body.size)} 135 | 136 | break if buf.size == 0 137 | break if content_length == -1 138 | end 139 | raise EOFError, "bad boundary end of body part" unless boundary_end =~ /--/ 140 | parts 141 | end 142 | 143 | # lifted from webrick/httputils.rb 144 | def parse_headers(raw) 145 | header = Hash.new([].freeze) 146 | field = nil 147 | tmp = raw.gsub(/^(\r\n)+|(\r\n)+$/, '') 148 | (tmp.respond_to?(:lines) ? tmp.lines : tmp).each {|line| 149 | case line 150 | when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om 151 | field, value = $1, $2 152 | field.downcase! 153 | header[field] = [] unless header.has_key?(field) 154 | header[field] << value 155 | when /^\s+(.*?)\s*\z/om 156 | value = $1 157 | unless field 158 | raise "bad header '#{line.inspect}'." 159 | end 160 | header[field][-1] << " " << value 161 | else 162 | raise "bad header '#{line.inspect}'." 163 | end 164 | } 165 | header.each {|key, values| 166 | values.each {|value| 167 | value.strip! 168 | value.gsub!(/\s+/, " ") 169 | } 170 | } 171 | header 172 | end 173 | end 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/handsoap/http/drivers/curb_driver.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'handsoap/http/drivers/abstract_driver' 3 | 4 | module Handsoap 5 | module Http 6 | module Drivers 7 | class CurbDriver < AbstractDriver 8 | attr_accessor :enable_cookies 9 | 10 | def initialize 11 | @enable_cookies = false 12 | end 13 | 14 | def self.load! 15 | require 'curb' 16 | end 17 | 18 | def get_curl(url) 19 | if @curl 20 | @curl.url = url 21 | else 22 | @curl = ::Curl::Easy.new(url) 23 | @curl.timeout = Handsoap.timeout 24 | @curl.enable_cookies = @enable_cookies 25 | 26 | # enables both deflate and gzip compression of responses 27 | @curl.encoding = '' 28 | 29 | if Handsoap.follow_redirects? 30 | @curl.follow_location = true 31 | @curl.max_redirects = Handsoap.max_redirects 32 | end 33 | end 34 | @curl 35 | end 36 | 37 | private :get_curl 38 | 39 | def send_http_request(request) 40 | http_client = get_curl(request.url) 41 | # Set credentials. The driver will negotiate the actual scheme 42 | if request.username && request.password 43 | http_client.userpwd = [request.username, ":", request.password].join 44 | end 45 | http_client.cacert = request.trust_ca_file if request.trust_ca_file 46 | http_client.cert = request.client_cert_file if request.client_cert_file 47 | # I have submitted a patch for this to curb, but it's not yet supported. If you get errors, try upgrading curb. 48 | http_client.cert_key = request.client_cert_key_file if request.client_cert_key_file 49 | # pack headers 50 | headers = request.headers.inject([]) do |arr, (k,v)| 51 | arr + v.map {|x| "#{k}: #{x}" } 52 | end 53 | http_client.headers = headers 54 | # I don't think put/delete is actually supported .. 55 | case request.http_method 56 | when :get 57 | http_client.http_get 58 | when :post 59 | http_client.http_post(request.body) 60 | when :put 61 | http_client.http_put(request.body) 62 | when :delete 63 | http_client.http_delete 64 | else 65 | raise "Unsupported request method #{request.http_method}" 66 | end 67 | parse_http_part(http_client.header_str.gsub(/^HTTP.*\r\n/, ""), http_client.body_str, http_client.response_code, http_client.content_type) 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/handsoap/http/drivers/event_machine_driver.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Handsoap 4 | module Http 5 | module Drivers 6 | class EventMachineDriver < AbstractDriver 7 | def self.load! 8 | require 'eventmachine' 9 | require 'em-http' 10 | end 11 | 12 | def send_http_request_async(request) 13 | emr = EventMachine::HttpRequest.new(request.url) 14 | 15 | if request.username && request.password 16 | # TODO: Verify that this is actually supported? 17 | request.headers['authorization'] = [request.username, request.password] 18 | end 19 | 20 | case request.http_method 21 | when :get 22 | emdef = emr.get(:head => request.headers) 23 | when :post 24 | emdef = emr.post(:head => request.headers, :body => request.body) 25 | when :put 26 | emdef = emr.put(:head => request.headers, :body => request.body) 27 | when :delete 28 | emdef = emr.delete 29 | else 30 | raise "Unsupported request method #{request.http_method}" 31 | end 32 | 33 | deferred = Handsoap::Deferred.new 34 | emdef.callback do 35 | http_response = parse_http_part(emdef.response_header, emdef.response, emdef.response_header.status) 36 | deferred.trigger_callback http_response 37 | end 38 | emdef.errback do 39 | deferred.trigger_errback emdef 40 | end 41 | deferred 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/handsoap/http/drivers/http_client_driver.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'handsoap/http/drivers/abstract_driver' 3 | 4 | module Handsoap 5 | module Http 6 | module Drivers 7 | class HttpClientDriver < AbstractDriver 8 | def self.load! 9 | require 'httpclient' 10 | end 11 | 12 | def send_http_request(request) 13 | http_client = HTTPClient.new 14 | # Set credentials. The driver will negotiate the actual scheme 15 | if request.username && request.password 16 | domain = request.url.match(/^(http(s?):\/\/[^\/]+\/)/)[1] 17 | http_client.set_auth(domain, request.username, request.password) 18 | end 19 | http_client.ssl_config.set_trust_ca(request.trust_ca_file) if request.trust_ca_file 20 | http_client.ssl_config.set_client_cert_file(request.client_cert_file,request.client_cert_key_file) if request.client_cert_file and request.client_cert_key_file 21 | http_client.ssl_config.verify_mode = request.ssl_verify_mode if request.ssl_verify_mode 22 | # pack headers 23 | headers = request.headers.inject([]) do |arr, (k,v)| 24 | arr + v.map {|x| [k,x] } 25 | end 26 | response = http_client.request(request.http_method, request.url, nil, request.body, headers) 27 | response_headers = response.header.all.inject({}) do |h, (k, v)| 28 | k.downcase! 29 | if h[k].nil? 30 | h[k] = [v] 31 | else 32 | h[k] << v 33 | end 34 | h 35 | end 36 | parse_http_part(response_headers, response.content, response.status, response.contenttype) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/handsoap/http/drivers/mock_driver.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'handsoap/http/drivers/abstract_driver' 3 | 4 | module Handsoap 5 | module Http 6 | module Drivers 7 | # A mock driver for your testing needs. 8 | # 9 | # To use it, create a new instance and assign to +Handsoap::Http.drivers+. Then configure +Handsoap::Service+ to use it: 10 | # 11 | # Handsoap::Http.drivers[:mock] = Handsoap::Http::Drivers::MockDriver.new :status => 200, :headers => headers, :content => body 12 | # Handsoap.http_driver = :mock 13 | # 14 | # Remember that headers should use \r\n, rather than \n. 15 | class MockDriver < AbstractDriver 16 | attr_accessor :mock, :last_request, :is_loaded 17 | 18 | def initialize(mock) 19 | @mock = mock 20 | @is_loaded = false 21 | end 22 | 23 | def load! 24 | is_loaded = true 25 | end 26 | 27 | def new 28 | self 29 | end 30 | 31 | def send_http_request(request) 32 | @last_request = request 33 | (mock.kind_of? Hash) ? 34 | parse_http_part(mock[:headers], mock[:content], mock[:status], mock[:content_type]) : mock 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/handsoap/http/drivers/net_http_driver.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Handsoap 4 | module Http 5 | module Drivers 6 | class NetHttpDriver < AbstractDriver 7 | def self.load! 8 | require 'net/http' 9 | require 'uri' 10 | end 11 | 12 | def send_http_request(request) 13 | url = request.url 14 | unless url.kind_of? ::URI::Generic 15 | url = ::URI.parse(url) 16 | end 17 | 18 | path = url.request_uri 19 | 20 | http_request = case request.http_method 21 | when :get 22 | Net::HTTP::Get.new(path) 23 | when :post 24 | Net::HTTP::Post.new(path) 25 | when :put 26 | Net::HTTP::Put.new(path) 27 | when :delete 28 | Net::HTTP::Delete.new(path) 29 | else 30 | raise "Unsupported request method #{request.http_method}" 31 | end 32 | 33 | http_client = Net::HTTP.new(url.host, url.port) 34 | 35 | #http_client.read_timeout = 120 36 | http_client.open_timeout = Handsoap.timeout 37 | http_client.read_timeout = Handsoap.timeout 38 | 39 | http_client.use_ssl = true if url.scheme == 'https' 40 | 41 | if request.username && request.password 42 | # TODO: http://codesnippets.joyent.com/posts/show/1075 43 | http_request.basic_auth request.username, request.password 44 | end 45 | request.headers.each do |k, values| 46 | values.each do |v| 47 | http_request.add_field(k, v) 48 | end 49 | end 50 | http_request.body = request.body 51 | # require 'stringio' 52 | # debug_output = StringIO.new 53 | # http_client.set_debug_output debug_output 54 | http_response = http_client.start do |client| 55 | # requesting gzipped response if server can provide it 56 | http_request['Accept-Encoding'] = 'gzip' 57 | client.request(http_request) 58 | end 59 | # puts debug_output.string 60 | # hacky-wacky 61 | def http_response.get_headers 62 | @header.inject({}) do |h, (k, v)| 63 | h[k.downcase] = v 64 | h 65 | end 66 | end 67 | # net/http only supports basic auth. We raise a warning if the server requires something else. 68 | if http_response.code == 401 && http_response.get_headers['www-authenticate'] 69 | auth_type = http_response.get_headers['www-authenticate'].chomp.match(/\w+/)[0].downcase 70 | if auth_type != "basic" 71 | raise "Authentication type #{auth_type} is unsupported by net/http" 72 | end 73 | end 74 | 75 | # http://stackoverflow.com/questions/13397119/ruby-nethttp-not-decoding-gzip 76 | body =\ 77 | begin 78 | Zlib::GzipReader.new(StringIO.new(http_response.body)).read 79 | rescue Zlib::GzipFile::Error, Zlib::Error # Not gzipped 80 | http_response.body 81 | end 82 | parse_http_part(http_response.get_headers, body, http_response.code) 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/handsoap/http/part.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Handsoap 4 | module Http 5 | 6 | # Represents a HTTP Part. 7 | # For simple HTTP-requests there is only one part, which is the response. 8 | class Part 9 | attr_reader :headers, :body, :parts 10 | 11 | def initialize(headers, body, parts = nil) 12 | @headers = headers 13 | @body = body 14 | @parts = parts 15 | end 16 | 17 | # Returns a header. 18 | # Returns String | Array | nil 19 | def [](key) 20 | key.to_s.downcase! 21 | (@headers[key] && @headers[key].length == 1) ? @headers[key].first : @headers[key] 22 | end 23 | 24 | # Returns the mime-type part of the content-type header 25 | def mime_type 26 | @headers['content-type'].first.match(/^[^;]+/).to_s if @headers['content-type'] 27 | end 28 | 29 | # Returns the charset part of the content-type header 30 | def charset 31 | if @headers['content-type'] 32 | match_data = @headers['content-type'].first.match(/^[^;]+; charset=([^;]+)/) 33 | if match_data 34 | match_data[1].to_s 35 | end 36 | end 37 | end 38 | 39 | def multipart? 40 | !! @parts 41 | end 42 | 43 | def inspect(&block) 44 | str = inspect_head 45 | if headers.any? 46 | str << headers.map { |key,values| values.map {|value| normalize_header_key(key) + ": " + value + "\n" }.join("") }.join("") 47 | end 48 | if body 49 | if multipart? 50 | if block_given? 51 | str << parts.map{|part| part.inspect(&block) }.join("") 52 | else 53 | str << parts.map{|part| part.inspect }.join("") 54 | end 55 | elsif body 56 | str << "---\n" 57 | if block_given? 58 | str << yield(body) 59 | else 60 | str << body 61 | end 62 | str << "\n---" 63 | end 64 | end 65 | end 66 | 67 | private 68 | 69 | def inspect_head 70 | "--- Part ---\n" 71 | end 72 | 73 | def normalize_header_key(key) 74 | key.split("-").map{|s| s.downcase.capitalize }.join("-") 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/handsoap/http/request.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Handsoap 4 | module Http 5 | 6 | # Represents a HTTP Request. 7 | class Request 8 | attr_reader :url, :http_method, :headers, :body, :username, :password, :trust_ca_file, :client_cert_file, :client_cert_key_file,:ssl_verify_mode 9 | attr_writer :body, :http_method 10 | def initialize(url, http_method = :get) 11 | @url = url 12 | @http_method = http_method 13 | @headers = {} 14 | @body = nil 15 | @username = nil 16 | @password = nil 17 | @trust_ca_file = nil 18 | @client_cert_file = nil 19 | @client_cert_key_file = nil 20 | @ssl_verify_mode = nil 21 | end 22 | def set_trust_ca_file(ca_file) 23 | @trust_ca_file = ca_file 24 | end 25 | def set_client_cert_files(client_cert_file,client_cert_key_file) 26 | @client_cert_file = client_cert_file 27 | @client_cert_key_file = client_cert_key_file 28 | end 29 | def set_ssl_verify_mode(mode) 30 | @ssl_verify_mode = mode 31 | end 32 | def set_auth(username, password) 33 | @username = username 34 | @password = password 35 | end 36 | def add_header(key, value) 37 | if @headers[key].nil? 38 | @headers[key] = [] 39 | end 40 | @headers[key] << value 41 | end 42 | def set_header(key, value) 43 | if value.nil? 44 | @headers[key] = nil 45 | else 46 | @headers[key] = [value] 47 | end 48 | end 49 | def inspect 50 | "===============\n" + 51 | "--- Request ---\n" + 52 | "#{http_method.to_s.upcase} #{url}\n" + 53 | ( 54 | if username && password 55 | "Auth credentials: #{username}:#{password}\n" 56 | else 57 | "" 58 | end 59 | ) + 60 | ( 61 | if headers.any? 62 | "---\n" + headers.map { |key,values| values.map {|value| key + ": " + value + "\n" }.join("") }.join("") 63 | else 64 | "" 65 | end 66 | ) + 67 | ( 68 | if body 69 | "---\n" + body 70 | else 71 | "" 72 | end 73 | ) 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/handsoap/http/response.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'handsoap/http/part' 3 | 4 | module Handsoap 5 | module Http 6 | 7 | # Represents a HTTP Response. 8 | class Response < Part 9 | attr_reader :status 10 | def initialize(status, headers, body, parts = nil) 11 | @status = status.to_i 12 | super(headers, body, parts) 13 | end 14 | def primary_part 15 | # Strictly speaking, the main part doesn't need to be first, but until proven otherwise, we'll just assume that. 16 | if multipart? 17 | parts.first 18 | else 19 | self 20 | end 21 | end 22 | private 23 | def inspect_head 24 | "--- Response ---\n" + "HTTP Status: #{status}\n" 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/handsoap/parser.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'httpclient' 3 | require 'openssl' 4 | require 'nokogiri' 5 | 6 | module Handsoap 7 | # Classes for parsing a WSDL. 8 | # 9 | # Used internally by the generator. 10 | module Parser #:nodoc: all 11 | class Interface 12 | attr_accessor :name, :operations 13 | 14 | def initialize(name, operations = []) 15 | @name = name 16 | @operations = operations || [] 17 | end 18 | end 19 | 20 | class Binding 21 | attr_accessor :name, :protocol, :interface, :transport, :style, :encoding, :verb, :actions 22 | 23 | def initialize(name, optional = {}) 24 | @name = name 25 | @actions = optional[:actions] || [] 26 | @protocol = optional[:protocol] 27 | @interface = optional[:interface] 28 | @transport = optional[:transport] 29 | @style = optional[:style] 30 | @encoding = optional[:encoding] 31 | @verb = optional[:verb] 32 | end 33 | end 34 | 35 | class Endpoint 36 | attr_accessor :name, :protocol, :binding, :url 37 | 38 | def initialize(name, protocol, binding, url) 39 | @name = name 40 | @protocol = protocol 41 | @binding = binding 42 | @url = url 43 | end 44 | end 45 | 46 | class Operation 47 | attr_accessor :name, :input, :output 48 | 49 | def initialize(name, optional = {}) 50 | @name = name 51 | @input = optional[:input] 52 | @output = optional[:output] 53 | end 54 | end 55 | 56 | class Action 57 | attr_accessor :name, :soap_action, :location 58 | 59 | def initialize(name, optional = {}) 60 | @name = name 61 | @soap_action = optional[:soap_action] 62 | @location = optional[:location] 63 | end 64 | end 65 | 66 | class Wsdl 67 | attr_reader :url 68 | 69 | def initialize(doc, url = "void://") 70 | @doc = doc 71 | @url = url 72 | end 73 | 74 | def self.read(url) 75 | if url =~ /^http(s?):/ 76 | request = ::HTTPClient.new 77 | request.ssl_config.verify_mode = ::OpenSSL::SSL::VERIFY_NONE 78 | response = request.get(url) 79 | xml_src = response.content 80 | else 81 | xml_src = Kernel.open(url).read 82 | end 83 | self.new(Nokogiri.XML(xml_src), url) 84 | end 85 | 86 | def ns 87 | { 88 | 'wsdl1' => "http://schemas.xmlsoap.org/wsdl/", 89 | 'wsdl2' => "http://www.w3.org/ns/wsdl/", 90 | 'soap11' => "http://schemas.xmlsoap.org/wsdl/soap/", 91 | 'soap12' => "http://schemas.xmlsoap.org/wsdl/soap12/", 92 | 'http' => "http://schemas.xmlsoap.org/wsdl/http/" 93 | } 94 | end 95 | private :ns 96 | 97 | def protocol_from_ns(node) 98 | href = node.namespace.respond_to?(:href) ? node.namespace.href : @doc.namespaces["xmlns:#{node.namespace}"] 99 | case href 100 | when "http://schemas.xmlsoap.org/wsdl/soap/" 101 | :soap11 102 | when "http://schemas.xmlsoap.org/wsdl/soap12/" 103 | :soap12 104 | when "http://schemas.xmlsoap.org/wsdl/http/" 105 | :http 106 | else 107 | raise "Unknown namespace '#{href}'" 108 | end 109 | end 110 | private :protocol_from_ns 111 | 112 | def is_wsdl2?(node) 113 | href = node.namespace.respond_to?(:href) ? node.namespace.href : @doc.namespaces["xmlns:#{node.namespace}"] 114 | case href 115 | when "http://schemas.xmlsoap.org/wsdl/" 116 | false 117 | when "http://www.w3.org/ns/wsdl/" 118 | true 119 | else 120 | raise "Unknown namespace '#{href}'" 121 | end 122 | end 123 | private :is_wsdl2? 124 | 125 | def service 126 | services = @doc.xpath("//wsdl1:service|//wsdl2:service", ns) 127 | raise "Expected exactly 1 service in WSDL" if services.length != 1 128 | services[0][:name] 129 | end 130 | 131 | def interface 132 | all_interfaces = self.interfaces 133 | if all_interfaces.length != 1 134 | # There are more than one portType, so we take a pick 135 | all_bindings = self.bindings 136 | all_interfaces.each do |interface| 137 | b = all_bindings.find {|binding| binding.name == interface.name } 138 | if [:soap11, :soap12].include? b.protocol 139 | return interface 140 | end 141 | end 142 | raise "Can't find a suitable soap 1.1 or 1.2 interface/portType in WSDL" 143 | end 144 | all_interfaces.first 145 | end 146 | 147 | def target_ns 148 | @doc.root[:targetNamespace] || raise("Attribute targetNamespace not defined") 149 | end 150 | 151 | def preferred_protocol 152 | e = endpoints 153 | if e.select { |endpoint| endpoint.protocol == :soap12 }.any? 154 | :soap12 155 | elsif e.select { |endpoint| endpoint.protocol == :soap11 }.any? 156 | :soap11 157 | else 158 | raise "Can't find any soap 1.1 or soap 1.2 endpoints" 159 | end 160 | end 161 | 162 | def interfaces 163 | @doc.xpath("//wsdl1:portType|//wsdl2:interface", ns).map do |port_type| 164 | operations = port_type.xpath("./wsdl1:operation|./wsdl2:operation", ns).map do |operation| 165 | if is_wsdl2?(operation) 166 | input_node = operation.xpath("./wsdl2:input", ns).first 167 | input = input_node ? input_node[:element] : nil 168 | output_node = operation.xpath("./wsdl2:output", ns).first 169 | output = output_node ? output_node[:element] : nil 170 | else 171 | input_node = operation.xpath("./wsdl1:input", ns).first 172 | input = input_node ? input_node[:message] : nil 173 | output_node = operation.xpath("./wsdl1:output", ns).first 174 | output = output_node ? output_node[:message] : nil 175 | end 176 | Operation.new(operation[:name], :input => input, :output => output) 177 | end 178 | Interface.new(port_type[:name], operations) 179 | end 180 | end 181 | 182 | def endpoints 183 | @doc.xpath("//wsdl1:service/wsdl1:port|//wsdl2:service/wsdl2:endpoint", ns).map do |port| 184 | binding = port[:binding] 185 | if is_wsdl2?(port) 186 | location = port[:address] 187 | protocol = :binding 188 | else 189 | address = port.xpath("./soap11:address|./soap12:address|./http:address", ns).first 190 | location = address[:location] 191 | protocol = protocol_from_ns(address) 192 | end 193 | Endpoint.new(port[:name], protocol, binding, location) 194 | end 195 | end 196 | 197 | def bindings 198 | @doc.xpath("//wsdl1:binding|//wsdl2:binding", ns).map do |binding| 199 | raise "WSDL 2.0 not supported" if is_wsdl2?(binding) 200 | soap_binding = binding.xpath("./soap11:binding|./soap12:binding|./http:binding", ns).first 201 | protocol = protocol_from_ns(soap_binding) 202 | actions = [] 203 | style = nil 204 | encoding = nil 205 | actions = binding.xpath("./wsdl1:operation", ns).map do |operation| 206 | soap_operation = operation.xpath("./soap11:operation|./soap12:operation|./http:operation", ns).first 207 | if soap_operation[:style] 208 | raise "Mixed styles not supported" if style && style != soap_operation[:style] 209 | style = soap_operation[:style] 210 | end 211 | xquery = [] 212 | ['soap11', 'soap12', 'http'].each do |version| 213 | ['input', 'output'].each do |message_name| 214 | ['header', 'body'].each do |part_name| 215 | xquery << "./wsdl1:#{message_name}/#{version}:#{part_name}" 216 | end 217 | end 218 | end 219 | operation.xpath(xquery.join('|'), ns).each do |thing| 220 | raise "Mixed encodings not supported" if encoding && encoding != thing[:use] 221 | encoding = thing[:use] 222 | end 223 | Action.new( 224 | operation[:name], 225 | :soap_action => soap_operation[:soapAction], 226 | :location => soap_operation[:location]) 227 | end 228 | Binding.new( 229 | binding[:name], 230 | :protocol => protocol, 231 | :interface => binding[:type], 232 | :transport => soap_binding[:transport], 233 | :style => style, 234 | :encoding => encoding, 235 | :verb => soap_binding[:verb], 236 | :actions => actions) 237 | end 238 | end 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /lib/handsoap/service.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'time' 3 | require 'handsoap/xml_mason' 4 | require 'handsoap/xml_query_front' 5 | require 'handsoap/http' 6 | require 'handsoap/deferred' 7 | 8 | module Handsoap 9 | 10 | def self.store_raw_response=(boolean) 11 | @store_raw_response = boolean 12 | end 13 | 14 | def self.store_raw_response? 15 | !!@store_raw_response 16 | end 17 | 18 | def self.http_driver 19 | @http_driver || (self.http_driver = :curb) 20 | end 21 | 22 | def self.http_driver=(driver) 23 | @http_driver = driver 24 | Handsoap::Http.drivers[driver].load! 25 | return driver 26 | end 27 | 28 | def self.xml_query_driver 29 | @xml_query_driver || (self.xml_query_driver = :nokogiri) 30 | end 31 | 32 | def self.xml_query_driver=(driver) 33 | @xml_query_driver = Handsoap::XmlQueryFront.load_driver!(driver) 34 | end 35 | 36 | # Sets the timeout 37 | def self.timeout=(timeout) 38 | @timeout = timeout 39 | end 40 | 41 | # fetches the timeout 42 | # the default timeout is set to 60seconds 43 | def self.timeout 44 | @timeout || (self.timeout = 60) 45 | end 46 | 47 | # Tell Handsoap to follow redirects 48 | def self.follow_redirects! 49 | @follow_redirects = true 50 | end 51 | 52 | # Check whether Handsoap should follow redirects 53 | def self.follow_redirects? 54 | @follow_redirects || false 55 | end 56 | 57 | # Sets the max number of redirects 58 | def self.max_redirects=(max_redirects) 59 | @max_redirects = max_redirects 60 | end 61 | 62 | # Fetches the max number of redirects 63 | # The default is 1 64 | def self.max_redirects 65 | @max_redirects || (self.max_redirects = 1) 66 | end 67 | 68 | # Wraps SOAP errors in a standard class. 69 | class Fault < StandardError 70 | attr_reader :code, :reason, :details 71 | 72 | def initialize(code, reason, details) 73 | @code = code 74 | @reason = reason 75 | @details = details 76 | end 77 | 78 | def to_s 79 | "Handsoap::Fault { :code => '#{@code}', :reason => '#{@reason}' }" 80 | end 81 | 82 | def self.from_xml(node, options = { :namespace => nil }) 83 | if not options[:namespace] 84 | raise "Missing option :namespace" 85 | end 86 | 87 | ns = { 'env' => options[:namespace] } 88 | 89 | # tries to find SOAP1.2 fault code 90 | fault_code = node.xpath("./env:Code/env:Value", ns).to_s 91 | 92 | # if no SOAP1.2 fault code was found, try the SOAP1.1 way 93 | unless fault_code 94 | fault_code = node.xpath('./faultcode', ns).to_s 95 | 96 | # if fault_code is blank, add the namespace and try again 97 | unless fault_code 98 | fault_code = node.xpath("//env:faultcode", ns).to_s 99 | end 100 | end 101 | 102 | # tries to find SOAP1.2 reason 103 | reason = node.xpath("./env:Reason/env:Text[1]", ns).to_s 104 | 105 | # if no SOAP1.2 faultstring was found, try the SOAP1.1 way 106 | unless reason 107 | reason = node.xpath('./faultstring', ns).to_s 108 | 109 | # if reason is blank, add the namespace and try again 110 | unless reason 111 | reason = node.xpath("//env:faultstring", ns).to_s 112 | end 113 | end 114 | 115 | details = node.xpath('./detail/*', ns) 116 | self.new(fault_code, reason, details) 117 | end 118 | end 119 | 120 | class HttpError < StandardError 121 | attr_reader :response 122 | def initialize(response) 123 | @response = response 124 | super() 125 | end 126 | end 127 | 128 | class SoapResponse 129 | 130 | attr_reader :document, :http_response, :raw_xml 131 | 132 | def initialize(document, http_response, raw_xml=nil) 133 | @document = document 134 | @http_response = http_response 135 | @raw_xml = raw_xml 136 | end 137 | 138 | def method_missing(method, *args, &block) 139 | if @document.respond_to?(method) 140 | @document.__send__ method, *args, &block 141 | else 142 | super 143 | end 144 | end 145 | 146 | end 147 | 148 | class AsyncDispatch 149 | attr_reader :action, :options, :request_block, :response_block 150 | def request(action, options = { :soap_action => :auto }, &block) 151 | @action = action 152 | @options = options 153 | @request_block = block 154 | end 155 | def response(&block) 156 | @response_block = block 157 | end 158 | end 159 | 160 | class Service 161 | @@logger = nil 162 | def self.logger=(io) 163 | @@logger = io 164 | end 165 | # Sets the endpoint for the service. 166 | # Arguments: 167 | # :uri => endpoint uri of the service. Required. 168 | # :version => 1 | 2 169 | # :envelope_namespace => Namespace of SOAP-envelope 170 | # :request_content_type => Content-Type of HTTP request. 171 | # You must supply either :version or both :envelope_namspace and :request_content_type. 172 | # :version is simply a shortcut for default values. 173 | def self.endpoint(args = {}) 174 | @uri = args[:uri] || raise("Missing option :uri") 175 | if args[:version] 176 | soap_namespace = { 1 => 'http://schemas.xmlsoap.org/soap/envelope/', 2 => 'http://www.w3.org/2003/05/soap-envelope' } 177 | raise("Unknown protocol version '#{@protocol_version.inspect}'") if soap_namespace[args[:version]].nil? 178 | @envelope_namespace = soap_namespace[args[:version]] 179 | @request_content_type = args[:version] == 1 ? "text/xml" : "application/soap+xml" 180 | end 181 | @envelope_namespace = args[:envelope_namespace] unless args[:envelope_namespace].nil? 182 | @request_content_type = args[:request_content_type] unless args[:request_content_type].nil? 183 | if @envelope_namespace.nil? || @request_content_type.nil? 184 | raise("Missing option :envelope_namespace, :request_content_type or :version") 185 | end 186 | end 187 | def self.envelope_namespace 188 | @envelope_namespace 189 | end 190 | def self.request_content_type 191 | @request_content_type 192 | end 193 | def self.uri 194 | @uri 195 | end 196 | @@instance = {} 197 | def self.instance 198 | @@instance[self.to_s] ||= self.new 199 | end 200 | def self.method_missing(method, *args, &block) 201 | if instance.respond_to?(method) 202 | instance.__send__ method, *args, &block 203 | else 204 | super 205 | end 206 | end 207 | def envelope_namespace 208 | self.class.envelope_namespace 209 | end 210 | def request_content_type 211 | self.class.request_content_type 212 | end 213 | def uri 214 | self.class.uri 215 | end 216 | def http_driver_instance 217 | Handsoap::Http.drivers[Handsoap.http_driver].new 218 | end 219 | # Creates an XML document and sends it over HTTP. 220 | # 221 | # +action+ is the QName of the rootnode of the envelope. 222 | # 223 | # +options+ currently takes one option +:soap_action+, which can be one of: 224 | # 225 | # :auto sends a SOAPAction http header, deduced from the action name. (This is the default) 226 | # 227 | # +String+ sends a SOAPAction http header. 228 | # 229 | # +nil+ sends no SOAPAction http header. 230 | def invoke(action, options = { :soap_action => :auto, :http_options => nil }, &block) # :yields: Handsoap::XmlMason::Element 231 | if action 232 | if options.kind_of? String 233 | options = { :soap_action => options } 234 | end 235 | if options[:soap_action] == :auto 236 | options[:soap_action] = action.gsub(/^.+:/, "") 237 | elsif options[:soap_action] == :none 238 | options[:soap_action] = nil 239 | end 240 | doc = make_envelope do |body,header| 241 | if options[:soap_header] 242 | iterate_hash_array(header, options[:soap_header]) 243 | end 244 | 245 | if options[:soap_body] 246 | action_hash = { action => options[:soap_body] } 247 | iterate_hash_array(body, action_hash) 248 | else 249 | body.add(action) 250 | end 251 | end 252 | if block_given? 253 | yield doc.find(action) 254 | end 255 | # ready to dispatch 256 | headers = { 257 | "Content-Type" => "#{self.request_content_type}; charset=UTF-8" 258 | } 259 | headers["SOAPAction"] = options[:soap_action] unless options[:soap_action].nil? 260 | on_before_dispatch(doc) 261 | request = make_http_request(self.uri, doc.to_s, headers, options[:http_options]) 262 | response = http_driver_instance.send_http_request(request) 263 | parse_http_response(response) 264 | end 265 | end 266 | 267 | 268 | 269 | # Async invocation 270 | # 271 | # Creates an XML document and sends it over HTTP. 272 | # 273 | # +user_block+ Block from userland 274 | def async(user_block, &block) # :yields: Handsoap::AsyncDispatch 275 | # Setup userland handlers 276 | userland = Handsoap::Deferred.new 277 | user_block.call(userland) 278 | raise "Missing :callback" unless userland.has_callback? 279 | raise "Missing :errback" unless userland.has_errback? 280 | # Setup service level handlers 281 | dispatcher = Handsoap::AsyncDispatch.new 282 | yield dispatcher 283 | raise "Missing :request_block" unless dispatcher.request_block 284 | raise "Missing :response_block" unless dispatcher.response_block 285 | # Done with the external configuration .. let's roll 286 | action = dispatcher.action 287 | options = dispatcher.options 288 | if action #TODO: What if no action ?!? 289 | if options.kind_of? String 290 | options = { :soap_action => options } 291 | end 292 | if options[:soap_action] == :auto 293 | options[:soap_action] = action.gsub(/^.+:/, "") 294 | elsif options[:soap_action] == :none 295 | options[:soap_action] = nil 296 | end 297 | doc = make_envelope do |body,header| 298 | if options[:soap_header] 299 | iterate_hash_array(header, options[:soap_header]) 300 | end 301 | 302 | if options[:soap_body] 303 | action_hash = { action => options[:soap_body] } 304 | iterate_hash_array(body, action_hash) 305 | else 306 | body.add(action) 307 | end 308 | end 309 | dispatcher.request_block.call doc.find(action) 310 | # ready to dispatch 311 | headers = { 312 | "Content-Type" => "#{self.request_content_type}; charset=UTF-8" 313 | } 314 | headers["SOAPAction"] = options[:soap_action] unless options[:soap_action].nil? 315 | on_before_dispatch(doc) 316 | request = make_http_request(self.uri, doc.to_s, headers) 317 | driver = self.http_driver_instance 318 | if driver.respond_to? :send_http_request_async 319 | deferred = driver.send_http_request_async(request) 320 | else 321 | # Fake async for sync-only drivers 322 | deferred = Handsoap::Deferred.new 323 | begin 324 | deferred.trigger_callback driver.send_http_request(request) 325 | rescue 326 | deferred.trigger_errback $! 327 | end 328 | end 329 | deferred.callback do |http_response| 330 | begin 331 | # Parse response 332 | response_document = parse_http_response(http_response) 333 | # Transform response 334 | result = dispatcher.response_block.call(response_document) 335 | # Yield to userland code 336 | userland.trigger_callback(result) 337 | rescue 338 | userland.trigger_errback $! 339 | end 340 | end 341 | # Pass driver level errors on 342 | deferred.errback do |ex| 343 | userland.trigger_errback(ex) 344 | end 345 | end 346 | return nil 347 | end 348 | 349 | 350 | 351 | #Used to iterate over a Hash, that can include Hash, Array or String/Float/Integer etc and insert it in the correct element. 352 | def iterate_hash_array(element, hash_array) 353 | hash_array.each {|hash| iterate_hash_array(element, hash) } if hash_array.is_a?(Array) 354 | hash_array.each do |name,value| 355 | if value.is_a?(Hash) 356 | element.add(name){|subelement| iterate_hash_array(subelement, value)} 357 | elsif value.is_a?(Array) 358 | element.add(name) do |subelement| 359 | value.each do |item| 360 | iterate_hash_array(subelement, item) if item.is_a?(Hash) 361 | end 362 | end 363 | else 364 | element.add name, value.to_s 365 | end 366 | end 367 | end 368 | 369 | # Hook that is called when a new request document is created. 370 | # 371 | # You can override this to add namespaces and other elements that are common to all requests (Such as authentication). 372 | def on_create_document(doc) 373 | end 374 | # Hook that is called before the message is dispatched. 375 | # 376 | # You can override this to provide filtering and logging. 377 | def on_before_dispatch(doc) 378 | end 379 | # Hook that is called after the http_client is created. 380 | # 381 | # You can override this to customize the http_client 382 | def on_after_create_http_request(http_request) 383 | end 384 | # Hook that is called when there is a response. 385 | # 386 | # You can override this to register common namespaces, useful for parsing the document. 387 | def on_response_document(doc) 388 | end 389 | # Hook that is called if there is a HTTP level error. 390 | # 391 | # Default behaviour is to raise an error. 392 | def on_http_error(response) 393 | raise HttpError, response 394 | end 395 | # Hook that is called if the dispatch returns a +Fault+. 396 | # 397 | # Default behaviour is to raise the Fault, but you can override this to provide logging and more fine-grained handling faults. 398 | # 399 | # See also: parse_soap_fault 400 | def on_fault(fault) 401 | raise fault 402 | end 403 | # Hook that is called if the response does not contain a valid SOAP enevlope. 404 | # 405 | # Default behaviour is to raise an error 406 | # 407 | # Note that if your service has operations that are one-way, you shouldn't raise an error here. 408 | # This is however a fairly exotic case, so that is why the default behaviour is to raise an error. 409 | def on_missing_document(response) 410 | raise "The response is not a valid SOAP envelope" 411 | end 412 | 413 | def debug(message = nil) #:nodoc: 414 | if @@logger 415 | if message 416 | @@logger.puts(message) 417 | end 418 | if block_given? 419 | yield @@logger 420 | end 421 | end 422 | end 423 | 424 | def make_http_request(uri, post_body, headers, http_options=nil) 425 | request = Handsoap::Http::Request.new(uri, :post) 426 | 427 | # SSL CA AND CLIENT CERTIFICATES 428 | if http_options 429 | request.set_trust_ca_file(http_options[:trust_ca_file]) if http_options[:trust_ca_file] 430 | request.set_client_cert_files(http_options[:client_cert_file], http_options[:client_cert_key_file]) if http_options[:client_cert_file] && http_options[:client_cert_key_file] 431 | request.set_ssl_verify_mode(http_options[:ssl_verify_mode]) if http_options[:ssl_verify_mode] 432 | end 433 | 434 | headers.each do |key, value| 435 | request.add_header(key, value) 436 | end 437 | request.body = post_body 438 | debug do |logger| 439 | logger.puts request.inspect 440 | end 441 | on_after_create_http_request(request) 442 | request 443 | end 444 | 445 | # Start the parsing pipe-line. 446 | # There are various stages and hooks for each, so that you can override those in your service classes. 447 | def parse_http_response(response) 448 | debug do |logger| 449 | logger.puts(response.inspect do |body| 450 | Handsoap.pretty_format_envelope(body.force_encoding('utf-8')).chomp 451 | end) 452 | end 453 | raw_xml_document = response.primary_part.body.force_encoding('utf-8') 454 | xml_document = parse_soap_response_document(raw_xml_document) 455 | soap_fault = parse_soap_fault(xml_document) 456 | # Is the response a soap-fault? 457 | unless soap_fault.nil? 458 | return on_fault(soap_fault) 459 | end 460 | # Does the http-status indicate an error? 461 | if response.status >= 400 462 | return on_http_error(response) 463 | end 464 | # Does the response contain a valid xml-document? 465 | if xml_document.nil? 466 | return on_missing_document(response) 467 | end 468 | # Everything seems in order. 469 | on_response_document(xml_document) 470 | args = [xml_document, response] 471 | args << raw_xml_document if Handsoap.store_raw_response? 472 | return SoapResponse.new(*args) 473 | end 474 | 475 | # Creates a standard SOAP envelope and yields the +Body+ element. 476 | def make_envelope # :yields: Handsoap::XmlMason::Element 477 | doc = XmlMason::Document.new do |doc| 478 | doc.alias 'env', self.envelope_namespace 479 | doc.add "env:Envelope" do |env| 480 | env.add "*:Header" 481 | env.add "*:Body" 482 | end 483 | end 484 | self.class.fire_on_create_document doc # deprecated .. use instance method 485 | on_create_document(doc) 486 | if block_given? 487 | yield doc.find("Body"),doc.find("Header") 488 | end 489 | return doc 490 | end 491 | 492 | # String -> [XmlDocument | nil] 493 | def parse_soap_response_document(http_body) 494 | begin 495 | Handsoap::XmlQueryFront.parse_string(http_body, Handsoap.xml_query_driver) 496 | rescue Handsoap::XmlQueryFront::ParseError => ex 497 | nil 498 | end 499 | end 500 | 501 | # XmlDocument -> [Fault | nil] 502 | def parse_soap_fault(document) 503 | unless document.nil? 504 | node = document.xpath('/env:Envelope/env:Body/descendant-or-self::env:Fault', { 'env' => self.envelope_namespace }).first 505 | Fault.from_xml(node, :namespace => self.envelope_namespace) unless node.nil? 506 | end 507 | end 508 | end 509 | 510 | def self.pretty_format_envelope(xml_string) 511 | if /^<.*:Envelope/.match(xml_string) 512 | begin 513 | doc = Handsoap::XmlQueryFront.parse_string(xml_string, Handsoap.xml_query_driver) 514 | rescue 515 | return xml_string 516 | end 517 | return doc.to_xml 518 | # return "\n\e[1;33m" + doc.to_s + "\e[0m" 519 | end 520 | return xml_string 521 | end 522 | end 523 | 524 | # Legacy/BC code here. This shouldn't be used in new applications. 525 | module Handsoap 526 | class Service 527 | # Registers a simple method mapping without any arguments and no parsing of response. 528 | # 529 | # This is deprecated 530 | def self.map_method(mapping) 531 | if @mapping.nil? 532 | @mapping = {} 533 | end 534 | @mapping.merge! mapping 535 | end 536 | def self.get_mapping(name) 537 | @mapping[name] if @mapping 538 | end 539 | def method_missing(method, *args, &block) 540 | action = self.class.get_mapping(method) 541 | if action 542 | invoke(action, *args, &block) 543 | else 544 | super 545 | end 546 | end 547 | # Registers a block to call when a request document is created. 548 | # 549 | # This is deprecated, in favour of #on_create_document 550 | def self.on_create_document(&block) 551 | @create_document_callback = block 552 | end 553 | def self.fire_on_create_document(doc) 554 | if @create_document_callback 555 | @create_document_callback.call doc 556 | end 557 | end 558 | private 559 | # Helper to serialize a node into a ruby string 560 | # 561 | # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_s 562 | def xml_to_str(node, xquery = nil) 563 | n = xquery ? node.xpath(xquery, ns).first : node 564 | return if n.nil? 565 | n.to_s 566 | end 567 | alias_method :xml_to_s, :xml_to_str 568 | # Helper to serialize a node into a ruby integer 569 | # 570 | # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_i 571 | def xml_to_int(node, xquery = nil) 572 | n = xquery ? node.xpath(xquery, ns).first : node 573 | return if n.nil? 574 | n.to_s.to_i 575 | end 576 | alias_method :xml_to_i, :xml_to_int 577 | # Helper to serialize a node into a ruby float 578 | # 579 | # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_f 580 | def xml_to_float(node, xquery = nil) 581 | n = xquery ? node.xpath(xquery, ns).first : node 582 | return if n.nil? 583 | n.to_s.to_f 584 | end 585 | alias_method :xml_to_f, :xml_to_float 586 | # Helper to serialize a node into a ruby boolean 587 | # 588 | # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_boolean 589 | def xml_to_bool(node, xquery = nil) 590 | n = xquery ? node.xpath(xquery, ns).first : node 591 | return if n.nil? 592 | n.to_s == "true" 593 | end 594 | # Helper to serialize a node into a ruby Time object 595 | # 596 | # *deprecated*. Use Handsoap::XmlQueryFront::XmlElement#to_date 597 | def xml_to_date(node, xquery = nil) 598 | n = xquery ? node.xpath(xquery, ns).first : node 599 | return if n.nil? 600 | Time.iso8601(n.to_s) 601 | end 602 | end 603 | end 604 | -------------------------------------------------------------------------------- /lib/handsoap/xml_mason.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | module Handsoap 4 | 5 | # XmlMason is a simple XML builder. 6 | module XmlMason 7 | 8 | XML_ESCAPE = { '&' => '&', '"' => '"', '>' => '>', '<' => '<' } 9 | 10 | def self.xml_escape(s) 11 | s.to_s.gsub(/[&"><]/) { |special| XML_ESCAPE[special] } 12 | end 13 | 14 | class Node 15 | def initialize 16 | @namespaces = {} 17 | end 18 | def add(node_name, value = nil, options = {}) # :yields: Handsoap::XmlMason::Element 19 | prefix, name = parse_ns(node_name) 20 | node = append_child Element.new(self, prefix, name, value, options) 21 | if block_given? 22 | yield node 23 | end 24 | end 25 | # Registers a prefix for a namespace. 26 | # 27 | # You must register a namespace, before you can refer it. 28 | def alias(prefix, namespaces) 29 | @namespaces[prefix] = namespaces 30 | end 31 | # Finds the first element whos +node_name+ equals +name+ 32 | # 33 | # Doesn't regard namespaces/prefixes. 34 | def find(name) 35 | raise NotImplementedError.new 36 | end 37 | # Finds all elements whos +node_name+ equals +name+ 38 | # 39 | # Doesn't regard namespaces/prefixes. 40 | def find_all(name) 41 | raise NotImplementedError.new 42 | end 43 | def parse_ns(name) 44 | matches = name.match /^([^:]+):(.*)$/ 45 | if matches 46 | [matches[1] == '*' ? @prefix : matches[1], matches[2]] 47 | else 48 | [nil, name] 49 | end 50 | end 51 | private :parse_ns 52 | end 53 | 54 | class Document < Node 55 | def initialize # :yields: Document 56 | super 57 | @document_element = nil 58 | @xml_header = true 59 | if block_given? 60 | yield self 61 | end 62 | end 63 | def xml_header=(xml_header) 64 | @xml_header = !! xml_header 65 | end 66 | def append_child(node) 67 | if not @document_element.nil? 68 | raise "There can only be one element at the top level." 69 | end 70 | @document_element = node 71 | end 72 | def find(name) 73 | @document_element.find(name) 74 | end 75 | def find_all(name) 76 | @document_element.find_all(name) 77 | end 78 | def get_namespace(prefix) 79 | @namespaces[prefix] || raise("No alias registered for prefix '#{prefix}'") 80 | end 81 | def defines_namespace?(prefix) 82 | false 83 | end 84 | def to_s 85 | if @document_element.nil? 86 | raise "No document element added." 87 | end 88 | (@xml_header ? "\n" : "") + @document_element.to_s 89 | end 90 | end 91 | 92 | class TextNode 93 | def initialize(text) 94 | @text = text 95 | end 96 | def to_s(indentation = '') 97 | XmlMason.xml_escape(@text) 98 | end 99 | end 100 | 101 | class RawContent < TextNode 102 | def to_s(indentation = '') 103 | @text 104 | end 105 | end 106 | 107 | class Element < Node 108 | def initialize(parent, prefix, node_name, value = nil, options = {}) # :yields: Handsoap::XmlMason::Element 109 | super() 110 | # if prefix.to_s == "" 111 | # raise "missing prefix" 112 | # end 113 | @parent = parent 114 | @prefix = prefix 115 | @node_name = node_name 116 | @children = [] 117 | @attributes = {} 118 | if options[:attributes] 119 | @attributes = options[:attributes] 120 | end 121 | if not value.nil? 122 | set_value value.to_s, options 123 | end 124 | if block_given? 125 | yield self 126 | end 127 | end 128 | # Returns the document that this element belongs to, or self if this is the document. 129 | def document 130 | @parent.respond_to?(:document) ? @parent.document : @parent 131 | end 132 | # Returns the qname (prefix:nodename) 133 | def full_name 134 | @prefix.nil? ? @node_name : (@prefix + ":" + @node_name) 135 | end 136 | # Adds a child node. 137 | # 138 | # You usually won't need to call this method, but will rather use +add+ 139 | def append_child(node) 140 | if value_node? 141 | raise "Element already has a text value. Can't add nodes" 142 | end 143 | @children << node 144 | return node 145 | end 146 | # Sets the inner text of this element. 147 | # 148 | # By default the string is escaped, but you can pass the option flag :raw to inject XML. 149 | # 150 | # You usually won't need to call this method, but will rather use +add+ 151 | def set_value(value, options = {}) 152 | if @children.length > 0 153 | raise "Element already has children. Can't set value" 154 | end 155 | if options && options.include?(:raw) 156 | @children = [RawContent.new(value)] 157 | else 158 | @children = [TextNode.new(value)] 159 | end 160 | end 161 | # Sets the value of an attribute. 162 | def set_attr(name, value) 163 | full_name = parse_ns(name).join(":") 164 | @attributes[name] = value 165 | end 166 | def find(name) 167 | name = name.to_s if name.kind_of? Symbol 168 | if @node_name == name || full_name == name 169 | return self 170 | end 171 | @children.each do |node| 172 | if node.respond_to? :find 173 | tmp = node.find(name) 174 | if tmp 175 | return tmp 176 | end 177 | end 178 | end 179 | return nil 180 | end 181 | def find_all(name) 182 | name = name.to_s if name.kind_of? Symbol 183 | result = [] 184 | if @node_name == name || full_name == name 185 | result << self 186 | end 187 | @children.each do |node| 188 | if node.respond_to? :find 189 | result = result.concat(node.find_all(name)) 190 | end 191 | end 192 | return result 193 | end 194 | def value_node? 195 | @children.length == 1 && @children[0].kind_of?(TextNode) 196 | end 197 | def get_namespace(prefix) 198 | @namespaces[prefix] || @parent.get_namespace(prefix) 199 | end 200 | def defines_namespace?(prefix) 201 | @attributes.keys.include?("xmlns:#{prefix}") || @parent.defines_namespace?(prefix) 202 | end 203 | def to_s(indentation = '') 204 | # todo resolve attribute prefixes aswell 205 | if @prefix && (not defines_namespace?(@prefix)) 206 | set_attr "xmlns:#{@prefix}", get_namespace(@prefix) 207 | end 208 | name = XmlMason.xml_escape(full_name) 209 | attr = (@attributes.any? ? (" " + @attributes.map { |key, value| XmlMason.xml_escape(key) + '="' + XmlMason.xml_escape(value) + '"' }.join(" ")) : "") 210 | if @children.any? 211 | if value_node? 212 | children = @children[0].to_s(indentation + " ") 213 | else 214 | children = @children.map { |node| "\n" + node.to_s(indentation + " ") }.join("") + "\n" + indentation 215 | end 216 | indentation + "<" + name + attr + ">" + children + "" 217 | else 218 | indentation + "<" + name + attr + " />" 219 | end 220 | end 221 | end 222 | end 223 | 224 | end 225 | -------------------------------------------------------------------------------- /lib/handsoap/xml_query_front.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | module Handsoap 3 | # 4 | # A simple frontend for parsing XML document with Xpath. 5 | # 6 | # This provides a unified interface for multiple xpath-capable dom-parsers, 7 | # allowing seamless switching between the underlying implementations. 8 | # 9 | # A document is loaded using the function Handsoap::XmlQueryFront.parse_string, passing 10 | # the xml source string and a driver, which can (currently) be one of: 11 | # 12 | # :rexml 13 | # :nokogiri 14 | # :libxml 15 | # 16 | # The resulting object is a wrapper, of the type Handsoap::XmlQueryFront::XmlElement. 17 | # 18 | module XmlQueryFront 19 | 20 | # This error is raised if the document didn't parse 21 | class ParseError < RuntimeError; end 22 | 23 | # Loads requirements for a driver. 24 | # 25 | # This function is implicitly called by +parse_string+. 26 | def self.load_driver!(driver) 27 | if driver == :rexml 28 | require 'rexml/document' 29 | elsif driver == :nokogiri 30 | require 'nokogiri' 31 | begin 32 | gem('nokogiri') # work around bug in rubygems for Ruby 1.9 33 | 34 | if Gem.loaded_specs['nokogiri'].version < Gem::Version.new('1.3.0') 35 | raise "Incompatible version of Nokogiri. Please upgrade gem." 36 | end 37 | rescue NoMethodError 38 | end 39 | elsif driver == :libxml 40 | require 'libxml' 41 | else 42 | raise "Unknown driver #{driver}" 43 | end 44 | return driver 45 | end 46 | 47 | # Returns a wrapped XML parser, using the requested driver. 48 | # 49 | # +driver+ can be one of the following: 50 | # :rexml 51 | # :nokogiri 52 | # :libxml 53 | def self.parse_string(xml_string, driver) 54 | load_driver!(driver) 55 | if driver == :rexml 56 | doc = REXML::Document.new(xml_string) 57 | raise ParseError.new if doc.root.nil? 58 | XmlQueryFront::REXMLDriver.new(doc) 59 | elsif driver == :nokogiri 60 | doc = Nokogiri::XML(xml_string) 61 | raise ParseError.new unless (doc && doc.root && doc.errors.empty?) 62 | XmlQueryFront::NokogiriDriver.new(doc) 63 | elsif driver == :libxml 64 | begin 65 | LibXML::XML::Error.set_handler &LibXML::XML::Error::QUIET_HANDLER 66 | doc = XmlQueryFront::LibXMLDriver.new(LibXML::XML::Parser.string(xml_string).parse) 67 | rescue ArgumentError, LibXML::XML::Error => ex 68 | raise ParseError.new 69 | end 70 | end 71 | end 72 | 73 | # NodeSelection is a wrapper around Array, that implicitly delegates XmlElement methods to the first element. 74 | # 75 | # It makes mapping code prettier, since you often need to access the first element of a selection. 76 | class NodeSelection < Array 77 | def to_i 78 | self.first.to_i if self.any? 79 | end 80 | def to_f 81 | self.first.to_f if self.any? 82 | end 83 | def to_boolean 84 | self.first.to_boolean if self.any? 85 | end 86 | def to_date 87 | self.first.to_date if self.any? 88 | end 89 | def to_big_decimal(decimal_places = 2) 90 | self.first.to_big_decimal(decimal_places) if self.any? 91 | end 92 | def to_s 93 | self.first.to_s if self.any? 94 | end 95 | def node_name 96 | self.first.node_name if self.any? 97 | end 98 | def node_namespace 99 | self.first.node_namespace if self.any? 100 | end 101 | def xpath(expression, ns = nil) 102 | self.first.xpath(expression, ns) 103 | end 104 | def /(expression) 105 | self.first.xpath(expression) 106 | end 107 | def to_xml 108 | self.first.to_xml if self.any? 109 | end 110 | def to_raw 111 | self.first.to_raw if self.any? 112 | end 113 | end 114 | 115 | # Wraps the underlying (native) xml driver, and provides a uniform interface. 116 | module XmlElement 117 | def initialize(element, namespaces = {}) 118 | @element = element 119 | @namespaces = namespaces 120 | end 121 | # Registers a prefix to refer to a namespace. 122 | # 123 | # You can either register a nemspace with this function or pass it explicitly to the +xpath+ method. 124 | def add_namespace(prefix, uri) 125 | @namespaces[prefix] = uri 126 | end 127 | # Checks that an xpath-query doesn't refer to any undefined prefixes in +ns+ 128 | def assert_prefixes!(expression, ns) 129 | expression.scan(/([a-zA-Z_][a-zA-Z0-9_.-]*):[^:]+/).map{|m| m[0] }.each do |prefix| 130 | raise "Undefined prefix '#{prefix}' in #{ns.inspect}" if ns[prefix].nil? 131 | end 132 | end 133 | # Returns the value of the element as an integer. 134 | # 135 | # See +to_s+ 136 | def to_i 137 | t = self.to_s 138 | return if t.nil? 139 | t.to_i 140 | end 141 | # Returns the value of the element as a float. 142 | # 143 | # See +to_s+ 144 | def to_f 145 | t = self.to_s 146 | return if t.nil? 147 | t.to_f 148 | end 149 | # Returns the value of the element as an boolean. 150 | # 151 | # See +to_s+ 152 | def to_boolean 153 | t = self.to_s 154 | return if t.nil? 155 | t.downcase == 'true' 156 | end 157 | # Returns the value of the element as a ruby Time object. 158 | # 159 | # See +to_s+ 160 | def to_date 161 | t = self.to_s 162 | return if t.nil? 163 | Time.iso8601(t) 164 | end 165 | # Returns the value of the element as an instance of BigDecimal 166 | # 167 | # See +to_s+ 168 | def to_big_decimal(decimal_places = 2) 169 | t = self.to_s 170 | return if t.nil? 171 | BigDecimal.new t, decimal_places 172 | end 173 | # Returns the inner text content of this element, or the value (if it's an attr or textnode). 174 | # 175 | # The output is a UTF-8 encoded string, without xml-entities. 176 | def to_s 177 | raise NotImplementedError.new 178 | end 179 | # Returns the underlying native element. 180 | # 181 | # You shouldn't need to use this, since doing so would void portability. 182 | def native_element 183 | @element 184 | end 185 | # Returns the node name of the current element. 186 | def node_name 187 | raise NotImplementedError.new 188 | end 189 | # Returns the node namespace uri of the current element if any, +nil+ otherwise. 190 | # Result returned for attribute nodes varies for different drivers, currently. 191 | def node_namespace 192 | raise NotImplementedError.new 193 | end 194 | # Queries the document with XPath, relative to the current element. 195 | # 196 | # +ns+ Should be a Hash of prefix => namespace 197 | # 198 | # Returns a +NodeSelection+ 199 | # 200 | # See add_namespace 201 | def xpath(expression, ns = nil) 202 | raise NotImplementedError.new 203 | end 204 | # Returns a +NodeSelection+ 205 | def children 206 | raise NotImplementedError.new 207 | end 208 | # Returns the outer XML for this element. 209 | def to_xml 210 | raise NotImplementedError.new 211 | end 212 | # Returns the outer XML for this element, preserving the original formatting. 213 | def to_raw 214 | raise NotImplementedError.new 215 | end 216 | # alias of +xpath+ 217 | def /(expression) 218 | self.xpath(expression) 219 | end 220 | # Returns the attribute value of the underlying element. 221 | # 222 | # Shortcut for: 223 | # 224 | # (node/"@attribute_name").to_s 225 | def [](attribute_name) 226 | raise NotImplementedError.new 227 | end 228 | end 229 | 230 | # Driver for +libxml+. 231 | # 232 | # http://libxml.rubyforge.org/ 233 | class LibXMLDriver 234 | include XmlElement 235 | def node_name 236 | @element.name 237 | end 238 | def node_namespace 239 | if @element.respond_to? :namespaces 240 | if namespace = @element.namespaces.namespace 241 | namespace.href 242 | end 243 | end 244 | end 245 | def xpath(expression, ns = nil) 246 | ns = {} if ns.nil? 247 | ns = @namespaces.merge(ns) 248 | assert_prefixes!(expression, ns) 249 | NodeSelection.new(@element.find(expression, ns.map{|k,v| "#{k}:#{v}" }).to_a.map{|node| LibXMLDriver.new(node, ns) }) 250 | end 251 | def children 252 | NodeSelection.new(@element.children.map{|node| LibXMLDriver.new(node) }) 253 | end 254 | def [](attribute_name) 255 | raise ArgumentError.new unless attribute_name.kind_of? String 256 | @element[attribute_name] 257 | end 258 | def to_xml 259 | @element.to_s(:indent => true) 260 | end 261 | def to_raw 262 | @element.to_s(:indent => false) 263 | end 264 | def to_s 265 | if @element.kind_of? LibXML::XML::Attr 266 | @element.value 267 | else 268 | @element.content 269 | end 270 | end 271 | end 272 | 273 | # Driver for +REXML+ 274 | # 275 | # http://www.germane-software.com/software/rexml/ 276 | class REXMLDriver 277 | include XmlElement 278 | def node_name 279 | if @element.respond_to? :name 280 | @element.name 281 | else 282 | @element.class.name.gsub(/.*::([^:]+)$/, "\\1").downcase 283 | end 284 | end 285 | def node_namespace 286 | if @element.respond_to? :namespace 287 | namespace = @element.namespace 288 | return if namespace == '' 289 | end 290 | namespace 291 | end 292 | def xpath(expression, ns = nil) 293 | ns = {} if ns.nil? 294 | ns = @namespaces.merge(ns) 295 | assert_prefixes!(expression, ns) 296 | NodeSelection.new(REXML::XPath.match(@element, expression, ns).map{|node| REXMLDriver.new(node, ns) }) 297 | end 298 | def children 299 | NodeSelection.new(@element.children.map{|node| REXMLDriver.new(node) }) 300 | end 301 | def [](attribute_name) 302 | raise ArgumentError.new unless attribute_name.kind_of? String 303 | @element.attributes[attribute_name] 304 | end 305 | def to_xml 306 | require 'rexml/formatters/pretty' 307 | formatter = REXML::Formatters::Pretty.new 308 | out = String.new 309 | formatter.write(@element, out) 310 | # patch for REXML's broken formatting 311 | out.gsub(/>\n\s+([^<]+)\n\s+<\//, ">\\1 'UTF-8') 351 | end 352 | def to_raw 353 | @element.serialize(:encoding => 'UTF-8', :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML) 354 | end 355 | def to_s 356 | if @element.kind_of?(Nokogiri::XML::Text) || @element.kind_of?(Nokogiri::XML::CDATA) 357 | element = @element 358 | elsif @element.kind_of?(Nokogiri::XML::Attr) 359 | return @element.value 360 | else 361 | element = @element.children.first 362 | end 363 | return if element.nil? 364 | # This looks messy because it is .. Nokogiri's interface is in a flux 365 | if element.kind_of?(Nokogiri::XML::CDATA) 366 | element.serialize(:encoding => 'UTF-8').gsub(/^$/, "") 367 | else 368 | element.serialize(:encoding => 'UTF-8').gsub('<', '<').gsub('>', '>').gsub('"', '"').gsub(''', "'").gsub('&', '&') 369 | end 370 | end 371 | end 372 | end 373 | end 374 | -------------------------------------------------------------------------------- /tests/GoogleSearch-soapui-project.xml: -------------------------------------------------------------------------------- 1 | 2 | /home/tkn/public/handsoap/testshttp://api.google.com/GoogleSearch.wsdl 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | ]]>http://schemas.xmlsoap.org/wsdl/http://api.google.com/search/beta2http://127.0.0.1:8088/mockGoogleSearchBindingUTF-8http://api.google.com/search/beta2 145 | 146 | 147 | 148 | ? 149 | ? 150 | 151 | 152 | ]]>UTF-8http://api.google.com/search/beta2 153 | 154 | 155 | 156 | ? 157 | ? 158 | ? 159 | ? 160 | ? 161 | ? 162 | ? 163 | ? 164 | ? 165 | ? 166 | 167 | 168 | ]]>UTF-8http://api.google.com/search/beta2 169 | 170 | 171 | 172 | ? 173 | ? 174 | 175 | 176 | ]]>SEQUENCEResponse 1 177 | 178 | 179 | 180 | cid:109721102465 181 | 182 | 183 | ]]>SEQUENCEResponse 1 184 | 185 | 186 | 187 | 188 | 189 | ? 190 | ? 191 | ? 192 | ? 193 | 194 | ? 195 | ? 196 | ? 197 | ? 198 | 199 | ? 200 | 201 | 202 | 203 | ]]>SEQUENCEResponse 1 204 | 205 | 206 | 207 | ? 208 | 209 | 210 | ]]> -------------------------------------------------------------------------------- /tests/Weather.wsdl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | Gets Information for each WeatherID 167 | 168 | 169 | 170 | 171 | Allows you to get your City Forecast Over the Next 7 Days, which is updated hourly. U.S. Only 172 | 173 | 174 | 175 | 176 | Allows you to get your City's Weather, which is updated hourly. U.S. Only 177 | 178 | 179 | 180 | 181 | 182 | 183 | Gets Information for each WeatherID 184 | 185 | 186 | 187 | 188 | Allows you to get your City Forecast Over the Next 7 Days, which is updated hourly. U.S. Only 189 | 190 | 191 | 192 | 193 | Allows you to get your City's Weather, which is updated hourly. U.S. Only 194 | 195 | 196 | 197 | 198 | 199 | 200 | Gets Information for each WeatherID 201 | 202 | 203 | 204 | 205 | Allows you to get your City Forecast Over the Next 7 Days, which is updated hourly. U.S. Only 206 | 207 | 208 | 209 | 210 | Allows you to get your City's Weather, which is updated hourly. U.S. Only 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | -------------------------------------------------------------------------------- /tests/WeatherSummary.wsdl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 22 | 23 | 24 | 26 | 28 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 99 | 100 | 101 | WeatherSummary 102 | 103 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /tests/account_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'rubygems' 3 | require 'test/unit' 4 | 5 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + "/../lib") 6 | require 'handsoap' 7 | 8 | ACCOUNT_SERVICE_ENDPOINT = { 9 | :uri => 'http://ws.example.org/', 10 | :version => 1 11 | } 12 | 13 | class AccountService < Handsoap::Service 14 | endpoint ACCOUNT_SERVICE_ENDPOINT 15 | 16 | def on_create_document(doc) 17 | doc.alias 'tns', 'http://schema.example.org/AccountService' 18 | doc.alias 's', "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" 19 | header = doc.find("Header") 20 | header.add "s:Security" do |s| 21 | s.set_attr "env:mustUnderstand", "0" 22 | s.add "s:Username", @@username 23 | end 24 | end 25 | 26 | def on_response_document(doc) 27 | # register namespaces for the response 28 | doc.add_namespace 'ns', 'http://schema.example.org/AccountService' 29 | end 30 | 31 | @@username = "" 32 | def self.username=(username) 33 | @@username = username 34 | end 35 | 36 | # public methods 37 | 38 | def get_account_by_id(account_id) 39 | soap_action = 'http://ws.example.org/AccountService/GetAccountById' 40 | response = invoke('tns:GetAccountById', soap_action) do |message| 41 | message.add 'account-id', account_id 42 | end 43 | # 44 | # 53 | Account.new :msisdn => (node/"@msisdn").to_s, 54 | :created => (node/"@created").to_date, 55 | :buy_attempts => (node/"@buy-attempts").to_i, 56 | :blacklisted => (node/"@blacklisted").to_boolean, 57 | :application_id => (node/"@application-id").to_i, 58 | :amount_used => (node/"@amount-used").to_i, 59 | :account_id => (node/"@account-id").to_i, 60 | :credit => (node/"@credit").to_big_decimal 61 | end 62 | end 63 | 64 | class Account 65 | attr_accessor :msisdn, :application_id, :account_id, :created, :buy_attempts, :amount_used 66 | attr_writer :blacklisted 67 | def initialize(values = {}) 68 | @msisdn = values[:msisdn] 69 | @application_id = values[:application_id] 70 | @account_id = values[:account_id] 71 | @created = values[:created] 72 | @buy_attempts = values[:buy_attempts] 73 | @blacklisted = values[:blacklisted] || false 74 | @amount_used = values[:amount_used] 75 | @credit = values[:credit] 76 | end 77 | def blacklisted? 78 | !! @blacklisted 79 | end 80 | end 81 | 82 | 83 | class AccountServiceTest < Test::Unit::TestCase 84 | 85 | def setup 86 | # AccountService.logger = $stdout 87 | AccountService.username = "someone" 88 | headers = 'Date: Fri, 14 Aug 2009 11:57:36 GMT 89 | Content-Type: text/xml;charset=UTF-8 90 | X-Powered-By: Servlet 2.4; JBoss-4.2.2.GA (build: SVNTag=JBoss_4_2_2_GA date=200710221139)/Tomcat-5.5 91 | Server: Apache-Coyote/1.1'.gsub(/\n/, "\r\n") 92 | body = ' 93 | 94 | 95 | 96 | 97 | 98 | 99 | ' 100 | Handsoap::Http.drivers[:mock] = Handsoap::Http::Drivers::MockDriver.new :headers => headers, :content => body, :status => 200 101 | Handsoap.http_driver = :mock 102 | end 103 | 104 | def test_get_account_by_id 105 | driver = Handsoap::Http.drivers[:mock].new # passthrough, doesn’t actually create a new instance 106 | result = AccountService.get_account_by_id(10) 107 | assert_equal 'http://ws.example.org/', driver.last_request.url 108 | assert_equal :post, driver.last_request.http_method 109 | assert_kind_of Account, result 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /tests/benchmark_integration_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + "/../lib") 3 | require 'rubygems' 4 | require 'handsoap' 5 | require 'soap/wsdlDriver' 6 | require 'soap/header/simplehandler' 7 | require 'benchmark' 8 | 9 | # 10 | # Start the mockservice with: 11 | # sh ~/path/to/soapui-2.5.1/bin/mockservicerunner.sh -p 8088 tests/GoogleSearch-soapui-project.xml 12 | # 13 | # Run the benchmark with (100 tries): 14 | # ruby tests/benchmark_integration_test.rb 100 15 | # 16 | 17 | # handsoap mappings: 18 | 19 | class TestService < Handsoap::Service 20 | endpoint :uri => 'http://127.0.0.1:8088/mockGoogleSearchBinding', :version => 1 21 | map_method :do_spelling_suggestion => "urn:doSpellingSuggestion" 22 | on_create_document do |doc| 23 | doc.alias 'urn', "urn:GoogleSearch" 24 | doc.alias 'xsi', "http://www.w3.org/2001/XMLSchema-instance" 25 | end 26 | def do_spelling_suggestion(key, phrase) 27 | invoke("urn:doSpellingSuggestion") do |message| 28 | message.add "key" do |k| 29 | k.set_attr "xsi:type", "xsd:string" 30 | k.add key 31 | end 32 | message.add "phrase" do |k| 33 | k.set_attr "xsi:type", "xsd:string" 34 | k.add phrase 35 | end 36 | end 37 | end 38 | 39 | end 40 | 41 | # soap4r mappings: 42 | 43 | def make_soap4r 44 | SOAP::WSDLDriverFactory.new('http://127.0.0.1:8088/mockGoogleSearchBinding?WSDL').create_rpc_driver('GoogleSearchService', 'GoogleSearchPort') 45 | end 46 | 47 | def make_handsoap 48 | TestService.new 49 | end 50 | 51 | service_4 = make_soap4r 52 | service_h = make_handsoap 53 | 54 | # TestService.logger = $stdout 55 | # service_4.wiredump_dev = $stdout 56 | 57 | times = ARGV[0].to_i 58 | if times < 1 59 | times = 1 60 | end 61 | puts "Benchmarking #{times} calls ..." 62 | Benchmark.bm(32) do |x| 63 | x.report("soap4r") do 64 | (1..times).each { 65 | service_4.doSpellingSuggestion("foo", "bar") 66 | } 67 | end 68 | Handsoap.http_driver = :curb 69 | Handsoap.xml_query_driver = :nokogiri 70 | x.report("handsoap+curb+nokogiri") do 71 | (1..times).each { 72 | service_h.do_spelling_suggestion("foo", "bar") 73 | } 74 | end 75 | Handsoap.http_driver = :curb 76 | Handsoap.xml_query_driver = :libxml 77 | x.report("handsoap+curb+libxml") do 78 | (1..times).each { 79 | service_h.do_spelling_suggestion("foo", "bar") 80 | } 81 | end 82 | Handsoap.http_driver = :curb 83 | Handsoap.xml_query_driver = :rexml 84 | x.report("handsoap+curb+rexml") do 85 | (1..times).each { 86 | service_h.do_spelling_suggestion("foo", "bar") 87 | } 88 | end 89 | Handsoap.http_driver = :httpclient 90 | Handsoap.xml_query_driver = :nokogiri 91 | x.report("handsoap+httpclient+nokogiri") do 92 | (1..times).each { 93 | service_h.do_spelling_suggestion("foo", "bar") 94 | } 95 | end 96 | Handsoap.http_driver = :httpclient 97 | Handsoap.xml_query_driver = :libxml 98 | x.report("handsoap+httpclient+libxml") do 99 | (1..times).each { 100 | service_h.do_spelling_suggestion("foo", "bar") 101 | } 102 | end 103 | Handsoap.http_driver = :httpclient 104 | Handsoap.xml_query_driver = :rexml 105 | x.report("handsoap+httpclient+rexml") do 106 | (1..times).each { 107 | service_h.do_spelling_suggestion("foo", "bar") 108 | } 109 | end 110 | end 111 | puts "---------------" 112 | puts "Legend:" 113 | puts "The user CPU time, system CPU time, the sum of the user and system CPU times," 114 | puts "and the elapsed real time. The unit of time is seconds." 115 | -------------------------------------------------------------------------------- /tests/dispatch_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'rubygems' 3 | require 'test/unit' 4 | $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib/" 5 | require 'handsoap.rb' 6 | 7 | def var_dump(val) 8 | puts val.to_yaml.gsub(/ !ruby\/object:.+$/, '') 9 | end 10 | 11 | class MockResponse < Handsoap::Http::Response 12 | attr_writer :status, :headers, :body, :parts 13 | end 14 | 15 | class TestService < Handsoap::Service 16 | endpoint :uri => 'http://example.com', :version => 1 17 | 18 | def on_create_document(doc) 19 | doc.alias 'sc002', "http://www.wstf.org/docs/scenarios/sc002" 20 | doc.find("Header").add "sc002:SessionData" do |s| 21 | s.add "ID", "Client-1" 22 | end 23 | end 24 | 25 | def on_response_document(doc) 26 | doc.add_namespace 'ns', 'http://www.wstf.org/docs/scenarios/sc002' 27 | end 28 | 29 | def echo(text) 30 | response = invoke('sc002:Echo') do |message| 31 | message.add "text", text 32 | end 33 | (response.document/"//ns:EchoResponse/ns:text").to_s 34 | end 35 | end 36 | 37 | class TestServiceLegacyStyle < Handsoap::Service 38 | endpoint :uri => 'http://example.com', :version => 1 39 | 40 | def on_create_document(doc) 41 | doc.alias 'sc002', "http://www.wstf.org/docs/scenarios/sc002" 42 | doc.find("Header").add "sc002:SessionData" do |s| 43 | s.add "ID", "Client-1" 44 | end 45 | end 46 | 47 | def ns 48 | { 'ns' => 'http://www.wstf.org/docs/scenarios/sc002' } 49 | end 50 | 51 | def echo(text) 52 | response = invoke('sc002:Echo') do |message| 53 | message.add "text", text 54 | end 55 | xml_to_str(response.document, "//ns:EchoResponse/ns:text/text()") 56 | end 57 | end 58 | 59 | class TestOfDispatch < Test::Unit::TestCase 60 | def setup 61 | body = ' 62 | 63 | 64 | 65 | Lirum Opossum 66 | 67 | 68 | ' 69 | @mock_http_response = MockResponse.new(200, {"content-type" => ["text/xml;charset=utf-8"]}, body) 70 | Handsoap::Http.drivers[:mock] = Handsoap::Http::Drivers::MockDriver.new(@mock_http_response) 71 | Handsoap.http_driver = :mock 72 | end 73 | 74 | def test_normal_usecase 75 | assert_equal "Lirum Opossum", TestService.echo("Lirum Opossum") 76 | end 77 | 78 | def test_raises_on_http_error 79 | @mock_http_response.status = 404 80 | assert_raise ::Handsoap::HttpError do 81 | TestService.echo("Lirum Opossum") 82 | end 83 | end 84 | 85 | def test_raises_on_invalid_document 86 | @mock_http_response.body = "not xml!" 87 | assert_raise RuntimeError do 88 | TestService.echo("Lirum Opossum") 89 | end 90 | end 91 | 92 | def test_raises_on_fault 93 | @mock_http_response.body = ' 94 | 95 | 96 | 97 | soap:Server 98 | Not a ninja 99 | 100 | 101 | 102 | ' 103 | assert_raise Handsoap::Fault do 104 | TestService.echo("Lirum Opossum") 105 | end 106 | end 107 | 108 | def test_legacy_parser_helpers 109 | assert_equal "Lirum Opossum", TestServiceLegacyStyle.echo("Lirum Opossum") 110 | end 111 | 112 | def test_multipart_response 113 | body = ' 114 | 115 | 116 | 117 | Lirum Opossum 118 | 119 | 120 | ' 121 | @mock_http_response.parts = [Handsoap::Http::Part.new({}, body, nil)] 122 | assert_equal "Lirum Opossum", TestService.echo("Lirum Opossum") 123 | end 124 | 125 | def test_raises_on_no_document 126 | @mock_http_response.status = 202 127 | @mock_http_response.body = '' 128 | assert_raise RuntimeError do 129 | TestService.echo("Lirum Opossum") 130 | end 131 | end 132 | 133 | end 134 | -------------------------------------------------------------------------------- /tests/event_machine_test.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | 4 | require "#{File.dirname(__FILE__)}/socket_server.rb" 5 | 6 | require 'eventmachine' 7 | 8 | $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib/" 9 | require 'handsoap' 10 | require 'handsoap/http' 11 | 12 | class TestDeferredService < Handsoap::Service 13 | endpoint :uri => "http://127.0.0.1:#{TestSocketServer.port}/", :version => 1 14 | 15 | def on_create_document(doc) 16 | doc.alias 'sc002', "http://www.wstf.org/docs/scenarios/sc002" 17 | doc.find("Header").add "sc002:SessionData" do |s| 18 | s.add "ID", "Client-1" 19 | end 20 | end 21 | 22 | def on_response_document(doc) 23 | doc.add_namespace 'ns', 'http://www.wstf.org/docs/scenarios/sc002' 24 | end 25 | 26 | def echo(text, &block) 27 | async(block) do |dispatcher| 28 | dispatcher.request("sc002:Echo") do |m| 29 | m.add "text", text 30 | end 31 | dispatcher.response do |response| 32 | (response/"//ns:EchoResponse/ns:text").to_s 33 | end 34 | end 35 | 36 | end 37 | end 38 | 39 | 40 | SOAP_RESPONSE = " 41 | 42 | 43 | 44 | I am living in the future. 45 | 46 | 47 | ".gsub( 48 | /\n/ , "\r\n") 49 | 50 | 51 | class TestOfEventMachineDriver < Test::Unit::TestCase 52 | def driver 53 | :event_machine 54 | end 55 | 56 | def test_connect_to_example_com 57 | TestSocketServer.reset! 58 | TestSocketServer.responses << "HTTP/1.1 200 OK 59 | Server: Ruby 60 | Connection: close 61 | Content-Type: text/plain 62 | Content-Length: 2 63 | Date: Wed, 19 Aug 2009 12:13:45 GMT 64 | 65 | OK".gsub(/\n/, "\r\n") 66 | 67 | EventMachine.run do 68 | driver = Handsoap::Http.drivers[self.driver].new 69 | request = Handsoap::Http::Request.new("http://127.0.0.1:#{TestSocketServer.port}/") 70 | deferred = driver.send_http_request_async(request) 71 | 72 | deferred.callback do |response| 73 | assert_equal "Ruby", response.headers['server'] 74 | assert_equal "OK", response.body 75 | assert_equal 200, response.status 76 | EventMachine.stop 77 | end 78 | end 79 | end 80 | 81 | def test_service 82 | TestSocketServer.reset! 83 | TestSocketServer.responses << "HTTP/1.1 200 OK 84 | Server: Ruby 85 | Connection: close 86 | Content-Type: application/xml 87 | Content-Length: #{SOAP_RESPONSE.size} 88 | Date: Wed, 19 Aug 2009 12:13:45 GMT 89 | 90 | ".gsub(/\n/, "\r\n") + SOAP_RESPONSE 91 | 92 | Handsoap.http_driver = :event_machine 93 | 94 | EventMachine.run do 95 | TestDeferredService.echo("I am living in the future.") do |d| 96 | d.callback do |text| 97 | assert_equal "I am living in the future.", text 98 | EventMachine.stop 99 | end 100 | d.errback do |mixed| 101 | flunk "Flunked![#{mixed}]" 102 | EventMachine.stop 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /tests/fault_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'rubygems' 3 | require 'test/unit' 4 | 5 | $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib/" 6 | require "handsoap" 7 | require 'handsoap/xml_query_front' 8 | require 'handsoap/service' 9 | 10 | class ParseFaultTestCase < Test::Unit::TestCase 11 | def get_fault_11 12 | xml_doc = ' 13 | 14 | 15 | 16 | soap:Server 17 | Error while blackList account: the application does not exist 18 | 19 | 20 | 21 | ' 22 | Handsoap::XmlQueryFront.parse_string(xml_doc, Handsoap.xml_query_driver) 23 | end 24 | 25 | def get_fault_12 26 | xml_doc = ' 27 | 29 | 30 | 31 | 32 | env:Sender 33 | 34 | rpc:BadArguments 35 | 36 | 37 | 38 | Processing error 39 | Chyba zpracování 40 | 41 | 42 | 44 | Name does not match card number 45 | 999 46 | 47 | 48 | 49 | 50 | ' 51 | Handsoap::XmlQueryFront.parse_string(xml_doc, Handsoap.xml_query_driver) 52 | end 53 | 54 | # Added to test issue 6, found here: http://github.com/unwire/handsoap/issues#issue/6 55 | # this is for SOAP 1.1 btw 56 | def get_fault_with_namespace 57 | xml_doc = ' 58 | 59 | 60 | 61 | a:Client 62 | Failed to parse the request 63 | Invalid parameter 64 | 65 | 66 | ' 67 | 68 | Handsoap::XmlQueryFront.parse_string(xml_doc, Handsoap.xml_query_driver) 69 | end 70 | 71 | # Tests that SOAP1.1 faults can be parsed 72 | def test_can_parse_soap_fault_11 73 | envelope_namespace = "http://schemas.xmlsoap.org/soap/envelope/" 74 | 75 | node = get_fault_11.xpath('/env:Envelope/env:Body/descendant-or-self::env:Fault', { 'env' => envelope_namespace }) 76 | fault = Handsoap::Fault.from_xml(node, :namespace => envelope_namespace) 77 | 78 | assert_kind_of Handsoap::Fault, fault 79 | assert_equal 'soap:Server', fault.code 80 | assert_equal 'Error while blackList account: the application does not exist', fault.reason 81 | end 82 | 83 | # Tests that SOAP1.2 faults can be parsed 84 | def test_can_parse_soap_fault_12 85 | envelope_namespace = "http://www.w3.org/2003/05/soap-envelope" 86 | 87 | node = get_fault_12.xpath('/env:Envelope/env:Body/descendant-or-self::env:Fault', { 'env' => envelope_namespace }) 88 | fault = Handsoap::Fault.from_xml(node, :namespace => envelope_namespace) 89 | 90 | assert_kind_of Handsoap::Fault, fault 91 | assert_equal 'env:Sender', fault.code 92 | assert_equal 'Processing error', fault.reason 93 | end 94 | 95 | # Added to test issue 6, found here: http://github.com/unwire/handsoap/issues#issue/6 96 | # This is for SOAP 1.1 btw 97 | def test_can_parse_soap_fault_with_namespace 98 | envelope_namespace = "http://schemas.xmlsoap.org/soap/envelope/" 99 | 100 | node = get_fault_with_namespace.xpath('/env:Envelope/env:Body/descendant-or-self::env:Fault', { 'env' => envelope_namespace }) 101 | fault = Handsoap::Fault.from_xml(node, :namespace => envelope_namespace) 102 | 103 | assert_kind_of Handsoap::Fault, fault 104 | assert_equal 'a:Client', fault.code 105 | assert_equal 'Failed to parse the request', fault.reason 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /tests/handsoap_generator_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'rubygems' 3 | require 'test/unit' 4 | require 'rails_generator' 5 | require 'rails_generator/scripts/generate' 6 | require "#{File.dirname(__FILE__)}/../generators/handsoap/handsoap_generator.rb" 7 | 8 | module Rails 9 | module Generator 10 | module Lookup 11 | module ClassMethods 12 | def sources 13 | [PathSource.new(:user, "#{File.dirname(__FILE__)}/../generators")] 14 | end 15 | end 16 | end 17 | end 18 | end 19 | 20 | class HandsoapGeneratorTest < Test::Unit::TestCase 21 | 22 | def setup 23 | FileUtils.mkdir_p(fake_rails_root) if File.directory?(fake_rails_root) 24 | @original_files = file_list 25 | end 26 | 27 | def invoke_generator! 28 | Rails::Generator::Scripts::Generate.new.run(["handsoap", "https://mooshup.com/services/system/version?wsdl", "--backtrace", "--quiet"], :destination => fake_rails_root) 29 | end 30 | 31 | def test_can_invoke_generator 32 | invoke_generator! 33 | end 34 | 35 | def test_generator_creates_files 36 | invoke_generator! 37 | assert file_list.find {|name| name.match("app/models/version_service.rb") } 38 | assert file_list.find {|name| name.match("test/integration/version_service_test.rb") } 39 | assert File.read(fake_rails_root + "/app/models/version_service.rb").any? 40 | assert File.read(fake_rails_root + "/test/integration/version_service_test.rb").any? 41 | end 42 | 43 | def test_running_generator_twice_silently_skips_files 44 | invoke_generator! 45 | invoke_generator! 46 | end 47 | 48 | def test_can_parse_multiple_interfaces 49 | wsdl_file = File.join(File.dirname(__FILE__), 'Weather.wsdl') 50 | Rails::Generator::Scripts::Generate.new.run(["handsoap", wsdl_file, "--backtrace", "--quiet"], :destination => fake_rails_root) 51 | end 52 | 53 | private 54 | 55 | def fake_rails_root 56 | File.join(File.dirname(__FILE__), 'rails_root') 57 | end 58 | 59 | def file_list 60 | Dir.glob(File.join(fake_rails_root, "**/*")) 61 | end 62 | end 63 | 64 | 65 | -------------------------------------------------------------------------------- /tests/http_test.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | 4 | require "#{File.dirname(__FILE__)}/socket_server.rb" 5 | 6 | $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib/" 7 | require "handsoap" 8 | require 'handsoap/http' 9 | 10 | module AbstractHttpDriverTestCase 11 | 12 | def setup 13 | Handsoap::Http.drivers[self.driver].load! 14 | end 15 | 16 | def test_connect_to_example_com 17 | TestSocketServer.reset! 18 | TestSocketServer.responses << "HTTP/1.1 200 OK 19 | Server: Ruby 20 | Connection: close 21 | Content-Type: text/plain 22 | Content-Length: 2 23 | Date: Wed, 19 Aug 2009 12:13:45 GMT 24 | 25 | OK".gsub(/\n/, "\r\n") 26 | 27 | driver = Handsoap::Http.drivers[self.driver].new 28 | request = Handsoap::Http::Request.new("http://127.0.0.1:#{TestSocketServer.port}/") 29 | response = driver.send_http_request(request) 30 | assert_equal 200, response.status 31 | assert_equal ["Ruby"], response.headers['server'] 32 | assert_equal "OK", response.body 33 | end 34 | 35 | def test_chunked 36 | TestSocketServer.reset! 37 | TestSocketServer.responses << "HTTP/1.1 200 OK 38 | Server: Ruby 39 | Connection: Keep-Alive 40 | Content-Type: text/plain 41 | Transfer-Encoding: chunked 42 | Date: Wed, 19 Aug 2009 12:13:45 GMT 43 | 44 | b 45 | Hello World 46 | 0 47 | 48 | ".gsub(/\n/, "\r\n") 49 | 50 | driver = Handsoap::Http.drivers[self.driver].new 51 | request = Handsoap::Http::Request.new("http://127.0.0.1:#{TestSocketServer.port}/") 52 | response = driver.send_http_request(request) 53 | assert_equal "Hello World", response.body 54 | end 55 | 56 | end 57 | 58 | class TestOfNetHttpDriver < Test::Unit::TestCase 59 | include AbstractHttpDriverTestCase 60 | def driver 61 | :net_http 62 | end 63 | end 64 | 65 | class TestOfCurbDriver < Test::Unit::TestCase 66 | include AbstractHttpDriverTestCase 67 | def driver 68 | :curb 69 | end 70 | 71 | # Curl will use 100-Continue if Content-Length > 1024 72 | def test_continue 73 | TestSocketServer.reset! 74 | # TestSocketServer.debug = true 75 | TestSocketServer.responses << "HTTP/1.1 100 Continue 76 | 77 | ".gsub(/\n/, "\r\n") 78 | TestSocketServer.responses << "HTTP/1.1 200 OK 79 | Server: Ruby 80 | Connection: close 81 | Content-Type: text/plain 82 | Content-Length: 9 83 | Date: Wed, 19 Aug 2009 12:13:45 GMT 84 | 85 | okeydokey".gsub(/\n/, "\r\n") 86 | 87 | driver = Handsoap::Http.drivers[self.driver].new 88 | request = Handsoap::Http::Request.new("http://127.0.0.1:#{TestSocketServer.port}/", :post) 89 | request.body = (0...1099).map{ ('a'..'z').to_a[rand(26)] }.join 90 | response = driver.send_http_request(request) 91 | assert_equal "okeydokey", response.body 92 | end 93 | 94 | def test_no_retain_cookie_between_requests_by_default 95 | driver = Handsoap::Http.drivers[self.driver].new 96 | 97 | TestSocketServer.reset! 98 | TestSocketServer.responses << "HTTP/1.1 200 OK 99 | Server: Ruby 100 | Connection: close 101 | Content-Type: text/plain 102 | Date: Wed, 19 Aug 2009 12:13:45 GMT 103 | Set-Cookie: SessionId=s5x1rcvuktc3c455hgu23bxx; path=/; HttpOnly 104 | 105 | okeydokey".gsub(/\n/, "\r\n") 106 | 107 | request = Handsoap::Http::Request.new("http://localhost:#{TestSocketServer.port}/", :post) 108 | response = driver.send_http_request(request) 109 | assert_equal "okeydokey", response.body 110 | 111 | TestSocketServer.responses << "HTTP/1.1 200 OK 112 | Server: Ruby 113 | Connection: close 114 | 115 | The second body".gsub(/\n/, "\r\n") 116 | 117 | driver.send_http_request(request) 118 | # second request must NOT include the Cookie returned in Set-Cookie on the first request 119 | assert ! TestSocketServer.requests.last.include?("Cookie: SessionId=s5x1rcvuktc3c455hgu23bxx") 120 | end 121 | 122 | def test_retain_cookie_between_requests_when_cookies_enabled 123 | driver = Handsoap::Http.drivers[self.driver].new 124 | driver.enable_cookies = true # enable in-built Cookie support in Curb 125 | 126 | TestSocketServer.reset! 127 | TestSocketServer.responses << "HTTP/1.1 200 OK 128 | Server: Ruby 129 | Connection: close 130 | Content-Type: text/plain 131 | Date: Wed, 19 Aug 2009 12:13:45 GMT 132 | Set-Cookie: SessionId=s5x1rcvuktc3c455hgu23bxx; path=/; HttpOnly 133 | 134 | okeydokey".gsub(/\n/, "\r\n") 135 | 136 | request = Handsoap::Http::Request.new("http://localhost:#{TestSocketServer.port}/", :post) 137 | response = driver.send_http_request(request) 138 | assert_equal "okeydokey", response.body 139 | 140 | TestSocketServer.responses << "HTTP/1.1 200 OK 141 | Server: Ruby 142 | Connection: close 143 | 144 | The second body".gsub(/\n/, "\r\n") 145 | 146 | driver.send_http_request(request) 147 | # second request must include the Cookie returned in Set-Cookie on the first request 148 | assert TestSocketServer.requests.last.include?("Cookie: SessionId=s5x1rcvuktc3c455hgu23bxx") 149 | end 150 | 151 | end 152 | 153 | class TestOfHttpclientDriver < Test::Unit::TestCase 154 | include AbstractHttpDriverTestCase 155 | def driver 156 | :httpclient 157 | end 158 | end 159 | 160 | class TestOfHttp < Test::Unit::TestCase 161 | def test_parse_multipart_small 162 | boundary = 'MIMEBoundaryurn_uuid_FF5B45112F1A1EA3831249088019646' 163 | content_io = '--MIMEBoundaryurn_uuid_FF5B45112F1A1EA3831249088019646 164 | Content-Type: application/xop+xml; charset=UTF-8; type="text/xml" 165 | Content-Transfer-Encoding: binary 166 | Content-ID: <0.urn:uuid:FF5B45112F1A1EA3831249088019647@apache.org> 167 | 168 | 169 | 170 | 171 | 172 | Lirum Opossum 173 | 174 | 175 | 176 | --MIMEBoundaryurn_uuid_FF5B45112F1A1EA3831249088019646-- 177 | ' 178 | content_io.gsub!(/\n/, "\r\n") 179 | parts = Handsoap::Http::Drivers::AbstractDriver.new.parse_multipart(boundary, content_io) 180 | assert_equal 1, parts.size 181 | assert parts.first[:body] =~ /^ 8080) 10 | server = HTTPServer.new(config) 11 | yield server if block_given? 12 | ['INT', 'TERM'].each {|signal| 13 | trap(signal) {server.shutdown} 14 | } 15 | server.start 16 | end 17 | 18 | start_webrick { |server| 19 | htdigest = HTTPAuth::Htdigest.new('/tmp/webrick-htdigest') 20 | htdigest.set_passwd "Restricted", "user", "password" 21 | authenticator = HTTPAuth::DigestAuth.new( 22 | :UserDB => htdigest, 23 | :Realm => "Restricted" 24 | ) 25 | 26 | server.mount_proc('/') {|request, response| 27 | response.body = "basic
\ndigest\n" 28 | } 29 | 30 | server.mount_proc('/basic') {|request, response| 31 | HTTPAuth.basic_auth(request, response, "Restricted") {|user, pass| 32 | # this block returns true if 33 | # authentication token is valid 34 | user == 'user' && pass == 'password' 35 | } 36 | response.body = "You are authenticated to see the super secret data\n" 37 | } 38 | 39 | server.mount_proc('/digest') {|request, response| 40 | authenticator.authenticate(request, response) 41 | response.body = "You are authenticated to see the super secret data\n" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/parser_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'rubygems' 3 | require 'test/unit' 4 | $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib/" 5 | require 'handsoap/parser.rb' 6 | 7 | def var_dump(val) 8 | puts val.to_yaml.gsub(/ !ruby\/object:.+$/, '') 9 | end 10 | 11 | # Amazon is rpc + literal and is self-contained 12 | # http://soap.amazon.com/schemas2/AmazonWebServices.wsdl 13 | class TestOfAmazonWebServicesConnection < Test::Unit::TestCase 14 | def test_can_connect_and_read_wsdl 15 | wsdl = Handsoap::Parser::Wsdl.read('http://soap.amazon.com/schemas2/AmazonWebServices.wsdl') 16 | assert_kind_of Handsoap::Parser::Interface, wsdl.interfaces.first 17 | end 18 | end 19 | 20 | class TestOfAmazonWebServices < Test::Unit::TestCase 21 | def get_wsdl 22 | unless @wsdl 23 | @wsdl = Handsoap::Parser::Wsdl.read('http://soap.amazon.com/schemas2/AmazonWebServices.wsdl') 24 | end 25 | return @wsdl 26 | end 27 | def test_can_parse_services 28 | wsdl = get_wsdl 29 | services = wsdl.endpoints 30 | assert_kind_of Array, services 31 | assert_kind_of Handsoap::Parser::Endpoint, services.first 32 | assert_equal 'AmazonSearchPort', services.first.name 33 | assert_equal 'typens:AmazonSearchBinding', services.first.binding 34 | end 35 | def test_can_parse_port_types 36 | wsdl = get_wsdl 37 | port_types = wsdl.interfaces 38 | assert_kind_of Array, port_types 39 | assert_kind_of Handsoap::Parser::Interface, port_types.first 40 | assert_equal 'AmazonSearchPort', port_types.first.name 41 | assert_kind_of Array, port_types.first.operations 42 | assert_equal 'KeywordSearchRequest', port_types.first.operations.first.name 43 | end 44 | def test_can_parse_bindings 45 | wsdl = get_wsdl 46 | bindings = wsdl.bindings 47 | assert_kind_of Array, bindings 48 | assert_equal 'AmazonSearchBinding', bindings.first.name 49 | assert_kind_of Array, bindings.first.actions 50 | assert_equal 'KeywordSearchRequest', bindings.first.actions.first.name 51 | end 52 | end 53 | 54 | # Thomas-Bayer is rpc + document and has external type definitions 55 | # http://www.thomas-bayer.com/names-service/soap?wsdl 56 | class TestOfThomasBayerNameServiceConnection < Test::Unit::TestCase 57 | def test_can_connect_and_read_wsdl 58 | wsdl = Handsoap::Parser::Wsdl.read('http://www.thomas-bayer.com/names-service/soap?wsdl') 59 | assert_kind_of Handsoap::Parser::Interface, wsdl.interfaces.first 60 | end 61 | end 62 | 63 | class TestOfThomasBayerNameService < Test::Unit::TestCase 64 | def get_wsdl 65 | unless @wsdl 66 | @wsdl = Handsoap::Parser::Wsdl.read('http://www.thomas-bayer.com/names-service/soap?wsdl') 67 | end 68 | return @wsdl 69 | end 70 | def test_can_parse_services 71 | wsdl = get_wsdl 72 | services = wsdl.endpoints 73 | assert_kind_of Array, services 74 | assert_kind_of Handsoap::Parser::Endpoint, services.first 75 | assert_equal 'NamesServicePort', services.first.name 76 | assert_equal 'tns:NamesServicePortBinding', services.first.binding 77 | end 78 | def test_can_parse_port_types 79 | wsdl = get_wsdl 80 | port_types = wsdl.interfaces 81 | assert_kind_of Array, port_types 82 | assert_kind_of Handsoap::Parser::Interface, port_types.first 83 | assert_equal 'NamesService', port_types.first.name 84 | assert_kind_of Array, port_types.first.operations 85 | assert_equal 'getCountries', port_types.first.operations.first.name 86 | end 87 | def test_can_parse_bindings 88 | wsdl = get_wsdl 89 | bindings = wsdl.bindings 90 | assert_kind_of Array, bindings 91 | assert_equal 'NamesServicePortBinding', bindings.first.name 92 | assert_kind_of Array, bindings.first.actions 93 | assert_equal 'getCountries', bindings.first.actions.first.name 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /tests/service_integration_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'rubygems' 3 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + "/../lib") 4 | require 'handsoap' 5 | 6 | class TestService < Handsoap::Service 7 | endpoint :uri => 'http://127.0.0.1:8088/mocksc002SOAP11Binding', :version => 1 8 | map_method :begin => "sc002:Begin" 9 | map_method :notify => "sc002:Notify" 10 | map_method :echo => "sc002:Echo" 11 | def on_create_document(doc) 12 | doc.alias 'sc002', "http://www.wstf.org/docs/scenarios/sc002" 13 | doc.find("Header").add "sc002:SessionData" do |session_data| 14 | session_data.add "ID", "Client-1" 15 | end 16 | end 17 | def on_missing_document(http_response_body) 18 | # pass 19 | end 20 | end 21 | 22 | Handsoap::Service.logger = $stdout 23 | 24 | s = TestService.new 25 | 26 | s.begin 27 | 28 | s.notify do |x| 29 | x.add "text", "Hello" 30 | end 31 | 32 | s.echo do |x| 33 | x.add "text", "Hello" 34 | end 35 | 36 | -------------------------------------------------------------------------------- /tests/service_test.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | 4 | $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib/" 5 | require "handsoap" 6 | 7 | class TestFollowRedirects < Test::Unit::TestCase 8 | def test_follow_redirects 9 | assert !Handsoap.follow_redirects? 10 | Handsoap.follow_redirects! 11 | assert Handsoap.follow_redirects? 12 | end 13 | 14 | def test_max_redirects 15 | assert_equal Handsoap.max_redirects, 1 16 | Handsoap.max_redirects = 10 17 | assert_equal Handsoap.max_redirects, 10 18 | end 19 | end -------------------------------------------------------------------------------- /tests/socket_server.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | include Socket::Constants 3 | 4 | class TestSocketServer 5 | 6 | class << self 7 | attr_accessor :requests, :responses, :debug 8 | attr_reader :port 9 | end 10 | 11 | def self.reset! 12 | @debug = false 13 | @requests = [] 14 | @responses = [] 15 | end 16 | 17 | def self.start 18 | @socket = Socket.new AF_INET, SOCK_STREAM, 0 19 | @socket.bind Socket.pack_sockaddr_in(0, "127.0.0.1") 20 | @port = @socket.getsockname.unpack("snA*")[1] 21 | self.reset! 22 | @socket_thread = Thread.new do 23 | while true 24 | @socket.listen 1 25 | client_fd, client_sockaddr = @socket.sysaccept 26 | client_socket = Socket.for_fd client_fd 27 | while @responses.any? 28 | @requests << client_socket.recvfrom(8192)[0] 29 | response = @responses.shift 30 | if @debug 31 | puts "---" 32 | puts @requests 33 | puts "---" 34 | puts response 35 | end 36 | client_socket.print response 37 | end 38 | client_socket.close 39 | end 40 | end 41 | end 42 | 43 | self.start 44 | end 45 | -------------------------------------------------------------------------------- /tests/xml_mason_test.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'rubygems' 3 | $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib/" 4 | require "handsoap" 5 | require 'handsoap/xml_mason' 6 | 7 | require 'test/unit' 8 | 9 | class TestOfXmlMason < Test::Unit::TestCase 10 | def test_namespaces_are_automagically_assigned_upon_usage 11 | doc = Handsoap::XmlMason::Document.new do |doc| 12 | doc.alias 'x', 'http://example.com/x' 13 | doc.alias 'y', 'http://example.com/y' 14 | doc.add 'x:body' do |b| 15 | b.add 'y:yonks' 16 | end 17 | end 18 | xml = "\n" + 19 | "\n" + 20 | " \n" + 21 | "" 22 | assert_equal xml, doc.to_s 23 | end 24 | def test_namespaces_are_only_declared_on_the_topmost_level 25 | doc = Handsoap::XmlMason::Document.new do |doc| 26 | doc.alias 'x', 'http://example.com/x' 27 | doc.add 'x:body' do |b| 28 | b.add 'x:yonks' 29 | end 30 | end 31 | xml = "\n" + 32 | "\n" + 33 | " \n" + 34 | "" 35 | assert_equal xml, doc.to_s 36 | end 37 | def test_unused_namespaces_arent_included 38 | doc = Handsoap::XmlMason::Document.new do |doc| 39 | doc.alias 'x', 'http://example.com/x' 40 | doc.alias 'y', 'http://example.com/y' 41 | doc.add 'x:body' do |b| 42 | b.add 'x:yonks' 43 | end 44 | end 45 | xml = "\n" + 46 | "\n" + 47 | " \n" + 48 | "" 49 | assert_equal xml, doc.to_s 50 | end 51 | def test_textnodes_arent_indented 52 | doc = Handsoap::XmlMason::Document.new do |doc| 53 | doc.add 'body' do |b| 54 | b.add 'yonks', "lorem\nipsum\ndolor\nsit amet" 55 | end 56 | end 57 | contents = doc.to_s.match(/([\w\W]*)<\/yonks>/)[1] 58 | assert_equal "lorem\nipsum\ndolor\nsit amet", contents 59 | end 60 | def test_node_contents_is_escaped 61 | doc = Handsoap::XmlMason::Document.new do |doc| 62 | doc.add 'body' do |b| 63 | b.add 'yonks' do |y| 64 | y.set_value 'bold' 65 | end 66 | end 67 | end 68 | contents = doc.to_s.match(/([\w\W]*)<\/yonks>/)[1] 69 | assert_equal "<b>bold</b>", contents 70 | end 71 | def test_node_contents_is_not_escaped_if_flag_raw 72 | doc = Handsoap::XmlMason::Document.new do |doc| 73 | doc.add 'body' do |b| 74 | b.add 'yonks' do |y| 75 | y.set_value 'bold', :raw 76 | end 77 | end 78 | end 79 | contents = doc.to_s.match(/([\w\W]*)<\/yonks>/)[1] 80 | assert_equal "bold", contents 81 | end 82 | def test_finder_can_locate_node_by_nodename 83 | node = nil 84 | doc = Handsoap::XmlMason::Document.new do |doc| 85 | doc.add 'body' do |b| 86 | b.add 'yonks', "lorem\nipsum\ndolor\nsit amet" 87 | b.add 'ninja' do |n| 88 | node = n 89 | n.set_value "ninja" 90 | end 91 | b.add 'ninjitsu' do |n| 92 | n.set_value "ninjitsu" 93 | end 94 | end 95 | end 96 | assert_equal "ninjitsu", node.document.find('ninjitsu').to_s 97 | assert_equal "ninjitsu", node.document.find(:ninjitsu).to_s 98 | end 99 | def test_xml_header_is_optional 100 | doc = Handsoap::XmlMason::Document.new do |doc| 101 | doc.add "foo", "Lorem Ipsum" 102 | end 103 | doc.xml_header = false 104 | assert_equal "Lorem Ipsum", doc.to_s 105 | end 106 | end 107 | 108 | =begin 109 | 110 | # doc = Handsoap::XmlMason::Document.new do |doc| 111 | # doc.alias 'env', "http://www.w3.org/2003/05/soap-envelope" 112 | # doc.alias 'm', "http://travelcompany.example.org/reservation" 113 | # doc.alias 'n', "http://mycompany.example.com/employees" 114 | # doc.alias 'p', "http://travelcompany.example.org/reservation/travel" 115 | # doc.alias 'q', "http://travelcompany.example.org/reservation/hotels" 116 | 117 | # doc.add "env:Envelope" do |env| 118 | # env.add "Header" do |header| 119 | # header.add 'm:reservation' do |r| 120 | # r.set_attr 'env:role', "http://www.w3.org/2003/05/soap-envelope/role/next" 121 | # r.set_attr 'env:mustUnderstand', "true" 122 | # r.add 'reference', "uuid:093a2da1-q345-739r-ba5d-pqff98fe8j7d" 123 | # r.add 'dateAndTime', "2001-11-29T13:20:00.000-05:00" 124 | # end 125 | # header.add 'n:passenger' do |p| 126 | # p.set_attr 'env:role', "http://www.w3.org/2003/05/soap-envelope/role/next" 127 | # p.set_attr 'env:mustUnderstand', "true" 128 | # p.add 'name', "Åke Jógvan Øyvind" 129 | # end 130 | # end 131 | # env.add "Body" do |body| 132 | # body.add 'p:itinerary' do |i| 133 | # i.add 'departure' do |d| 134 | # d.add 'departing', "New York" 135 | # d.add 'arriving', "Los Angeles" 136 | # d.add 'departureDate', "2001-12-14" 137 | # d.add 'departureTime', "late afternoon" 138 | # d.add 'seatPreference', "aisle" 139 | # end 140 | # i.add 'return' do |r| 141 | # r.add 'departing', "Los Angeles" 142 | # r.add 'arriving', "New York" 143 | # r.add 'departureDate', "2001-12-20" 144 | # r.add 'departureTime', "mid-morning" 145 | # r.add 'seatPreference' 146 | # end 147 | # end 148 | # body.add 'q:lodging' do |l| 149 | # l.add 'preference', "none" 150 | # end 151 | # end 152 | # end 153 | # end 154 | 155 | # puts doc 156 | # puts doc.find("Body") 157 | # puts doc.find_all("departureTime") 158 | 159 | =end 160 | -------------------------------------------------------------------------------- /tests/xml_query_front_test.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | 4 | $LOAD_PATH << "#{File.dirname(__FILE__)}/../lib/" 5 | require "handsoap" 6 | require 'handsoap/xml_query_front' 7 | 8 | module AbstractXmlDriverTestCase 9 | def xml_source 10 | ' 11 | 13 | 14 | ' + "bl\303\245b\303\246rgr\303\270d" + ' 15 | 16 | blåbærgrød 17 | 19 | 21 | 22 | 3f8ceabd-2d15-47f0-b35e-d52ee868a4a6 23 | 24 | 25 | http://location_to_thumbnail_for_www.alexa.com 27 | www.alexa.com 28 | 29 | 30 | Success 31 | 32 | 33 | 35 | 36 | 3f8ceabd-2d15-47f0-b35e-d52ee868a4a6 37 | 38 | 39 | http://location_to_thumbnail_for_www.amazon.com 41 | www.amazon.com 42 | 43 | 44 | Success 45 | 46 | 47 | 49 | 50 | 3f8ceabd-2d15-47f0-b35e-d52ee868a4a6 51 | 52 | 53 | http://location_to_thumbnail_for_www.a9.com 55 | www.a9.com 56 | 57 | 58 | Success 59 | 60 | 61 | 62 | 63 | ' 64 | end 65 | def create_default_document 66 | doc = Handsoap::XmlQueryFront.parse_string(xml_source, driver) 67 | doc.add_namespace("foo", "http://ast.amazonaws.com/doc/2006-05-15/") 68 | doc.add_namespace("aws", "http://ast.amazonaws.com/doc/2006-05-15/") 69 | doc 70 | end 71 | def test_query_for_undefined_prefix_raises 72 | doc = Handsoap::XmlQueryFront.parse_string(xml_source, driver) 73 | assert_raise RuntimeError do 74 | doc.xpath("//aws:OperationRequest") 75 | end 76 | end 77 | def test_axis_isnt_interpreted_as_a_namespace 78 | doc = Handsoap::XmlQueryFront.parse_string(xml_source, driver) 79 | doc.xpath('/env:Envelope/env:Body/descendant-or-self::env:Fault', { 'env' => "void://" }) 80 | end 81 | def test_query_for_defined_prefix 82 | doc = Handsoap::XmlQueryFront.parse_string(xml_source, driver) 83 | doc.add_namespace("aws", "http://ast.amazonaws.com/doc/2006-05-15/") 84 | doc.xpath("//aws:OperationRequest") 85 | end 86 | def test_get_node_name 87 | doc = create_default_document 88 | assert_equal "Thumbnail", doc.xpath("//aws:Response/aws:ThumbnailResult/*").first.node_name 89 | end 90 | def test_get_node_namespace 91 | doc = create_default_document 92 | assert_equal "http://ast.amazonaws.com/doc/2006-05-15/", doc.xpath("//aws:Response/aws:ThumbnailResult/*").first.node_namespace 93 | end 94 | def test_get_nil_node_namespace 95 | doc = create_default_document 96 | assert_equal nil, doc.xpath("//entity-test").first.node_namespace 97 | end 98 | def test_get_attribute_name 99 | doc = create_default_document 100 | assert_equal "Exists", doc.xpath("//aws:Thumbnail/@Exists").first.node_name 101 | end 102 | def test_get_text_selection_as_string 103 | doc = create_default_document 104 | assert_equal "http://location_to_thumbnail_for_www.alexa.com", doc.xpath("//aws:Thumbnail[1]/text()").to_s 105 | end 106 | def test_query_with_multiple_prefixes_for_same_namespace 107 | doc = create_default_document 108 | assert_equal "3f8ceabd-2d15-47f0-b35e-d52ee868a4a6", doc.xpath("//foo:OperationRequest/aws:RequestId").first.to_s 109 | end 110 | def test_hpricot_style_searching_is_supported 111 | doc = create_default_document 112 | assert_equal "3f8ceabd-2d15-47f0-b35e-d52ee868a4a6", (doc/"//foo:OperationRequest/aws:RequestId").first.to_s 113 | end 114 | def test_query_result_is_mappable 115 | doc = create_default_document 116 | assert_equal "3f8ceabd-2d15-47f0-b35e-d52ee868a4a6\n3f8ceabd-2d15-47f0-b35e-d52ee868a4a6\n3f8ceabd-2d15-47f0-b35e-d52ee868a4a6", doc.xpath("//foo:OperationRequest/aws:RequestId").map{|e| e.to_s }.join("\n") 117 | end 118 | def test_resultset_inherits_prefixes 119 | doc = create_default_document 120 | assert_equal "3f8ceabd-2d15-47f0-b35e-d52ee868a4a6", doc.xpath("//foo:OperationRequest").first.xpath("aws:RequestId").first.to_s 121 | end 122 | def test_resultset_delegates_slash 123 | doc = create_default_document 124 | operation_request = (doc/"//foo:OperationRequest") 125 | assert_equal "3f8ceabd-2d15-47f0-b35e-d52ee868a4a6", (operation_request/"aws:RequestId").to_s 126 | end 127 | def test_attribute_can_cast_to_boolean 128 | doc = create_default_document 129 | assert_kind_of TrueClass, doc.xpath("//aws:Thumbnail/@Exists").first.to_boolean 130 | end 131 | def test_text_content_is_utf8 132 | doc = create_default_document 133 | assert_equal "bl\303\245b\303\246rgr\303\270d", doc.xpath("//encoding-test").first.to_s 134 | end 135 | def test_cdata_has_no_surrounding_markers 136 | doc = create_default_document 137 | assert_equal "character data", doc.xpath("//cdata-test").first.to_s 138 | end 139 | def test_entity_escaped_text_content_is_utf8 140 | doc = create_default_document 141 | assert_equal "bl\303\245b\303\246rgr\303\270d", doc.xpath("//entity-test").first.to_s 142 | end 143 | def test_entity_escaped_attribute_is_utf8 144 | doc = create_default_document 145 | assert_equal "bl\303\245b\303\246rgr\303\270d", doc.xpath("//aws:Thumbnail/@attr-test").first.to_s 146 | end 147 | def test_error_on_parsing_non_xml 148 | assert_raise Handsoap::XmlQueryFront::ParseError do 149 | doc = Handsoap::XmlQueryFront.parse_string("blah", driver) 150 | end 151 | end 152 | def test_error_on_parsing_empty_string 153 | assert_raise Handsoap::XmlQueryFront::ParseError do 154 | doc = Handsoap::XmlQueryFront.parse_string("", driver) 155 | end 156 | end 157 | def test_error_on_parsing_empty_document 158 | assert_raise Handsoap::XmlQueryFront::ParseError do 159 | doc = Handsoap::XmlQueryFront.parse_string("", driver) 160 | end 161 | end 162 | def test_serialize_pretty 163 | doc = Handsoap::XmlQueryFront.parse_string('blah', driver) 164 | assert_equal "\n blah\n", doc.xpath("//foo").to_xml 165 | end 166 | def test_serialize_raw 167 | str = "\n\t\tblah\n\n" 168 | doc = Handsoap::XmlQueryFront.parse_string("" + str, driver) 169 | assert_equal str, doc.xpath("//foo").to_raw 170 | end 171 | def test_an_unformatted_string_can_be_serialized_raw 172 | doc = Handsoap::XmlQueryFront.parse_string('blah', driver) 173 | assert_equal "blah", doc.xpath("//foo").to_raw 174 | end 175 | def test_query_by_syntactic_sugar 176 | doc = create_default_document 177 | assert_equal 3, (doc/"//aws:OperationRequest[1]/aws:RequestId").to_i 178 | assert_equal (doc/"//aws:OperationRequest[1]/aws:RequestId").to_i, (doc/"//aws:OperationRequest[1]/aws:RequestId").first.to_i 179 | end 180 | def test_attribute_hash_access 181 | doc = create_default_document 182 | node = doc.xpath("//aws:Thumbnail").first 183 | assert_equal "bl\303\245b\303\246rgr\303\270d", node['attr-test'] 184 | end 185 | def test_attribute_hash_access_fails_with_a_symbol 186 | doc = create_default_document 187 | node = doc.xpath("//aws:Thumbnail").first 188 | assert_raise ArgumentError do 189 | assert_equal "foobar", node[:foo] 190 | end 191 | end 192 | def test_select_children 193 | doc = create_default_document 194 | node = doc.xpath("//aws:ThumbnailResponse").first 195 | result = node.children.map { |node| node.node_name }.join(",") 196 | assert_equal "text,Response,text,Response,text,Response,text", result 197 | end 198 | end 199 | 200 | class TestOfREXMLDriver < Test::Unit::TestCase 201 | include AbstractXmlDriverTestCase 202 | def driver 203 | :rexml 204 | end 205 | end 206 | 207 | class TestOfNokogiriDriver < Test::Unit::TestCase 208 | include AbstractXmlDriverTestCase 209 | def driver 210 | :nokogiri 211 | end 212 | end 213 | 214 | class TestOfLibXMLDriver < Test::Unit::TestCase 215 | include AbstractXmlDriverTestCase 216 | def driver 217 | :libxml 218 | end 219 | end 220 | --------------------------------------------------------------------------------