├── .rspec ├── Gemfile ├── lib ├── savon │ ├── version.rb │ ├── error.rb │ ├── core_ext │ │ ├── object.rb │ │ └── string.rb │ ├── soap.rb │ ├── hooks │ │ ├── hook.rb │ │ └── group.rb │ ├── http │ │ └── error.rb │ ├── wasabi │ │ └── document.rb │ ├── soap │ │ ├── fault.rb │ │ ├── request.rb │ │ ├── response.rb │ │ └── xml.rb │ ├── model.rb │ ├── global.rb │ └── client.rb └── savon.rb ├── spec ├── fixtures │ ├── gzip │ │ └── message.gz │ ├── response │ │ ├── soap_fault.xml │ │ ├── header.xml │ │ ├── taxcloud.xml │ │ ├── soap_fault12.xml │ │ ├── authentication.xml │ │ ├── another_soap_fault.xml │ │ ├── list.xml │ │ └── multi_ref.xml │ └── wsdl │ │ ├── lower_camel.xml │ │ ├── multiple_types.xml │ │ ├── multiple_namespaces.xml │ │ ├── authentication.xml │ │ └── taxcloud.xml ├── spec_helper.rb ├── savon │ ├── core_ext │ │ ├── object_spec.rb │ │ └── string_spec.rb │ ├── soap_spec.rb │ ├── wasabi │ │ └── document_spec.rb │ ├── http │ │ └── error_spec.rb │ ├── soap │ │ ├── request_spec.rb │ │ ├── fault_spec.rb │ │ ├── response_spec.rb │ │ └── xml_spec.rb │ ├── savon_spec.rb │ ├── model_spec.rb │ └── client_spec.rb └── support │ ├── fixture.rb │ └── endpoint.rb ├── .gitignore ├── Rakefile ├── .travis.yml ├── LICENSE ├── README.md ├── savon.gemspec └── CHANGELOG.md /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | 4 | gem "httpclient", "~> 2.1.5" 5 | -------------------------------------------------------------------------------- /lib/savon/version.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | 3 | Version = "1.0.0" 4 | 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/gzip/message.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyric/savon/master/spec/fixtures/gzip/message.gz -------------------------------------------------------------------------------- /lib/savon/error.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | 3 | # Base class for Savon errors. 4 | class Error < RuntimeError; end 5 | 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .yardoc 3 | doc 4 | coverage 5 | tmp 6 | *.rbc 7 | *~ 8 | *.gem 9 | .bundle 10 | Gemfile.lock 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new 5 | 6 | task :default => :spec 7 | task :test => :spec 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/travis-ci/travis-ci/wiki/.travis.yml-options 2 | script: "bundle exec rake" 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - ree 8 | - rbx 9 | - rbx-2.0 10 | - jruby 11 | notifications: 12 | irc: "irc.freenode.org#savon" 13 | -------------------------------------------------------------------------------- /spec/fixtures/response/soap_fault.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | soap:Server 5 | Fault occurred while processing. 6 | 7 | 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | Bundler.require :default, :development 3 | 4 | RSpec.configure do |config| 5 | config.mock_with :mocha 6 | end 7 | 8 | # Disable logging and deprecations for specs. 9 | Savon.configure do |config| 10 | config.log = false 11 | config.deprecate = false 12 | end 13 | 14 | require "support/endpoint" 15 | require "support/fixture" 16 | -------------------------------------------------------------------------------- /lib/savon.rb: -------------------------------------------------------------------------------- 1 | require "savon/version" 2 | require "savon/global" 3 | require "savon/client" 4 | require "savon/model" 5 | 6 | module Savon 7 | extend Global 8 | 9 | # Yields this module to a given +block+. Please refer to the 10 | # Savon::Global module for configuration options. 11 | def self.configure 12 | yield self if block_given? 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/savon/core_ext/object.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | module CoreExt 3 | module Object 4 | 5 | # Returns +true+ if the Object is nil, false or empty. Implementation from ActiveSupport. 6 | def blank? 7 | respond_to?(:empty?) ? empty? : !self 8 | end unless method_defined?(:blank?) 9 | 10 | end 11 | end 12 | end 13 | 14 | Object.send :include, Savon::CoreExt::Object 15 | -------------------------------------------------------------------------------- /lib/savon/soap.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | 3 | # = Savon::SOAP 4 | # 5 | # Contains various SOAP details. 6 | module SOAP 7 | 8 | # Default SOAP version. 9 | DefaultVersion = 1 10 | 11 | # Supported SOAP versions. 12 | Versions = 1..2 13 | 14 | # SOAP namespaces by SOAP version. 15 | Namespace = { 16 | 1 => "http://schemas.xmlsoap.org/soap/envelope/", 17 | 2 => "http://www.w3.org/2003/05/soap-envelope" 18 | } 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/savon/core_ext/object_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Object do 4 | 5 | describe "blank?" do 6 | it "returns true for Objects perceived to be blank" do 7 | ["", false, nil, [], {}].each do |object| 8 | object.should be_blank 9 | end 10 | end 11 | 12 | it "returns false for every other Object" do 13 | ["!blank", true, [:a], {:a => "b"}].each do |object| 14 | object.should_not be_blank 15 | end 16 | end 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /spec/savon/soap_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::SOAP do 4 | 5 | it "should contain the SOAP namespace for each supported SOAP version" do 6 | Savon::SOAP::Versions.each do |soap_version| 7 | Savon::SOAP::Namespace[soap_version].should be_a(String) 8 | Savon::SOAP::Namespace[soap_version].should_not be_empty 9 | end 10 | end 11 | 12 | it "should contain a Rage of supported SOAP versions" do 13 | Savon::SOAP::Versions.should == (1..2) 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /spec/fixtures/response/header.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ABCD1234 5 | 6 | 7 | 8 | 9 | P 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/fixtures/response/taxcloud.xml: -------------------------------------------------------------------------------- 1 | ErrorErrorInvalid apiLoginID and/or apiKey 2 | -------------------------------------------------------------------------------- /lib/savon/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | require "savon/soap" 2 | 3 | module Savon 4 | module CoreExt 5 | module String 6 | 7 | # Returns the String in snake_case. 8 | def snakecase 9 | str = dup 10 | str.gsub! /::/, '/' 11 | str.gsub! /([A-Z]+)([A-Z][a-z])/, '\1_\2' 12 | str.gsub! /([a-z\d])([A-Z])/, '\1_\2' 13 | str.tr! ".", "_" 14 | str.tr! "-", "_" 15 | str.downcase! 16 | str 17 | end unless method_defined?(:snakecase) 18 | 19 | end 20 | end 21 | end 22 | 23 | String.send :include, Savon::CoreExt::String 24 | -------------------------------------------------------------------------------- /spec/fixtures/response/soap_fault12.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | soap:Sender 6 | 7 | m:MessageTimeout 8 | 9 | 10 | 11 | Sender Timeout 12 | 13 | 14 | P5M 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/fixtures/response/authentication.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | a68d1d6379b62ff339a0e0c69ed4d9cf 7 | AAAJxA;cIedoT;mY10ExZwG6JuKgp2OYKxow== 8 | radclient 9 | 10 | true 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/response/another_soap_fault.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | ERR_NO_SESSION 9 | doGetItemsInfo - Wrong session 10 | Wrong session message 11 | 80:1289245853:55 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/support/fixture.rb: -------------------------------------------------------------------------------- 1 | class Fixture 2 | 3 | TYPES = { :gzip => "gz", :response => "xml", :wsdl => "xml" } 4 | 5 | class << self 6 | 7 | def [](type, fixture) 8 | fixtures(type)[fixture] ||= read_file type, fixture 9 | end 10 | 11 | def response_hash(fixture) 12 | @response_hash ||= {} 13 | @response_hash[fixture] ||= Nori.parse(response(fixture))[:envelope][:body] 14 | end 15 | 16 | TYPES.each do |type, ext| 17 | define_method(type) { |fixture| self[type, fixture] } 18 | end 19 | 20 | private 21 | 22 | def fixtures(type) 23 | @fixtures ||= {} 24 | @fixtures[type] ||= {} 25 | end 26 | 27 | def read_file(type, fixture) 28 | path = File.expand_path "../../fixtures/#{type}/#{fixture}.#{TYPES[type]}", __FILE__ 29 | raise ArgumentError, "Unable to load: #{path}" unless File.exist? path 30 | 31 | File.read path 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/support/endpoint.rb: -------------------------------------------------------------------------------- 1 | class Endpoint 2 | class << self 3 | 4 | # Returns the WSDL endpoint for a given +type+ of request. 5 | def wsdl(type = nil) 6 | case type 7 | when :no_namespace then "http://nons.example.com/Service?wsdl" 8 | when :namespaced_actions then "http://nsactions.example.com/Service?wsdl" 9 | when :geotrust then "https://test-api.geotrust.com/webtrust/query.jws?WSDL" 10 | else soap(type) 11 | end 12 | end 13 | 14 | # Returns the SOAP endpoint for a given +type+ of request. 15 | def soap(type = nil) 16 | case type 17 | when :soap_fault then "http://soapfault.example.com/Service?wsdl" 18 | when :http_error then "http://httperror.example.com/Service?wsdl" 19 | when :invalid then "http://invalid.example.com/Service?wsdl" 20 | else "http://example.com/validation/1.0/AuthenticationService" 21 | end 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/savon/hooks/hook.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | module Hooks 3 | 4 | # = Savon::Hooks::Hook 5 | # 6 | # A hook used somewhere in the system. 7 | class Hook 8 | 9 | HOOKS = [ 10 | 11 | # Replaces the POST request executed to call a service. 12 | # See: Savon::SOAP::Request#response 13 | # 14 | # Receives the Savon::SOAP::Request and is expected to return an HTTPI::Response. 15 | # It can change the request and return something falsy to still execute the POST request. 16 | :soap_request 17 | 18 | ] 19 | 20 | # Expects an +id+, the name of the +hook+ to use and a +block+ to be called. 21 | def initialize(id, hook, &block) 22 | self.id = id 23 | self.hook = hook 24 | self.block = block 25 | end 26 | 27 | attr_accessor :id, :hook, :block 28 | 29 | # Calls the +block+ with the given +args+. 30 | def call(*args) 31 | block.call(*args) 32 | end 33 | 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/fixtures/response/list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 2010-09-21T18:22:01.558+10:00 7 | Notes Log 8 | test 9 | 10 | 11 | 2010-09-21T18:22:07.038+10:00 12 | Notes Log 13 | another test 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Daniel Harrington 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/savon/http/error.rb: -------------------------------------------------------------------------------- 1 | require "savon/error" 2 | require "savon/soap/xml" 3 | 4 | module Savon 5 | module HTTP 6 | 7 | # = Savon::HTTP::Error 8 | # 9 | # Represents an HTTP error. Contains the original HTTPI::Response. 10 | class Error < Error 11 | 12 | # Expects an HTTPI::Response. 13 | def initialize(http) 14 | self.http = http 15 | end 16 | 17 | # Accessor for the HTTPI::Response. 18 | attr_accessor :http 19 | 20 | # Returns whether an HTTP error is present. 21 | def present? 22 | http.error? 23 | end 24 | 25 | # Returns the HTTP error message. 26 | def to_s 27 | return "" unless present? 28 | 29 | @message ||= begin 30 | message = "HTTP error (#{http.code})" 31 | message << ": #{http.body}" unless http.body.empty? 32 | end 33 | end 34 | 35 | # Returns the HTTP response as a Hash. 36 | def to_hash 37 | @hash = { :code => http.code, :headers => http.headers, :body => http.body } 38 | end 39 | 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/savon/hooks/group.rb: -------------------------------------------------------------------------------- 1 | require "savon/hooks/hook" 2 | 3 | module Savon 4 | module Hooks 5 | 6 | # = Savon::Hooks::Group 7 | # 8 | # Manages a list of hooks. 9 | class Group 10 | 11 | # Accepts an Array of +hooks+ to start with. 12 | def initialize(hooks = nil) 13 | self.hooks = hooks 14 | end 15 | 16 | attr_writer :hooks 17 | 18 | def hooks 19 | @hooks ||= [] 20 | end 21 | 22 | # Adds a new hook. 23 | def define(id, hook, &block) 24 | hooks << Hook.new(id, hook, &block) 25 | end 26 | 27 | # Removes hooks matching the given +ids+. 28 | def reject!(*ids) 29 | ids = ids.flatten 30 | hooks.reject! { |hook| ids.include? hook.id } 31 | end 32 | 33 | # Returns a new group for a given +hook+. 34 | def select(hook) 35 | Group.new hooks.select { |h| h.hook == hook } 36 | end 37 | 38 | # Calls the hooks with the given +args+ and returns the 39 | # value of the last hooks. 40 | def call(*args) 41 | hooks.inject(nil) { |memo, hook| hook.call(*args) } 42 | end 43 | 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/savon/core_ext/string_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe String do 4 | 5 | describe "snakecase" do 6 | it "lowercases one word CamelCase" do 7 | "Merb".snakecase.should == "merb" 8 | end 9 | 10 | it "makes one underscore snakecase two word CamelCase" do 11 | "MerbCore".snakecase.should == "merb_core" 12 | end 13 | 14 | it "handles CamelCase with more than 2 words" do 15 | "SoYouWantContributeToMerbCore".snakecase.should == "so_you_want_contribute_to_merb_core" 16 | end 17 | 18 | it "handles CamelCase with more than 2 capital letter in a row" do 19 | "CNN".snakecase.should == "cnn" 20 | "CNNNews".snakecase.should == "cnn_news" 21 | "HeadlineCNNNews".snakecase.should == "headline_cnn_news" 22 | end 23 | 24 | it "does NOT change one word lowercase" do 25 | "merb".snakecase.should == "merb" 26 | end 27 | 28 | it "leaves snake_case as is" do 29 | "merb_core".snakecase.should == "merb_core" 30 | end 31 | 32 | it "converts period characters to underscores" do 33 | "User.GetEmail".snakecase.should == "user_get_email" 34 | end 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Savon [![Build Status](https://secure.travis-ci.org/rubiii/savon.png)](http://travis-ci.org/rubiii/savon) 2 | ===== 3 | 4 | Heavy metal Ruby SOAP client 5 | 6 | [Documentation](http://savonrb.com) | [RDoc](http://rubydoc.info/gems/savon) | 7 | [Mailing list](https://groups.google.com/forum/#!forum/savonrb) | [Twitter](http://twitter.com/savonrb) 8 | 9 | Installation 10 | ------------ 11 | 12 | Savon is available through [Rubygems](http://rubygems.org/gems/savon) and can be installed via: 13 | 14 | ``` 15 | $ gem install savon 16 | ``` 17 | 18 | Introduction 19 | ------------ 20 | 21 | ``` ruby 22 | require "savon" 23 | 24 | # create a client for your SOAP service 25 | client = Savon::Client.new("http://service.example.com?wsdl") 26 | 27 | client.wsdl.soap_actions 28 | # => [:create_user, :get_user, :get_all_users] 29 | 30 | # execute a SOAP request to call the "getUser" action 31 | response = client.request(:get_user) do 32 | soap.body = { :id => 1 } 33 | end 34 | 35 | response.body 36 | # => { :get_user_response => { :first_name => "The", :last_name => "Hoff" } } 37 | ``` 38 | 39 | Documentation 40 | ------------- 41 | 42 | Continue reading at [savonrb.com](http://savonrb.com) 43 | -------------------------------------------------------------------------------- /lib/savon/wasabi/document.rb: -------------------------------------------------------------------------------- 1 | require "wasabi" 2 | require "httpi/request" 3 | 4 | module Savon 5 | module Wasabi 6 | 7 | # = Savon::Wasabi::Document 8 | # 9 | # Extends the document handling of the Wasabi::Document by 10 | # adding support for remote and local WSDL documents. 11 | class Document < ::Wasabi::Document 12 | 13 | # Hooks into Wasabi and extends its document handling. 14 | def xml 15 | @xml ||= document.kind_of?(String) ? resolve_document : document 16 | end 17 | 18 | # Sets the HTTPI::Request for remote WSDL documents. 19 | attr_writer :request 20 | 21 | private 22 | 23 | # Sets up and returns the HTTPI::Request. 24 | def request 25 | @request ||= HTTPI::Request.new 26 | @request.url = document 27 | @request 28 | end 29 | 30 | # Resolves and returns the raw WSDL document. 31 | def resolve_document 32 | case document 33 | when /^http[s]?:/ then HTTPI.get(request).body 34 | when /^= 2.1.2" 18 | s.add_dependency "nori", "~> 1.0" 19 | s.add_dependency "httpi", "~> 0.9" 20 | s.add_dependency "wasabi", "~> 2.0" 21 | s.add_dependency "akami", "~> 1.0" 22 | s.add_dependency "gyoku", ">= 0.4.0" 23 | s.add_dependency "nokogiri", ">= 1.4.0" 24 | 25 | s.add_development_dependency "rake", "~> 0.8.7" 26 | s.add_development_dependency "rspec", "~> 2.5.0" 27 | s.add_development_dependency "mocha", "~> 0.9.8" 28 | s.add_development_dependency "timecop", "~> 0.3.5" 29 | 30 | s.add_development_dependency "autotest" 31 | s.add_development_dependency "ZenTest", "4.5.0" 32 | 33 | s.files = `git ls-files`.split("\n") 34 | s.require_path = "lib" 35 | end 36 | -------------------------------------------------------------------------------- /spec/savon/wasabi/document_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::Wasabi::Document do 4 | 5 | context "with a remote document" do 6 | before do 7 | response = HTTPI::Response.new 200, {}, Fixture.wsdl(:authentication) 8 | HTTPI.stubs(:get).returns(response) 9 | end 10 | 11 | it "should resolve via HTTP" do 12 | wsdl = Savon::Wasabi::Document.new("http://example.com?wsdl") 13 | wsdl.xml.should == Fixture.wsdl(:authentication) 14 | end 15 | 16 | it "should resolve via HTTPS" do 17 | wsdl = Savon::Wasabi::Document.new("https://example.com?wsdl") 18 | wsdl.xml.should == Fixture.wsdl(:authentication) 19 | end 20 | end 21 | 22 | context "with a local document" do 23 | before do 24 | HTTPI.expects(:get).never 25 | end 26 | 27 | it "should read the file" do 28 | wsdl = Savon::Wasabi::Document.new("spec/fixtures/wsdl/authentication.xml") 29 | wsdl.xml.should == Fixture.wsdl(:authentication) 30 | end 31 | end 32 | 33 | context "with raw XML" do 34 | before do 35 | HTTPI.expects(:get).never 36 | File.expects(:read).never 37 | end 38 | 39 | it "should use the raw XML" do 40 | wsdl = Savon::Wasabi::Document.new Fixture.wsdl(:authentication) 41 | wsdl.xml.should == Fixture.wsdl(:authentication) 42 | end 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /spec/savon/http/error_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::HTTP::Error do 4 | let(:http_error) { Savon::HTTP::Error.new new_response(:code => 404, :body => "Not Found") } 5 | let(:no_error) { Savon::HTTP::Error.new new_response } 6 | 7 | it "should be a Savon::Error" do 8 | Savon::HTTP::Error.should < Savon::Error 9 | end 10 | 11 | describe "#http" do 12 | it "should return the HTTPI::Response" do 13 | http_error.http.should be_an(HTTPI::Response) 14 | end 15 | end 16 | 17 | describe "#present?" do 18 | it "should return true if there was an HTTP error" do 19 | http_error.should be_present 20 | end 21 | 22 | it "should return false unless there was an HTTP error" do 23 | no_error.should_not be_present 24 | end 25 | end 26 | 27 | [:message, :to_s].each do |method| 28 | describe "##{method}" do 29 | it "should return an empty String unless an HTTP error is present" do 30 | no_error.send(method).should == "" 31 | end 32 | 33 | it "should return the HTTP error message" do 34 | http_error.send(method).should == "HTTP error (404): Not Found" 35 | end 36 | end 37 | end 38 | 39 | describe "#to_hash" do 40 | it "should return the HTTP response details as a Hash" do 41 | http_error.to_hash.should == { :code => 404, :headers => {}, :body => "Not Found" } 42 | end 43 | end 44 | 45 | def new_response(options = {}) 46 | defaults = { :code => 200, :headers => {}, :body => Fixture.response(:authentication) } 47 | response = defaults.merge options 48 | 49 | HTTPI::Response.new response[:code], response[:headers], response[:body] 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/savon/soap/fault.rb: -------------------------------------------------------------------------------- 1 | require "savon/error" 2 | require "savon/soap/xml" 3 | 4 | module Savon 5 | module SOAP 6 | 7 | # = Savon::SOAP::Fault 8 | # 9 | # Represents a SOAP fault. Contains the original HTTPI::Response. 10 | class Fault < Error 11 | 12 | # Expects an HTTPI::Response. 13 | def initialize(http) 14 | self.http = http 15 | end 16 | 17 | # Accessor for the HTTPI::Response. 18 | attr_accessor :http 19 | 20 | # Returns whether a SOAP fault is present. 21 | def present? 22 | @present ||= http.body.include?("Fault>") && (soap1_fault? || soap2_fault?) 23 | end 24 | 25 | # Returns the SOAP fault message. 26 | def to_s 27 | return "" unless present? 28 | @message ||= message_by_version to_hash[:fault] 29 | end 30 | 31 | # Returns the SOAP response body as a Hash. 32 | def to_hash 33 | @hash ||= Nori.parse(http.body)[:envelope][:body] 34 | end 35 | 36 | private 37 | 38 | # Returns whether the response contains a SOAP 1.1 fault. 39 | def soap1_fault? 40 | http.body.include?("faultcode>") && http.body.include?("faultstring>") 41 | end 42 | 43 | # Returns whether the response contains a SOAP 1.2 fault. 44 | def soap2_fault? 45 | http.body.include?("Code>") && http.body.include?("Reason>") 46 | end 47 | 48 | # Returns the SOAP fault message by version. 49 | def message_by_version(fault) 50 | if fault[:faultcode] 51 | "(#{fault[:faultcode]}) #{fault[:faultstring]}" 52 | elsif fault[:code] 53 | "(#{fault[:code][:value]}) #{fault[:reason][:text]}" 54 | end 55 | end 56 | 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/fixtures/wsdl/lower_camel.xml: -------------------------------------------------------------------------------- 1 | 2 | 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 | -------------------------------------------------------------------------------- /lib/savon/soap/request.rb: -------------------------------------------------------------------------------- 1 | require "httpi" 2 | require "savon/soap/response" 3 | 4 | module Savon 5 | module SOAP 6 | 7 | # = Savon::SOAP::Request 8 | # 9 | # Executes SOAP requests. 10 | class Request 11 | 12 | # Content-Types by SOAP version. 13 | ContentType = { 1 => "text/xml;charset=UTF-8", 2 => "application/soap+xml;charset=UTF-8" } 14 | 15 | # Expects an HTTPI::Request and a Savon::SOAP::XML object 16 | # to execute a SOAP request and returns the response. 17 | def self.execute(http, soap) 18 | new(http, soap).response 19 | end 20 | 21 | # Expects an HTTPI::Request and a Savon::SOAP::XML object. 22 | def initialize(http, soap) 23 | self.soap = soap 24 | self.http = configure(http) 25 | end 26 | 27 | attr_accessor :soap, :http 28 | 29 | # Executes the request and returns the response. 30 | def response 31 | @response ||= SOAP::Response.new( 32 | Savon.hooks.select(:soap_request).call(self) || with_logging { HTTPI.post(http) } 33 | ) 34 | end 35 | 36 | private 37 | 38 | # Configures a given +http+ from the +soap+ object. 39 | def configure(http) 40 | http.url = soap.endpoint 41 | http.body = soap.to_xml 42 | http.headers["Content-Type"] ||= ContentType[soap.version] 43 | http.headers["Content-Length"] ||= soap.to_xml.length.to_s 44 | 45 | http 46 | end 47 | 48 | # Logs the HTTP request, yields to a given +block+ and returns a Savon::SOAP::Response. 49 | def with_logging 50 | log_request http.url, http.headers, http.body 51 | response = yield 52 | log_response response.code, response.body 53 | response 54 | end 55 | 56 | # Logs the SOAP request +url+, +headers+ and +body+. 57 | def log_request(url, headers, body) 58 | Savon.log "SOAP request: #{url}" 59 | Savon.log headers.map { |key, value| "#{key}: #{value}" }.join(", ") 60 | Savon.log body 61 | end 62 | 63 | # Logs the SOAP response +code+ and +body+. 64 | def log_response(code, body) 65 | Savon.log "SOAP response (status #{code}):" 66 | Savon.log body 67 | end 68 | 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/savon/soap/request_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::SOAP::Request do 4 | let(:soap_request) { Savon::SOAP::Request.new HTTPI::Request.new, soap } 5 | let(:soap) { Savon::SOAP::XML.new Endpoint.soap, [nil, :get_user, {}], :id => 1 } 6 | 7 | it "contains the content type for each supported SOAP version" do 8 | content_type = Savon::SOAP::Request::ContentType 9 | content_type[1].should == "text/xml;charset=UTF-8" 10 | content_type[2].should == "application/soap+xml;charset=UTF-8" 11 | end 12 | 13 | describe ".execute" do 14 | it "executes a SOAP request and returns the response" do 15 | HTTPI.expects(:post).returns(HTTPI::Response.new 200, {}, Fixture.response(:authentication)) 16 | response = Savon::SOAP::Request.execute HTTPI::Request.new, soap 17 | response.should be_a(Savon::SOAP::Response) 18 | end 19 | end 20 | 21 | describe ".new" do 22 | it "uses the SOAP endpoint for the request" do 23 | soap_request.http.url.should == URI(soap.endpoint) 24 | end 25 | 26 | it "sets the SOAP body for the request" do 27 | soap_request.http.body.should == soap.to_xml 28 | end 29 | 30 | it "sets the Content-Type header for SOAP 1.1" do 31 | soap_request.http.headers["Content-Type"].should == Savon::SOAP::Request::ContentType[1] 32 | end 33 | 34 | it "sets the Content-Type header for SOAP 1.2" do 35 | soap.version = 2 36 | soap_request.http.headers["Content-Type"].should == Savon::SOAP::Request::ContentType[2] 37 | end 38 | 39 | it "does not set the Content-Type header if it's already specified" do 40 | headers = { "Content-Type" => "text/plain" } 41 | soap_request = Savon::SOAP::Request.new HTTPI::Request.new(:headers => headers), soap 42 | soap_request.http.headers["Content-Type"].should == headers["Content-Type"] 43 | end 44 | 45 | it "sets the Content-Length header" do 46 | soap_request.http.headers["Content-Length"].should == soap.to_xml.length.to_s 47 | end 48 | end 49 | 50 | describe "#response" do 51 | it "executes an HTTP POST request and returns a Savon::SOAP::Response" do 52 | HTTPI.expects(:post).returns(HTTPI::Response.new 200, {}, Fixture.response(:authentication)) 53 | soap_request.response.should be_a(Savon::SOAP::Response) 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /spec/savon/savon_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon do 4 | 5 | describe ".configure" do 6 | around do |example| 7 | Savon.reset_config! 8 | example.run 9 | Savon.reset_config! 10 | Savon.log = false # disable logging 11 | end 12 | 13 | describe "log" do 14 | it "should default to true" do 15 | Savon.log?.should be_true 16 | end 17 | 18 | it "should set whether to log HTTP requests" do 19 | Savon.configure { |config| config.log = false } 20 | Savon.log?.should be_false 21 | end 22 | end 23 | 24 | describe "logger" do 25 | it "should set the logger to use" do 26 | MyLogger = Class.new 27 | Savon.configure { |config| config.logger = MyLogger } 28 | Savon.logger.should == MyLogger 29 | end 30 | 31 | it "should default to Logger writing to STDOUT" do 32 | Savon.logger.should be_a(Logger) 33 | end 34 | end 35 | 36 | describe "log_level" do 37 | it "should default to :debug" do 38 | Savon.log_level.should == :debug 39 | end 40 | 41 | it "should set the log level to use" do 42 | Savon.configure { |config| config.log_level = :info } 43 | Savon.log_level.should == :info 44 | end 45 | end 46 | 47 | describe "raise_errors" do 48 | it "should default to true" do 49 | Savon.raise_errors?.should be_true 50 | end 51 | 52 | it "should not raise errors when disabled" do 53 | Savon.raise_errors = false 54 | Savon.raise_errors?.should be_false 55 | end 56 | end 57 | 58 | describe "soap_version" do 59 | it "should default to SOAP 1.1" do 60 | Savon.soap_version.should == 1 61 | end 62 | 63 | it "should return 2 if set to SOAP 1.2" do 64 | Savon.soap_version = 2 65 | Savon.soap_version.should == 2 66 | end 67 | 68 | it "should raise an ArgumentError in case of an invalid version" do 69 | lambda { Savon.soap_version = 3 }.should raise_error(ArgumentError) 70 | end 71 | end 72 | 73 | describe "strip_namespaces" do 74 | it "should default to true" do 75 | Savon.strip_namespaces?.should == true 76 | end 77 | 78 | it "should not strip namespaces when set to false" do 79 | Savon.strip_namespaces = false 80 | Savon.strip_namespaces?.should == false 81 | end 82 | end 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /spec/fixtures/wsdl/multiple_types.xml: -------------------------------------------------------------------------------- 1 | 2 | 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 | -------------------------------------------------------------------------------- /spec/fixtures/wsdl/multiple_namespaces.xml: -------------------------------------------------------------------------------- 1 | 2 | 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 | -------------------------------------------------------------------------------- /lib/savon/model.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | 3 | # = Savon::Model 4 | # 5 | # Model for SOAP service oriented applications. 6 | module Model 7 | 8 | def self.extended(base) 9 | base.setup 10 | end 11 | 12 | def setup 13 | class_action_module 14 | instance_action_module 15 | end 16 | 17 | # Accepts one or more SOAP actions and generates both class and instance methods named 18 | # after the given actions. Each generated method accepts an optional SOAP body Hash and 19 | # a block to be passed to Savon::Client#request and executes a SOAP request. 20 | def actions(*actions) 21 | actions.each do |action| 22 | define_class_action(action) 23 | define_instance_action(action) 24 | end 25 | end 26 | 27 | private 28 | 29 | # Defines a class-level SOAP action method. 30 | def define_class_action(action) 31 | class_action_module.module_eval %{ 32 | def #{action.to_s.snakecase}(body = nil, &block) 33 | response = client.request :wsdl, #{action.inspect}, :body => body, &block 34 | Savon.hooks.select(:model_soap_response).call(response) || response 35 | end 36 | } 37 | end 38 | 39 | # Defines an instance-level SOAP action method. 40 | def define_instance_action(action) 41 | instance_action_module.module_eval %{ 42 | def #{action.to_s.snakecase}(body = nil, &block) 43 | self.class.#{action.to_s.snakecase} body, &block 44 | end 45 | } 46 | end 47 | 48 | # Class methods. 49 | def class_action_module 50 | @class_action_module ||= Module.new do 51 | 52 | # Returns the memoized Savon::Client. 53 | def client(&block) 54 | @client ||= Savon::Client.new(&block) 55 | end 56 | 57 | # Sets the SOAP endpoint to the given +uri+. 58 | def endpoint(uri) 59 | client.wsdl.endpoint = uri 60 | end 61 | 62 | # Sets the target namespace. 63 | def namespace(uri) 64 | client.wsdl.namespace = uri 65 | end 66 | 67 | # Sets the WSDL document to the given +uri+. 68 | def document(uri) 69 | client.wsdl.document = uri 70 | end 71 | 72 | # Sets the HTTP headers. 73 | def headers(headers) 74 | client.http.headers = headers 75 | end 76 | 77 | # Sets basic auth +login+ and +password+. 78 | def basic_auth(login, password) 79 | client.http.auth.basic(login, password) 80 | end 81 | 82 | # Sets WSSE auth credentials. 83 | def wsse_auth(*args) 84 | client.wsse.credentials(*args) 85 | end 86 | 87 | end.tap { |mod| extend(mod) } 88 | end 89 | 90 | # Instance methods. 91 | def instance_action_module 92 | @instance_action_module ||= Module.new do 93 | 94 | # Returns the Savon::Client from the class instance. 95 | def client(&block) 96 | self.class.client(&block) 97 | end 98 | 99 | end.tap { |mod| include(mod) } 100 | end 101 | 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/savon/soap/response.rb: -------------------------------------------------------------------------------- 1 | require "savon/soap/xml" 2 | require "savon/soap/fault" 3 | require "savon/http/error" 4 | 5 | module Savon 6 | module SOAP 7 | 8 | # = Savon::SOAP::Response 9 | # 10 | # Represents the SOAP response and contains the HTTP response. 11 | class Response 12 | 13 | # Expects an HTTPI::Response and handles errors. 14 | def initialize(response) 15 | self.http = response 16 | raise_errors if Savon.raise_errors? 17 | end 18 | 19 | attr_accessor :http 20 | 21 | # Returns whether the request was successful. 22 | def success? 23 | !soap_fault? && !http_error? 24 | end 25 | 26 | # Returns whether there was a SOAP fault. 27 | def soap_fault? 28 | soap_fault.present? 29 | end 30 | 31 | # Returns the Savon::SOAP::Fault. 32 | def soap_fault 33 | @soap_fault ||= Fault.new http 34 | end 35 | 36 | # Returns whether there was an HTTP error. 37 | def http_error? 38 | http_error.present? 39 | end 40 | 41 | # Returns the Savon::HTTP::Error. 42 | def http_error 43 | @http_error ||= HTTP::Error.new http 44 | end 45 | 46 | # Shortcut accessor for the SOAP response body Hash. 47 | def [](key) 48 | body[key] 49 | end 50 | 51 | # Returns the SOAP response header as a Hash. 52 | def header 53 | hash[:envelope][:header] 54 | end 55 | 56 | # Returns the SOAP response body as a Hash. 57 | def body 58 | hash[:envelope][:body] 59 | end 60 | 61 | alias to_hash body 62 | 63 | # Traverses the SOAP response body Hash for a given +path+ of Hash keys and returns 64 | # the value as an Array. Defaults to return an empty Array in case the path does not 65 | # exist or returns nil. 66 | def to_array(*path) 67 | result = path.inject body do |memo, key| 68 | return [] unless memo[key] 69 | memo[key] 70 | end 71 | 72 | result.kind_of?(Array) ? result.compact : [result].compact 73 | end 74 | 75 | # Returns the complete SOAP response XML without normalization. 76 | def hash 77 | @hash ||= Nori.parse(to_xml) 78 | end 79 | 80 | # Returns the SOAP response XML. 81 | def to_xml 82 | http.body 83 | end 84 | 85 | # Returns a Nokogiri::XML::Document for the SOAP response XML. 86 | def doc 87 | @doc ||= Nokogiri::XML(to_xml) 88 | end 89 | 90 | # Returns an Array of Nokogiri::XML::Node objects retrieved with the given +path+. 91 | # Automatically adds all of the document's namespaces unless a +namespaces+ hash is provided. 92 | def xpath(path, namespaces = nil) 93 | doc.xpath(path, namespaces || xml_namespaces) 94 | end 95 | 96 | private 97 | 98 | def raise_errors 99 | raise soap_fault if soap_fault? 100 | raise http_error if http_error? 101 | end 102 | 103 | def xml_namespaces 104 | @xml_namespaces ||= doc.collect_namespaces 105 | end 106 | 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/savon/soap/fault_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::SOAP::Fault do 4 | let(:soap_fault) { Savon::SOAP::Fault.new new_response(:body => Fixture.response(:soap_fault)) } 5 | let(:soap_fault2) { Savon::SOAP::Fault.new new_response(:body => Fixture.response(:soap_fault12)) } 6 | let(:another_soap_fault) { Savon::SOAP::Fault.new new_response(:body => Fixture.response(:another_soap_fault)) } 7 | let(:no_fault) { Savon::SOAP::Fault.new new_response } 8 | 9 | it "should be a Savon::Error" do 10 | Savon::SOAP::Fault.should < Savon::Error 11 | end 12 | 13 | describe "#http" do 14 | it "should return the HTTPI::Response" do 15 | soap_fault.http.should be_an(HTTPI::Response) 16 | end 17 | end 18 | 19 | describe "#present?" do 20 | it "should return true if the HTTP response contains a SOAP 1.1 fault" do 21 | soap_fault.should be_present 22 | end 23 | 24 | it "should return true if the HTTP response contains a SOAP 1.2 fault" do 25 | soap_fault2.should be_present 26 | end 27 | 28 | it "should return true if the HTTP response contains a SOAP fault with different namespaces" do 29 | another_soap_fault.should be_present 30 | end 31 | 32 | it "should return false unless the HTTP response contains a SOAP fault" do 33 | no_fault.should_not be_present 34 | end 35 | end 36 | 37 | [:message, :to_s].each do |method| 38 | describe "##{method}" do 39 | it "should return an empty String unless a SOAP fault is present" do 40 | no_fault.send(method).should == "" 41 | end 42 | 43 | it "should return a SOAP 1.1 fault message" do 44 | soap_fault.send(method).should == "(soap:Server) Fault occurred while processing." 45 | end 46 | 47 | it "should return a SOAP 1.2 fault message" do 48 | soap_fault2.send(method).should == "(soap:Sender) Sender Timeout" 49 | end 50 | 51 | it "should return a SOAP fault message (with different namespaces)" do 52 | another_soap_fault.send(method).should == "(ERR_NO_SESSION) Wrong session message" 53 | end 54 | end 55 | end 56 | 57 | describe "#to_hash" do 58 | it "should return the SOAP response as a Hash unless a SOAP fault is present" do 59 | no_fault.to_hash[:authenticate_response][:return][:success].should be_true 60 | end 61 | 62 | it "should return a SOAP 1.1 fault as a Hash" do 63 | soap_fault.to_hash.should == { 64 | :fault => { 65 | :faultstring => "Fault occurred while processing.", 66 | :faultcode => "soap:Server" 67 | } 68 | } 69 | end 70 | 71 | it "should return a SOAP 1.2 fault as a Hash" do 72 | soap_fault2.to_hash.should == { 73 | :fault => { 74 | :detail => { :max_time => "P5M" }, 75 | :reason => { :text => "Sender Timeout" }, 76 | :code => { :value => "soap:Sender", :subcode => { :value => "m:MessageTimeout" } } 77 | } 78 | } 79 | end 80 | end 81 | 82 | def new_response(options = {}) 83 | defaults = { :code => 500, :headers => {}, :body => Fixture.response(:authentication) } 84 | response = defaults.merge options 85 | 86 | HTTPI::Response.new response[:code], response[:headers], response[:body] 87 | end 88 | 89 | end 90 | -------------------------------------------------------------------------------- /lib/savon/global.rb: -------------------------------------------------------------------------------- 1 | require "logger" 2 | require "savon/soap" 3 | require "savon/hooks/group" 4 | 5 | module Savon 6 | module Global 7 | 8 | # Sets whether to log HTTP requests. 9 | attr_writer :log 10 | 11 | # Returns whether to log HTTP requests. Defaults to +true+. 12 | def log? 13 | @log != false 14 | end 15 | 16 | # Sets the logger to use. 17 | attr_writer :logger 18 | 19 | # Returns the logger. Defaults to an instance of +Logger+ writing to STDOUT. 20 | def logger 21 | @logger ||= ::Logger.new STDOUT 22 | end 23 | 24 | # Sets the log level. 25 | attr_writer :log_level 26 | 27 | # Returns the log level. Defaults to :debug. 28 | def log_level 29 | @log_level ||= :debug 30 | end 31 | 32 | # Logs a given +message+. 33 | def log(message) 34 | logger.send log_level, message if log? 35 | end 36 | 37 | # Sets whether to raise HTTP errors and SOAP faults. 38 | attr_writer :raise_errors 39 | 40 | # Returns whether to raise errors. Defaults to +true+. 41 | def raise_errors? 42 | @raise_errors != false 43 | end 44 | 45 | # Sets the global SOAP version. 46 | def soap_version=(version) 47 | raise ArgumentError, "Invalid SOAP version: #{version}" unless SOAP::Versions.include? version 48 | @version = version 49 | end 50 | 51 | # Returns SOAP version. Defaults to +DefaultVersion+. 52 | def soap_version 53 | @version ||= SOAP::DefaultVersion 54 | end 55 | 56 | # Returns whether to strip namespaces in a SOAP response Hash. 57 | # Defaults to +true+. 58 | def strip_namespaces? 59 | Savon.deprecate("use Nori.strip_namespaces? instead of Savon.strip_namespaces?") 60 | Nori.strip_namespaces? 61 | end 62 | 63 | # Sets whether to strip namespaces in a SOAP response Hash. 64 | def strip_namespaces=(strip) 65 | Savon.deprecate("use Nori.strip_namespaces= instead of Savon.strip_namespaces=") 66 | Nori.strip_namespaces = strip 67 | end 68 | 69 | # Returns the global env_namespace. 70 | attr_reader :env_namespace 71 | 72 | # Sets the global env_namespace. 73 | attr_writer :env_namespace 74 | 75 | # Returns the global soap_header. 76 | attr_reader :soap_header 77 | 78 | # Sets the global soap_header. 79 | attr_writer :soap_header 80 | 81 | # Expects a +message+ and raises a warning if configured. 82 | def deprecate(message) 83 | warn("Deprecation: #{message}") if deprecate? 84 | end 85 | 86 | # Sets whether to warn about deprecations. 87 | def deprecate=(deprecate) 88 | @deprecate = deprecate 89 | end 90 | 91 | # Returns whether to warn about deprecation. 92 | def deprecate? 93 | @deprecate != false 94 | end 95 | 96 | # Returns the hooks. 97 | def hooks 98 | @hooks ||= Hooks::Group.new 99 | end 100 | 101 | # Reset to default configuration. 102 | def reset_config! 103 | self.log = true 104 | self.logger = ::Logger.new STDOUT 105 | self.log_level = :debug 106 | self.raise_errors = true 107 | self.soap_version = SOAP::DefaultVersion 108 | self.strip_namespaces = true 109 | self.env_namespace = nil 110 | self.soap_header = {} 111 | end 112 | 113 | end 114 | end 115 | 116 | -------------------------------------------------------------------------------- /spec/fixtures/wsdl/authentication.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /spec/fixtures/response/multi_ref.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 2009-09-22T13:47:23.000Z 12 | Archive 13 | 14 | 15 | Original 16 | Mail from 09-22-2009: Misc 17 | 18 | 19 | 2009-04-30T06:38:34.000Z 20 | Archive 21 | 22 | 23 | Original 24 | Mail from 04-29-2009: Technical support 25 | 26 | 27 | 2009-12-18T15:43:21.000Z 28 | Archive 29 | 30 | 31 | Original 32 | Mail from 12-17-2009: Misc 33 | 34 | 972219 35 | 708021 36 | 0 37 | 855763 38 | 39 | 40 | -------------------------------------------------------------------------------- /spec/savon/model_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::Model do 4 | 5 | let(:model) do 6 | Class.new { extend Savon::Model } 7 | end 8 | 9 | describe ":model_soap_response hook" do 10 | 11 | before(:all) do 12 | model.actions :get_user, "GetAllUsers" 13 | end 14 | 15 | after do 16 | Savon.hooks.reject! :test_hook 17 | end 18 | 19 | it "can be used for pre-processing SOAP responses" do 20 | Savon.hooks.define(:test_hook, :model_soap_response) do |response| 21 | "hello #{response}" 22 | end 23 | 24 | model.client.stubs(:request).returns("world") # 25 | model.get_user.should == "hello world" 26 | end 27 | 28 | end 29 | 30 | describe ".client" do 31 | 32 | it "passes a given block to a new Savon::Client" 33 | 34 | it "memoizes the Savon::Client" do 35 | model.client.should equal(model.client) 36 | end 37 | 38 | end 39 | 40 | describe ".endpoint" do 41 | 42 | it "sets the SOAP endpoint" do 43 | model.endpoint "http://example.com" 44 | model.client.wsdl.endpoint.should == "http://example.com" 45 | end 46 | 47 | end 48 | 49 | describe ".namespace" do 50 | 51 | it "sets the target namespace" do 52 | model.namespace "http://v1.example.com" 53 | model.client.wsdl.namespace.should == "http://v1.example.com" 54 | end 55 | 56 | end 57 | 58 | describe ".document" do 59 | 60 | it "sets the WSDL document" do 61 | model.document "http://example.com/?wsdl" 62 | model.client.wsdl.document.should == "http://example.com/?wsdl" 63 | end 64 | 65 | end 66 | 67 | describe ".headers" do 68 | 69 | it "sets the HTTP headers" do 70 | model.headers("Accept-Charset" => "utf-8") 71 | model.client.http.headers.should == { "Accept-Charset" => "utf-8" } 72 | end 73 | 74 | end 75 | 76 | describe ".basic_auth" do 77 | 78 | it "sets HTTP Basic auth credentials" do 79 | model.basic_auth "login", "password" 80 | model.client.http.auth.basic.should == ["login", "password"] 81 | end 82 | 83 | end 84 | 85 | describe ".wsse_auth" do 86 | 87 | it "sets WSSE auth credentials" do 88 | model.wsse_auth "login", "password", :digest 89 | 90 | model.client.wsse.username.should == "login" 91 | model.client.wsse.password.should == "password" 92 | model.client.wsse.should be_digest 93 | end 94 | 95 | end 96 | 97 | describe ".actions" do 98 | 99 | before(:all) do 100 | model.actions :get_user, "GetAllUsers" 101 | end 102 | 103 | it "defines class methods each action" do 104 | model.should respond_to(:get_user, :get_all_users) 105 | end 106 | 107 | it "defines instance methods each action" do 108 | model.new.should respond_to(:get_user, :get_all_users) 109 | end 110 | 111 | context "(class-level)" do 112 | 113 | it "executes SOAP requests with a given body" do 114 | model.client.expects(:request).with(:wsdl, :get_user, :body => { :id => 1 }) 115 | model.get_user :id => 1 116 | end 117 | 118 | it "accepts and passes Strings for action names" do 119 | model.client.expects(:request).with(:wsdl, "GetAllUsers", :body => { :id => 1 }) 120 | model.get_all_users :id => 1 121 | end 122 | end 123 | 124 | context "(instance-level)" do 125 | 126 | it "delegates to the corresponding class method" do 127 | model.expects(:get_all_users).with(:active => true) 128 | model.new.get_all_users :active => true 129 | end 130 | 131 | end 132 | 133 | end 134 | 135 | describe "#client" do 136 | 137 | it "returns the class-level Savon::Client" do 138 | model.new.client.should == model.client 139 | end 140 | 141 | end 142 | 143 | describe "overwriting action methods" do 144 | 145 | context "(class-level)" do 146 | 147 | let(:supermodel) do 148 | supermodel = model.dup 149 | supermodel.actions :get_user 150 | 151 | def supermodel.get_user(body = nil, &block) 152 | p "super" 153 | super 154 | end 155 | 156 | supermodel 157 | end 158 | 159 | it "works" do 160 | supermodel.client.expects(:request).with(:wsdl, :get_user, :body => { :id => 1 }) 161 | supermodel.expects(:p).with("super") # stupid, but works 162 | 163 | supermodel.get_user :id => 1 164 | end 165 | 166 | end 167 | 168 | context "(instance-level)" do 169 | 170 | let(:supermodel) do 171 | supermodel = model.dup 172 | supermodel.actions :get_user 173 | supermodel = supermodel.new 174 | 175 | def supermodel.get_user(body = nil, &block) 176 | p "super" 177 | super 178 | end 179 | 180 | supermodel 181 | end 182 | 183 | it "works" do 184 | supermodel.client.expects(:request).with(:wsdl, :get_user, :body => { :id => 1 }) 185 | supermodel.expects(:p).with("super") # stupid, but works 186 | 187 | supermodel.get_user :id => 1 188 | end 189 | 190 | end 191 | 192 | end 193 | 194 | end 195 | -------------------------------------------------------------------------------- /lib/savon/client.rb: -------------------------------------------------------------------------------- 1 | require "httpi/request" 2 | require "akami" 3 | 4 | require "savon/wasabi/document" 5 | require "savon/soap/xml" 6 | require "savon/soap/request" 7 | require "savon/soap/response" 8 | 9 | module Savon 10 | 11 | # = Savon::Client 12 | # 13 | # Savon::Client is the main object for connecting to a SOAP service. 14 | class Client 15 | 16 | # Initializes the Savon::Client for a SOAP service. Accepts a +block+ which is evaluated in the 17 | # context of this object to let you access the +wsdl+, +http+, and +wsse+ methods. 18 | # 19 | # == Examples 20 | # 21 | # # Using a remote WSDL 22 | # client = Savon::Client.new("http://example.com/UserService?wsdl") 23 | # 24 | # # Using a local WSDL 25 | # client = Savon::Client.new File.expand_path("../wsdl/service.xml", __FILE__) 26 | # 27 | # # Directly accessing a SOAP endpoint 28 | # client = Savon::Client.new do 29 | # wsdl.endpoint = "http://example.com/UserService" 30 | # wsdl.namespace = "http://users.example.com" 31 | # end 32 | def initialize(wsdl_document = nil, &block) 33 | wsdl.document = wsdl_document if wsdl_document 34 | process 1, &block if block 35 | wsdl.request = http 36 | end 37 | 38 | # Returns the Savon::Wasabi::Document. 39 | def wsdl 40 | @wsdl ||= Wasabi::Document.new 41 | end 42 | 43 | # Returns the HTTPI::Request. 44 | def http 45 | @http ||= HTTPI::Request.new 46 | end 47 | 48 | # Returns the Akami::WSSE object. 49 | def wsse 50 | @wsse ||= Akami.wsse 51 | end 52 | 53 | # Returns the Savon::SOAP::XML object. Please notice, that this object is only available 54 | # in a block given to Savon::Client#request. A new instance of this object is created 55 | # per SOAP request. 56 | attr_reader :soap 57 | 58 | # Executes a SOAP request for a given SOAP action. Accepts a +block+ which is evaluated in the 59 | # context of this object to let you access the +soap+, +wsdl+, +http+ and +wsse+ methods. 60 | # 61 | # == Examples 62 | # 63 | # # Calls a "getUser" SOAP action with the payload of "123" 64 | # client.request(:get_user) { soap.body = { :user_id => 123 } } 65 | # 66 | # # Prefixes the SOAP input tag with a given namespace: "..." 67 | # client.request(:wsdl, "GetUser") { soap.body = { :user_id => 123 } } 68 | # 69 | # # SOAP input tag with attributes: ..." 70 | # client.request(:get_user, "xmlns:wsdl" => "http://example.com") 71 | def request(*args, &block) 72 | raise ArgumentError, "Savon::Client#request requires at least one argument" if args.empty? 73 | 74 | self.soap = SOAP::XML.new 75 | preconfigure extract_options(args) 76 | process &block if block 77 | soap.wsse = wsse 78 | 79 | response = SOAP::Request.new(http, soap).response 80 | set_cookie response.http.headers 81 | response 82 | end 83 | 84 | private 85 | 86 | # Writer for the Savon::SOAP::XML object. 87 | attr_writer :soap 88 | 89 | # Accessor for the original self of a given block. 90 | attr_accessor :original_self 91 | 92 | # Passes a cookie from the last request +headers+ to the next one. 93 | def set_cookie(headers) 94 | http.headers["Cookie"] = headers["Set-Cookie"] if headers["Set-Cookie"] 95 | end 96 | 97 | # Expects an Array of +args+ and returns an Array containing the namespace (might be +nil+), 98 | # the SOAP input and a Hash of attributes for the input tag (which might be empty). 99 | def extract_options(args) 100 | attributes = Hash === args.last ? args.pop : {} 101 | namespace = args.size > 1 ? args.shift.to_sym : nil 102 | input = args.first 103 | 104 | [namespace, input, attributes] 105 | end 106 | 107 | # Expects an Array of +args+ to preconfigure the system. 108 | def preconfigure(args) 109 | soap.endpoint = wsdl.endpoint 110 | soap.namespace_identifier = args[0] 111 | soap.namespace = wsdl.namespace 112 | soap.element_form_default = wsdl.element_form_default if wsdl.document? 113 | 114 | body = args[2].delete(:body) 115 | soap.body = body if body 116 | 117 | wsdl.type_namespaces.each do |path, uri| 118 | soap.use_namespace(path, uri) 119 | end 120 | 121 | wsdl.type_definitions.each do |path, type| 122 | soap.types[path] = type 123 | end 124 | 125 | set_soap_action args[1] 126 | set_soap_input *args 127 | end 128 | 129 | # Expects an +input+ and sets the +SOAPAction+ HTTP headers. 130 | def set_soap_action(input_tag) 131 | soap_action = wsdl.soap_action(input_tag.to_sym) if wsdl.document? 132 | soap_action ||= Gyoku::XMLKey.create(input_tag).to_sym 133 | http.headers["SOAPAction"] = %{"#{soap_action}"} 134 | end 135 | 136 | # Expects a +namespace+, +input+ and +attributes+ and sets the SOAP input. 137 | def set_soap_input(namespace, input, attributes) 138 | new_input_tag = wsdl.soap_input(input.to_sym) if wsdl.document? 139 | new_input_tag ||= Gyoku::XMLKey.create(input) 140 | soap.input = [namespace, new_input_tag.to_sym, attributes] 141 | end 142 | 143 | # Processes a given +block+. Yields objects if the block expects any arguments. 144 | # Otherwise evaluates the block in the context of this object. 145 | def process(offset = 0, &block) 146 | block.arity > 0 ? yield_objects(offset, &block) : evaluate(&block) 147 | end 148 | 149 | # Yields a number of objects to a given +block+ depending on how many arguments 150 | # the block is expecting. 151 | def yield_objects(offset, &block) 152 | yield *[soap, wsdl, http, wsse][offset, block.arity] 153 | end 154 | 155 | # Evaluates a given +block+ inside this object. Stores the original block binding. 156 | def evaluate(&block) 157 | self.original_self = eval "self", block.binding 158 | instance_eval &block 159 | end 160 | 161 | # Handles calls to undefined methods by delegating to the original block binding. 162 | def method_missing(method, *args, &block) 163 | super unless original_self 164 | original_self.send method, *args, &block 165 | end 166 | 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /spec/savon/soap/response_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::SOAP::Response do 4 | 5 | describe ".new" do 6 | it "should raise a Savon::SOAP::Fault in case of a SOAP fault" do 7 | lambda { soap_fault_response }.should raise_error(Savon::SOAP::Fault) 8 | end 9 | 10 | it "should not raise a Savon::SOAP::Fault in case the default is turned off" do 11 | Savon.raise_errors = false 12 | lambda { soap_fault_response }.should_not raise_error(Savon::SOAP::Fault) 13 | Savon.raise_errors = true 14 | end 15 | 16 | it "should raise a Savon::HTTP::Error in case of an HTTP error" do 17 | lambda { soap_response :code => 500 }.should raise_error(Savon::HTTP::Error) 18 | end 19 | 20 | it "should not raise a Savon::HTTP::Error in case the default is turned off" do 21 | Savon.raise_errors = false 22 | soap_response :code => 500 23 | Savon.raise_errors = true 24 | end 25 | end 26 | 27 | describe "#success?" do 28 | around do |example| 29 | Savon.raise_errors = false 30 | example.run 31 | Savon.raise_errors = true 32 | end 33 | 34 | it "should return true if the request was successful" do 35 | soap_response.should be_a_success 36 | end 37 | 38 | it "should return false if there was a SOAP fault" do 39 | soap_fault_response.should_not be_a_success 40 | end 41 | 42 | it "should return false if there was an HTTP error" do 43 | http_error_response.should_not be_a_success 44 | end 45 | end 46 | 47 | describe "#soap_fault?" do 48 | around do |example| 49 | Savon.raise_errors = false 50 | example.run 51 | Savon.raise_errors = true 52 | end 53 | 54 | it "should not return true in case the response seems to be ok" do 55 | soap_response.soap_fault?.should be_false 56 | end 57 | 58 | it "should return true in case of a SOAP fault" do 59 | soap_fault_response.soap_fault?.should be_true 60 | end 61 | end 62 | 63 | describe "#soap_fault" do 64 | around do |example| 65 | Savon.raise_errors = false 66 | example.run 67 | Savon.raise_errors = true 68 | end 69 | 70 | it "should return a Savon::SOAP::Fault" do 71 | soap_fault_response.soap_fault.should be_a(Savon::SOAP::Fault) 72 | end 73 | 74 | it "should return a Savon::SOAP::Fault containing the HTTPI::Response" do 75 | soap_fault_response.soap_fault.http.should be_an(HTTPI::Response) 76 | end 77 | 78 | it "should return a Savon::SOAP::Fault even if the SOAP response seems to be ok" do 79 | soap_response.soap_fault.should be_a(Savon::SOAP::Fault) 80 | end 81 | end 82 | 83 | describe "#http_error?" do 84 | around do |example| 85 | Savon.raise_errors = false 86 | example.run 87 | Savon.raise_errors = true 88 | end 89 | 90 | it "should not return true in case the response seems to be ok" do 91 | soap_response.http_error?.should_not be_true 92 | end 93 | 94 | it "should return true in case of an HTTP error" do 95 | soap_response(:code => 500).http_error?.should be_true 96 | end 97 | end 98 | 99 | describe "#http_error" do 100 | around do |example| 101 | Savon.raise_errors = false 102 | example.run 103 | Savon.raise_errors = true 104 | end 105 | 106 | it "should return a Savon::HTTP::Error" do 107 | http_error_response.http_error.should be_a(Savon::HTTP::Error) 108 | end 109 | 110 | it "should return a Savon::HTTP::Error containing the HTTPI::Response" do 111 | http_error_response.http_error.http.should be_an(HTTPI::Response) 112 | end 113 | 114 | it "should return a Savon::HTTP::Error even if the HTTP response seems to be ok" do 115 | soap_response.http_error.should be_a(Savon::HTTP::Error) 116 | end 117 | end 118 | 119 | describe "#[]" do 120 | it "should return the SOAP response body as a Hash" do 121 | soap_response[:authenticate_response][:return].should == 122 | Fixture.response_hash(:authentication)[:authenticate_response][:return] 123 | end 124 | end 125 | 126 | describe "#header" do 127 | it "should return the SOAP response header as a Hash" do 128 | response = soap_response :body => Fixture.response(:header) 129 | response.header.should include(:session_number => "ABCD1234") 130 | end 131 | end 132 | 133 | %w(body to_hash).each do |method| 134 | describe "##{method}" do 135 | it "should return the SOAP response body as a Hash" do 136 | soap_response.send(method)[:authenticate_response][:return].should == 137 | Fixture.response_hash(:authentication)[:authenticate_response][:return] 138 | end 139 | 140 | it "should return a Hash for a SOAP multiRef response" do 141 | hash = soap_response(:body => Fixture.response(:multi_ref)).send(method) 142 | 143 | hash[:list_response].should be_a(Hash) 144 | hash[:multi_ref].should be_an(Array) 145 | end 146 | 147 | it "should add existing namespaced elements as an array" do 148 | hash = soap_response(:body => Fixture.response(:list)).send(method) 149 | 150 | hash[:multi_namespaced_entry_response][:history].should be_a(Hash) 151 | hash[:multi_namespaced_entry_response][:history][:case].should be_an(Array) 152 | end 153 | end 154 | end 155 | 156 | describe "#to_array" do 157 | context "when the given path exists" do 158 | it "should return an Array containing the path value" do 159 | soap_response.to_array(:authenticate_response, :return).should == 160 | [Fixture.response_hash(:authentication)[:authenticate_response][:return]] 161 | end 162 | end 163 | 164 | context "when the given path returns nil" do 165 | it "should return an empty Array" do 166 | soap_response.to_array(:authenticate_response, :undefined).should == [] 167 | end 168 | end 169 | 170 | context "when the given path does not exist at all" do 171 | it "should return an empty Array" do 172 | soap_response.to_array(:authenticate_response, :some, :undefined, :path).should == [] 173 | end 174 | end 175 | end 176 | 177 | describe "#hash" do 178 | it "should return the complete SOAP response XML as a Hash" do 179 | response = soap_response :body => Fixture.response(:header) 180 | response.hash[:envelope][:header][:session_number].should == "ABCD1234" 181 | end 182 | end 183 | 184 | describe "#to_xml" do 185 | it "should return the raw SOAP response body" do 186 | soap_response.to_xml.should == Fixture.response(:authentication) 187 | end 188 | end 189 | 190 | describe "#doc" do 191 | it "returns a Nokogiri::XML::Document for the SOAP response XML" do 192 | soap_response.doc.should be_a(Nokogiri::XML::Document) 193 | end 194 | end 195 | 196 | describe "#xpath" do 197 | it "permits XPath access to elements in the request" do 198 | soap_response.xpath("//client").first.inner_text.should == "radclient" 199 | soap_response.xpath("//ns2:authenticateResponse/return/success").first.inner_text.should == "true" 200 | end 201 | end 202 | 203 | describe "#http" do 204 | it "should return the HTTPI::Response" do 205 | soap_response.http.should be_an(HTTPI::Response) 206 | end 207 | end 208 | 209 | def soap_response(options = {}) 210 | defaults = { :code => 200, :headers => {}, :body => Fixture.response(:authentication) } 211 | response = defaults.merge options 212 | 213 | Savon::SOAP::Response.new HTTPI::Response.new(response[:code], response[:headers], response[:body]) 214 | end 215 | 216 | def soap_fault_response 217 | soap_response :code => 500, :body => Fixture.response(:soap_fault) 218 | end 219 | 220 | def http_error_response 221 | soap_response :code => 404, :body => "Not found" 222 | end 223 | 224 | end 225 | -------------------------------------------------------------------------------- /lib/savon/soap/xml.rb: -------------------------------------------------------------------------------- 1 | require "builder" 2 | require "gyoku" 3 | require "nori" 4 | 5 | require "savon/soap" 6 | 7 | Nori.configure do |config| 8 | config.strip_namespaces = true 9 | config.convert_tags_to { |tag| tag.snakecase.to_sym } 10 | end 11 | 12 | module Savon 13 | module SOAP 14 | 15 | # = Savon::SOAP::XML 16 | # 17 | # Represents the SOAP request XML. Contains various global and per request/instance settings 18 | # like the SOAP version, header, body and namespaces. 19 | class XML 20 | 21 | # XML Schema Type namespaces. 22 | SchemaTypes = { 23 | "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema", 24 | "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance" 25 | } 26 | 27 | # Accepts an +endpoint+, an +input+ tag and a SOAP +body+. 28 | def initialize(endpoint = nil, input = nil, body = nil) 29 | self.endpoint = endpoint if endpoint 30 | self.input = input if input 31 | self.body = body if body 32 | end 33 | 34 | # Accessor for the SOAP +input+ tag. 35 | attr_accessor :input 36 | 37 | # Accessor for the SOAP +endpoint+. 38 | attr_accessor :endpoint 39 | 40 | # Sets the SOAP +version+. 41 | def version=(version) 42 | raise ArgumentError, "Invalid SOAP version: #{version}" unless SOAP::Versions.include? version 43 | @version = version 44 | end 45 | 46 | # Returns the SOAP +version+. Defaults to Savon.soap_version. 47 | def version 48 | @version ||= Savon.soap_version 49 | end 50 | 51 | # Sets the SOAP +header+ Hash. 52 | attr_writer :header 53 | 54 | # Returns the SOAP +header+. Defaults to an empty Hash. 55 | def header 56 | @header ||= Savon.soap_header.nil? ? {} : Savon.soap_header 57 | end 58 | 59 | # Sets the SOAP envelope namespace. 60 | attr_writer :env_namespace 61 | 62 | # Returns the SOAP envelope namespace. Uses the global namespace if set Defaults to :env. 63 | def env_namespace 64 | @env_namespace ||= Savon.env_namespace.nil? ? :env : Savon.env_namespace 65 | end 66 | 67 | # Sets the +namespaces+ Hash. 68 | attr_writer :namespaces 69 | 70 | # Returns the +namespaces+. Defaults to a Hash containing the SOAP envelope namespace. 71 | def namespaces 72 | @namespaces ||= begin 73 | key = env_namespace.blank? ? "xmlns" : "xmlns:#{env_namespace}" 74 | { key => SOAP::Namespace[version] } 75 | end 76 | end 77 | 78 | def namespace_by_uri(uri) 79 | namespaces.each do |candidate_identifier, candidate_uri| 80 | return candidate_identifier.gsub(/^xmlns:/, '') if candidate_uri == uri 81 | end 82 | nil 83 | end 84 | 85 | def used_namespaces 86 | @used_namespaces ||= {} 87 | end 88 | 89 | def use_namespace(path, uri) 90 | @internal_namespace_count ||= 0 91 | 92 | unless identifier = namespace_by_uri(uri) 93 | identifier = "ins#{@internal_namespace_count}" 94 | namespaces["xmlns:#{identifier}"] = uri 95 | @internal_namespace_count += 1 96 | end 97 | 98 | used_namespaces[path] = identifier 99 | end 100 | 101 | def types 102 | @types ||= {} 103 | end 104 | 105 | # Sets the default namespace identifier. 106 | attr_writer :namespace_identifier 107 | 108 | # Returns the default namespace identifier. 109 | def namespace_identifier 110 | @namespace_identifier ||= :wsdl 111 | end 112 | 113 | # Returns whether all local elements should be namespaced. Might be set to :qualified, 114 | # but defaults to :unqualified. 115 | def element_form_default 116 | @element_form_default ||= :unqualified 117 | end 118 | 119 | # Sets whether all local elements should be namespaced. 120 | attr_writer :element_form_default 121 | 122 | # Accessor for the default namespace URI. 123 | attr_accessor :namespace 124 | 125 | # Accessor for the Savon::WSSE object. 126 | attr_accessor :wsse 127 | 128 | # Accepts a +block+ and yields a Builder::XmlMarkup object to let you create 129 | # custom body XML. 130 | def body 131 | @body = yield builder(nil) if block_given? 132 | @body 133 | end 134 | 135 | # Sets the SOAP +body+. Expected to be a Hash that can be translated to XML via `Gyoku.xml` 136 | # or any other Object responding to to_s. 137 | attr_writer :body 138 | 139 | # Accepts a +block+ and yields a Builder::XmlMarkup object to let you create 140 | # a completely custom XML. 141 | def xml(directive_tag = :xml, attrs = {}) 142 | @xml = yield builder(directive_tag, attrs) if block_given? 143 | end 144 | 145 | # Accepts an XML String and lets you specify a completely custom request body. 146 | attr_writer :xml 147 | 148 | # Returns the XML for a SOAP request. 149 | def to_xml 150 | @xml ||= tag(builder, :Envelope, complete_namespaces) do |xml| 151 | tag(xml, :Header) { xml << header_for_xml } unless header_for_xml.empty? 152 | 153 | if input.nil? 154 | tag(xml, :Body) 155 | else 156 | tag(xml, :Body) { xml.tag!(*add_namespace_to_input) { xml << body_to_xml } } 157 | end 158 | end 159 | end 160 | 161 | private 162 | 163 | # Returns a new Builder::XmlMarkup object. 164 | def builder(directive_tag = :xml, attrs = {}) 165 | builder = Builder::XmlMarkup.new 166 | builder.instruct!(directive_tag, attrs) if directive_tag 167 | builder 168 | end 169 | 170 | # Expects a builder +xml+ instance, a tag +name+ and accepts optional +namespaces+ 171 | # and a block to create an XML tag. 172 | def tag(xml, name, namespaces = {}, &block) 173 | return xml.tag! name, namespaces, &block if env_namespace.blank? 174 | xml.tag! env_namespace, name, namespaces, &block 175 | end 176 | 177 | # Returns the complete Hash of namespaces. 178 | def complete_namespaces 179 | defaults = SchemaTypes.dup 180 | defaults["xmlns:#{namespace_identifier}"] = namespace if namespace 181 | defaults.merge namespaces 182 | end 183 | 184 | # Returns the SOAP header as an XML String. 185 | def header_for_xml 186 | @header_for_xml ||= Gyoku.xml(header) + wsse_header 187 | end 188 | 189 | # Returns the WSSE header or an empty String in case WSSE was not set. 190 | def wsse_header 191 | wsse.respond_to?(:to_xml) ? wsse.to_xml : "" 192 | end 193 | 194 | # Returns the SOAP body as an XML String. 195 | def body_to_xml 196 | return body.to_s unless body.kind_of? Hash 197 | Gyoku.xml add_namespaces_to_body(body), :element_form_default => element_form_default, :namespace => namespace_identifier 198 | end 199 | 200 | def add_namespaces_to_body(hash, path = [input[1].to_s]) 201 | return unless hash 202 | return hash if hash.kind_of?(Array) 203 | return hash.to_s unless hash.kind_of? Hash 204 | 205 | hash.inject({}) do |newhash, (key, value)| 206 | camelcased_key = Gyoku::XMLKey.create(key) 207 | newpath = path + [camelcased_key] 208 | 209 | if used_namespaces[newpath] 210 | newhash.merge( 211 | "#{used_namespaces[newpath]}:#{camelcased_key}" => 212 | add_namespaces_to_body(value, types[newpath] ? [types[newpath]] : newpath) 213 | ) 214 | else 215 | newhash.merge(key => value) 216 | end 217 | end 218 | end 219 | 220 | def add_namespace_to_input 221 | return input.compact unless used_namespaces[[input[1].to_s]] 222 | [used_namespaces[[input[1].to_s]], input[1], input[2]] 223 | end 224 | 225 | end 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /spec/savon/soap/xml_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::SOAP::XML do 4 | let(:xml) { Savon::SOAP::XML.new Endpoint.soap, [nil, :authenticate, {}], :id => 1 } 5 | 6 | describe ".new" do 7 | it "should accept an endpoint, an input tag and a SOAP body" do 8 | xml = Savon::SOAP::XML.new Endpoint.soap, [nil, :authentication, {}], :id => 1 9 | 10 | xml.endpoint.should == Endpoint.soap 11 | xml.input.should == [nil, :authentication, {}] 12 | xml.body.should == { :id => 1 } 13 | end 14 | end 15 | 16 | describe "#input" do 17 | it "should set the input tag" do 18 | xml.input = :test 19 | xml.input.should == :test 20 | end 21 | end 22 | 23 | describe "#endpoint" do 24 | it "should set the endpoint to use" do 25 | xml.endpoint = "http://test.com" 26 | xml.endpoint.should == "http://test.com" 27 | end 28 | end 29 | 30 | describe "#version" do 31 | it "should default to SOAP 1.1" do 32 | xml.version.should == 1 33 | end 34 | 35 | it "should default to the global default" do 36 | Savon.soap_version = 2 37 | xml.version.should == 2 38 | 39 | reset_soap_version 40 | end 41 | 42 | it "should set the SOAP version to use" do 43 | xml.version = 2 44 | xml.version.should == 2 45 | end 46 | 47 | it "should raise an ArgumentError in case of an invalid version" do 48 | lambda { xml.version = 3 }.should raise_error(ArgumentError) 49 | end 50 | end 51 | 52 | describe "#header" do 53 | it "should default to an empty Hash" do 54 | xml.header.should == {} 55 | end 56 | 57 | it "should set the SOAP header" do 58 | xml.header = { "MySecret" => "abc" } 59 | xml.header.should == { "MySecret" => "abc" } 60 | end 61 | 62 | it "should use the global soap_header if set" do 63 | Savon.stubs(:soap_header).returns({ "MySecret" => "abc" }) 64 | xml.header.should == { "MySecret" => "abc" } 65 | end 66 | end 67 | 68 | describe "#env_namespace" do 69 | it "should default to :env" do 70 | xml.env_namespace.should == :env 71 | end 72 | 73 | it "should set the SOAP envelope namespace" do 74 | xml.env_namespace = :soapenv 75 | xml.env_namespace.should == :soapenv 76 | end 77 | 78 | it "should use the global env_namespace if set as the SOAP envelope namespace" do 79 | Savon.stubs(:env_namespace).returns(:soapenv) 80 | xml.env_namespace.should == :soapenv 81 | end 82 | end 83 | 84 | describe "#namespaces" do 85 | it "should default to a Hash containing the namespace for SOAP 1.1" do 86 | xml.namespaces.should == { "xmlns:env" => "http://schemas.xmlsoap.org/soap/envelope/" } 87 | end 88 | 89 | it "should default to a Hash containing the namespace for SOAP 1.2 if that's the current version" do 90 | xml.version = 2 91 | xml.namespaces.should == { "xmlns:env" => "http://www.w3.org/2003/05/soap-envelope" } 92 | end 93 | 94 | it "should set the SOAP header" do 95 | xml.namespaces = { "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema" } 96 | xml.namespaces.should == { "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema" } 97 | end 98 | end 99 | 100 | describe "#wsse" do 101 | it "should set the Akami::WSSE object" do 102 | xml.wsse = Akami.wsse 103 | xml.wsse.should be_a(Akami::WSSE) 104 | end 105 | end 106 | 107 | describe "#body" do 108 | it "should set the SOAP body Hash" do 109 | xml.body = { :id => 1 } 110 | xml.to_xml.should include("1") 111 | end 112 | 113 | it "should accepts an XML String" do 114 | xml.body = "1" 115 | xml.to_xml.should include("1") 116 | end 117 | 118 | it "should accept a block" do 119 | xml.body do |body| 120 | body.user { body.id 1 } 121 | end 122 | 123 | xml.to_xml.should include("1") 124 | end 125 | end 126 | 127 | describe "#xml" do 128 | it "lets you specify a completely custom XML String" do 129 | xml.xml = "xml" 130 | xml.to_xml.should == "xml" 131 | end 132 | 133 | it "yields a Builder::XmlMarkup object to a given block" do 134 | xml.xml { |xml| xml.using("Builder") } 135 | xml.to_xml.should == 'Builder' 136 | end 137 | 138 | it "accepts options to pass to the Builder::XmlMarkup instruct!" do 139 | xml.xml :xml, :aaa => :bbb do |xml| 140 | xml.using("Builder") 141 | end 142 | 143 | xml.to_xml.should == 'Builder' 144 | end 145 | 146 | it "can change encoding to UTF-16" do 147 | xml.xml(:xml, :encoding => "UTF-16") { |xml| xml.using("Builder") } 148 | xml.to_xml.should == 'Builder' 149 | end 150 | end 151 | 152 | describe "#to_xml" do 153 | after { reset_soap_version } 154 | 155 | context "by default" do 156 | it "should start with an XML declaration" do 157 | xml.to_xml.should match(/^<\?xml version="1.0" encoding="UTF-8"\?>/) 158 | end 159 | 160 | it "should use default SOAP envelope namespace" do 161 | xml.to_xml.should include("/) 167 | end 168 | 169 | it "should add the xsi namespace" do 170 | uri = "http://www.w3.org/2001/XMLSchema-instance" 171 | xml.to_xml.should match(//) 172 | end 173 | 174 | it "should have a SOAP envelope tag with a SOAP 1.1 namespace" do 175 | uri = "http://schemas.xmlsoap.org/soap/envelope/" 176 | xml.to_xml.should match(//) 177 | end 178 | 179 | it "should have a SOAP body containing the SOAP input tag and body Hash" do 180 | xml.to_xml.should include('1') 181 | end 182 | 183 | it "should accept a SOAP body as an XML String" do 184 | xml.body = "1" 185 | xml.to_xml.should include('1') 186 | end 187 | 188 | it "should not contain a SOAP header" do 189 | xml.to_xml.should_not include(' "secret", 197 | :attributes! => { :token => { :xmlns => "http://example.com" } } 198 | } 199 | 200 | xml.to_xml.should include('secret') 201 | end 202 | end 203 | 204 | context "with the global SOAP version set to 1.2" do 205 | it "should contain the namespace for SOAP 1.2" do 206 | Savon.soap_version = 2 207 | 208 | uri = "http://www.w3.org/2003/05/soap-envelope" 209 | xml.to_xml.should match(//) 210 | reset_soap_version 211 | end 212 | end 213 | 214 | context "with a global and request SOAP version" do 215 | it "should contain the namespace for the request SOAP version" do 216 | Savon.soap_version = 2 217 | xml.version = 1 218 | 219 | uri = "http://schemas.xmlsoap.org/soap/envelope/" 220 | xml.to_xml.should match(//) 221 | reset_soap_version 222 | end 223 | end 224 | 225 | context "with the SOAP envelope namespace set to an empty String" do 226 | it "should not add a namespace to SOAP envelope tags" do 227 | xml.env_namespace = "" 228 | xml.to_xml.should include(" { :id => 1, ":noNamespace" => true } 243 | end 244 | 245 | it "should namespace the default elements" do 246 | xml.element_form_default = :qualified 247 | xml.namespace_identifier = :wsdl 248 | 249 | xml.to_xml.should include( 250 | "", 251 | "1", 252 | "true" 253 | ) 254 | end 255 | end 256 | 257 | context "with WSSE authentication" do 258 | it "should containg a SOAP header with WSSE authentication details" do 259 | xml.wsse = Akami.wsse 260 | xml.wsse.credentials "username", "password" 261 | 262 | xml.to_xml.should include("username") 264 | xml.to_xml.should include("password") 265 | end 266 | end 267 | 268 | context "with a simple input tag (Symbol)" do 269 | it "should just add the input tag" do 270 | xml.input = [nil, :simple, {}] 271 | xml.to_xml.should include('1') 272 | end 273 | end 274 | 275 | context "with a simple input tag (Array)" do 276 | it "should just add the input tag" do 277 | xml.input = [nil, :simple, {}] 278 | xml.to_xml.should include('1') 279 | end 280 | end 281 | 282 | context "with an input tag and a namespace Hash (Array)" do 283 | it "should contain the input tag with namespaces" do 284 | xml.input = [nil, :getUser, { "active" => true }] 285 | xml.to_xml.should include('1') 286 | end 287 | end 288 | 289 | context "with a prefixed input tag (Array)" do 290 | it "should contain a prefixed input tag" do 291 | xml.input = [:wsdl, :getUser, {}] 292 | xml.to_xml.should include('1') 293 | end 294 | end 295 | 296 | context "with a prefixed input tag and a namespace Hash (Array)" do 297 | it "should contain a prefixed input tag with namespaces" do 298 | xml.input = [:wsdl, :getUser, { :only_active => false }] 299 | xml.to_xml.should include('1') 300 | end 301 | end 302 | end 303 | 304 | def reset_soap_version 305 | Savon.soap_version = Savon::SOAP::DefaultVersion 306 | end 307 | 308 | end 309 | 310 | -------------------------------------------------------------------------------- /spec/savon/client_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::Client do 4 | let(:client) { Savon::Client.new { wsdl.document = Endpoint.wsdl } } 5 | 6 | describe ".new" do 7 | context "with a String" do 8 | it "should set the WSDL document" do 9 | wsdl = "http://example.com/UserService?wsdl" 10 | client = Savon::Client.new(wsdl) 11 | client.wsdl.instance_variable_get("@document").should == wsdl 12 | end 13 | end 14 | 15 | context "with a block expecting one argument" do 16 | it "should yield the WSDL object" do 17 | Savon::Client.new { |wsdl| wsdl.should be_a(Savon::Wasabi::Document) } 18 | end 19 | end 20 | 21 | context "with a block expecting two arguments" do 22 | it "should yield the WSDL and HTTP objects" do 23 | Savon::Client.new do |wsdl, http| 24 | wsdl.should be_an(Savon::Wasabi::Document) 25 | http.should be_an(HTTPI::Request) 26 | end 27 | end 28 | end 29 | 30 | context "with a block expecting three arguments" do 31 | it "should yield the WSDL, HTTP and WSSE objects" do 32 | Savon::Client.new do |wsdl, http, wsse| 33 | wsdl.should be_an(Savon::Wasabi::Document) 34 | http.should be_an(HTTPI::Request) 35 | wsse.should be_an(Akami::WSSE) 36 | end 37 | end 38 | end 39 | 40 | context "with a block expecting no arguments" do 41 | it "should let you access the WSDL object" do 42 | Savon::Client.new { wsdl.should be_a(Savon::Wasabi::Document) } 43 | end 44 | 45 | it "should let you access the HTTP object" do 46 | Savon::Client.new { http.should be_an(HTTPI::Request) } 47 | end 48 | 49 | it "should let you access the WSSE object" do 50 | Savon::Client.new { wsse.should be_a(Akami::WSSE) } 51 | end 52 | end 53 | end 54 | 55 | describe "#wsdl" do 56 | it "should return the Savon::Wasabi::Document" do 57 | client.wsdl.should be_a(Savon::Wasabi::Document) 58 | end 59 | end 60 | 61 | describe "#http" do 62 | it "should return the HTTPI::Request" do 63 | client.http.should be_an(HTTPI::Request) 64 | end 65 | end 66 | 67 | describe "#wsse" do 68 | it "should return the Akami::WSSE object" do 69 | client.wsse.should be_a(Akami::WSSE) 70 | end 71 | end 72 | 73 | describe "#request" do 74 | before do 75 | HTTPI.stubs(:get).returns(new_response(:body => Fixture.wsdl(:authentication))) 76 | HTTPI.stubs(:post).returns(new_response) 77 | end 78 | 79 | context "without any arguments" do 80 | it "should raise an ArgumentError" do 81 | lambda { client.request }.should raise_error(ArgumentError) 82 | end 83 | end 84 | 85 | context "with a single argument (Symbol)" do 86 | it "should set the input tag to result in " do 87 | client.request(:get_user) { soap.input.should == [nil, :getUser, {}] } 88 | end 89 | 90 | it "should set the target namespace with the default identifier" do 91 | namespace = 'xmlns:wsdl="http://v1_0.ws.auth.order.example.com/"' 92 | HTTPI::Request.any_instance.expects(:body=).with { |value| value.include? namespace } 93 | 94 | client.request :get_user 95 | end 96 | 97 | it "should not set the target namespace if soap.namespace was set to nil" do 98 | namespace = 'wsdl="http://v1_0.ws.auth.order.example.com/"' 99 | HTTPI::Request.any_instance.expects(:body=).with { |value| !value.include?(namespace) } 100 | 101 | client.request(:get_user) { soap.namespace = nil } 102 | end 103 | end 104 | 105 | context "with a single argument (String)" do 106 | it "should set the input tag to result in " do 107 | client.request("get_user") { soap.input.should == [nil, :get_user, {}] } 108 | end 109 | end 110 | 111 | context "with a Symbol and a Hash" do 112 | it "should set the input tag to result in " do 113 | client.request(:get_user, :active => true) { soap.input.should == [nil, :getUser, { :active => true }] } 114 | end 115 | end 116 | 117 | context "with two Symbols" do 118 | it "should set the input tag to result in " do 119 | client.request(:v1, :get_user) { soap.input.should == [:v1, :getUser, {}] } 120 | end 121 | 122 | it "should set the target namespace with the given identifier" do 123 | namespace = 'xmlns:v1="http://v1_0.ws.auth.order.example.com/"' 124 | HTTPI::Request.any_instance.expects(:body=).with { |value| value.include? namespace } 125 | 126 | client.request :v1, :get_user 127 | end 128 | 129 | it "should not set the target namespace if soap.namespace was set to nil" do 130 | namespace = 'xmlns:v1="http://v1_0.ws.auth.order.example.com/"' 131 | HTTPI::Request.any_instance.expects(:body=).with { |value| !value.include?(namespace) } 132 | 133 | client.request(:v1, :get_user) { soap.namespace = nil } 134 | end 135 | end 136 | 137 | context "with two Symbols and a Hash" do 138 | it "should set the input tag to result in " do 139 | client.request(:wsdl, :get_user, :active => true) { soap.input.should == [:wsdl, :getUser, { :active => true }] } 140 | end 141 | end 142 | 143 | context "with a block expecting one argument" do 144 | it "should yield the SOAP object" do 145 | client.request(:authenticate) { |soap| soap.should be_a(Savon::SOAP::XML) } 146 | end 147 | end 148 | 149 | context "with a block expecting two arguments" do 150 | it "should yield the SOAP and WSDL objects" do 151 | client.request(:authenticate) do |soap, wsdl| 152 | soap.should be_a(Savon::SOAP::XML) 153 | wsdl.should be_an(Savon::Wasabi::Document) 154 | end 155 | end 156 | end 157 | 158 | context "with a block expecting three arguments" do 159 | it "should yield the SOAP, WSDL and HTTP objects" do 160 | client.request(:authenticate) do |soap, wsdl, http| 161 | soap.should be_a(Savon::SOAP::XML) 162 | wsdl.should be_an(Savon::Wasabi::Document) 163 | http.should be_an(HTTPI::Request) 164 | end 165 | end 166 | end 167 | 168 | context "with a block expecting four arguments" do 169 | it "should yield the SOAP, WSDL, HTTP and WSSE objects" do 170 | client.request(:authenticate) do |soap, wsdl, http, wsse| 171 | soap.should be_a(Savon::SOAP::XML) 172 | wsdl.should be_a(Savon::Wasabi::Document) 173 | http.should be_an(HTTPI::Request) 174 | wsse.should be_a(Akami::WSSE) 175 | end 176 | end 177 | end 178 | 179 | context "with a block expecting no arguments" do 180 | it "should let you access the SOAP object" do 181 | client.request(:authenticate) { soap.should be_a(Savon::SOAP::XML) } 182 | end 183 | 184 | it "should let you access the HTTP object" do 185 | client.request(:authenticate) { http.should be_an(HTTPI::Request) } 186 | end 187 | 188 | it "should let you access the WSSE object" do 189 | client.request(:authenticate) { wsse.should be_a(Akami::WSSE) } 190 | end 191 | 192 | it "should let you access the WSDL object" do 193 | client.request(:authenticate) { wsdl.should be_a(Savon::Wasabi::Document) } 194 | end 195 | end 196 | 197 | it "should not set the Cookie header for the next request" do 198 | client.http.headers.expects(:[]=).with("Cookie", anything).never 199 | client.http.headers.stubs(:[]=).with("SOAPAction", '"authenticate"') 200 | client.http.headers.stubs(:[]=).with("Content-Type", "text/xml;charset=UTF-8") 201 | client.http.headers.stubs(:[]=).with("Content-Length", "384") 202 | 203 | client.request :authenticate 204 | end 205 | end 206 | 207 | context "#request with a Set-Cookie response header" do 208 | before do 209 | HTTPI.stubs(:get).returns(new_response(:body => Fixture.wsdl(:authentication))) 210 | HTTPI.stubs(:post).returns(new_response(:headers => { "Set-Cookie" => "some-cookie" })) 211 | end 212 | 213 | it "should set the Cookie header for the next request" do 214 | client.http.headers.expects(:[]=).with("Cookie", "some-cookie") 215 | client.http.headers.stubs(:[]=).with("SOAPAction", '"authenticate"') 216 | client.http.headers.stubs(:[]=).with("Content-Type", "text/xml;charset=UTF-8") 217 | client.http.headers.stubs(:[]=).with("Content-Length", "384") 218 | 219 | client.request :authenticate 220 | end 221 | end 222 | 223 | context "with a remote WSDL document" do 224 | let(:client) { Savon::Client.new { wsdl.document = Endpoint.wsdl } } 225 | before { HTTPI.expects(:get).returns(new_response(:body => Fixture.wsdl(:authentication))) } 226 | 227 | it "adds a SOAPAction header containing the SOAP action name" do 228 | HTTPI.stubs(:post).returns(new_response) 229 | 230 | client.request :authenticate do 231 | http.headers["SOAPAction"].should == %{"authenticate"} 232 | end 233 | end 234 | 235 | it "should execute SOAP requests and return the response" do 236 | HTTPI.expects(:post).returns(new_response) 237 | response = client.request(:authenticate) 238 | 239 | response.should be_a(Savon::SOAP::Response) 240 | response.to_xml.should == Fixture.response(:authentication) 241 | end 242 | end 243 | 244 | context "with a local WSDL document" do 245 | let(:client) { Savon::Client.new { wsdl.document = "spec/fixtures/wsdl/authentication.xml" } } 246 | 247 | before { HTTPI.expects(:get).never } 248 | 249 | it "adds a SOAPAction header containing the SOAP action name" do 250 | HTTPI.stubs(:post).returns(new_response) 251 | 252 | client.request :authenticate do 253 | http.headers["SOAPAction"].should == %{"authenticate"} 254 | end 255 | end 256 | 257 | it "should execute SOAP requests and return the response" do 258 | HTTPI.expects(:post).returns(new_response) 259 | response = client.request(:authenticate) 260 | 261 | response.should be_a(Savon::SOAP::Response) 262 | response.to_xml.should == Fixture.response(:authentication) 263 | end 264 | end 265 | 266 | context "when the WSDL specifies multiple namespaces" do 267 | before do 268 | HTTPI.stubs(:get).returns(new_response(:body => Fixture.wsdl(:multiple_namespaces))) 269 | HTTPI.stubs(:post).returns(new_response) 270 | end 271 | 272 | it "qualifies each element with the appropriate namespace" do 273 | HTTPI::Request.any_instance.expects(:body=).with do |value| 274 | xml = Nokogiri::XML(value) 275 | 276 | title = xml.at_xpath( 277 | ".//actions:Save/actions:article/article:Title/text()", 278 | "article" => "http://example.com/article", 279 | "actions" => "http://example.com/actions").to_s 280 | author = xml.at_xpath( 281 | ".//actions:Save/actions:article/article:Author/text()", 282 | "article" => "http://example.com/article", 283 | "actions" => "http://example.com/actions").to_s 284 | 285 | title == "Hamlet" && author == "Shakespeare" 286 | end 287 | 288 | client.request :save do 289 | soap.body = { :article => { "Title" => "Hamlet", "Author" => "Shakespeare" } } 290 | end 291 | end 292 | 293 | it "still sends nil as xsi:nil as in the non-namespaced case" do 294 | HTTPI::Request.any_instance.expects(:body=).with do |value| 295 | xml = Nokogiri::XML(value) 296 | 297 | attribute = xml.at_xpath(".//article:Title/@xsi:nil", 298 | "xsi" => "http://www.w3.org/2001/XMLSchema-instance", 299 | "article" => "http://example.com/article").to_s 300 | 301 | attribute == "true" 302 | end 303 | 304 | client.request(:save) { soap.body = { :article => { "Title" => nil } } } 305 | end 306 | 307 | it "translates between symbol :save and string 'Save'" do 308 | HTTPI::Request.any_instance.expects(:body=).with do |value| 309 | xml = Nokogiri::XML(value) 310 | !!xml.at_xpath(".//actions:Save", "actions" => "http://example.com/actions") 311 | end 312 | 313 | client.request :save do 314 | soap.body = { :article => { :title => "Hamlet", :author => "Shakespeare" } } 315 | end 316 | end 317 | 318 | it "qualifies Save with the appropriate namespace" do 319 | HTTPI::Request.any_instance.expects(:body=).with do |value| 320 | xml = Nokogiri::XML(value) 321 | !!xml.at_xpath(".//actions:Save", "actions" => "http://example.com/actions") 322 | end 323 | 324 | client.request "Save" do 325 | soap.body = { :article => { :title => "Hamlet", :author => "Shakespeare" } } 326 | end 327 | end 328 | end 329 | 330 | context "when the WSDL has a lowerCamel name" do 331 | before do 332 | HTTPI.stubs(:get).returns(new_response(:body => Fixture.wsdl(:lower_camel))) 333 | HTTPI.stubs(:post).returns(new_response) 334 | end 335 | 336 | it "appends namespace when name is specified explicitly" do 337 | HTTPI::Request.any_instance.expects(:body=).with do |value| 338 | xml = Nokogiri::XML(value) 339 | !!xml.at_xpath(".//actions:Save/actions:lowerCamel", "actions" => "http://example.com/actions") 340 | end 341 | 342 | client.request("Save") { soap.body = { 'lowerCamel' => 'theValue' } } 343 | end 344 | 345 | it "still appends namespace when converting from symbol" do 346 | HTTPI::Request.any_instance.expects(:body=).with do |value| 347 | xml = Nokogiri::XML(value) 348 | !!xml.at_xpath(".//actions:Save/actions:lowerCamel", "actions" => "http://example.com/actions") 349 | end 350 | 351 | client.request("Save") { soap.body = { :lower_camel => 'theValue' } } 352 | end 353 | end 354 | 355 | context "with multiple types" do 356 | before do 357 | HTTPI.stubs(:get).returns(new_response(:body => Fixture.wsdl(:multiple_types))) 358 | HTTPI.stubs(:post).returns(new_response) 359 | end 360 | 361 | it "does not blow up" do 362 | HTTPI::Request.any_instance.expects(:body=).with { |value| value.include?("Save") } 363 | client.request(:save) { soap.body = {} } 364 | end 365 | end 366 | 367 | context "with an Array of namespaced items" do 368 | let(:client) { Savon::Client.new { wsdl.document = "spec/fixtures/wsdl/taxcloud.xml" } } 369 | 370 | before do 371 | HTTPI.stubs(:get).returns(new_response(:body => Fixture.wsdl(:taxcloud))) 372 | HTTPI.stubs(:post).returns(new_response) 373 | end 374 | 375 | it "should namespaces each Array item as expected" do 376 | HTTPI::Request.any_instance.expects(:body=).with do |value| 377 | value.include?("") && value.include?("SKU-TEST") 378 | end 379 | 380 | address = { "Address1" => "888 6th Ave", "Address2" => nil, "City" => "New York", "State" => "NY", "Zip5" => "10001", "Zip4" => nil } 381 | cart_item = { "Index" => 0, "ItemID" => "SKU-TEST", "TIC" => "00000", "Price" => 50.0, "Qty" => 1 } 382 | 383 | client.request :lookup, :body => { 384 | "customerID" => 123, 385 | "cartID" => 456, 386 | "cartItems" => { "CartItem" => [cart_item] }, 387 | "origin" => address, 388 | "destination" => address 389 | } 390 | end 391 | end 392 | 393 | context "without a WSDL document" do 394 | let(:client) do 395 | Savon::Client.new do 396 | wsdl.endpoint = Endpoint.soap 397 | wsdl.namespace = "http://v1_0.ws.auth.order.example.com/" 398 | end 399 | end 400 | 401 | before { HTTPI.expects(:get).never } 402 | 403 | it "raise an ArgumentError when trying to access the WSDL" do 404 | lambda { client.wsdl.soap_actions }.should raise_error(ArgumentError, /Wasabi/) 405 | end 406 | 407 | it "adds a SOAPAction header containing the SOAP action name" do 408 | HTTPI.stubs(:post).returns(new_response) 409 | 410 | client.request :authenticate do 411 | http.headers["SOAPAction"].should == %{"authenticate"} 412 | end 413 | end 414 | 415 | it "should execute SOAP requests and return the response" do 416 | HTTPI.expects(:post).returns(new_response) 417 | response = client.request(:authenticate) 418 | 419 | response.should be_a(Savon::SOAP::Response) 420 | response.to_xml.should == Fixture.response(:authentication) 421 | end 422 | end 423 | 424 | context "when encountering a SOAP fault" do 425 | let(:client) do 426 | Savon::Client.new do 427 | wsdl.endpoint = Endpoint.soap 428 | wsdl.namespace = "http://v1_0.ws.auth.order.example.com/" 429 | end 430 | end 431 | 432 | before { HTTPI::expects(:post).returns(new_response(:code => 500, :body => Fixture.response(:soap_fault))) } 433 | 434 | it "should raise a Savon::SOAP::Fault" do 435 | lambda { client.request :authenticate }.should raise_error(Savon::SOAP::Fault) 436 | end 437 | end 438 | 439 | context "when encountering an HTTP error" do 440 | let(:client) do 441 | Savon::Client.new do 442 | wsdl.endpoint = Endpoint.soap 443 | wsdl.namespace = "http://v1_0.ws.auth.order.example.com/" 444 | end 445 | end 446 | 447 | before { HTTPI::expects(:post).returns(new_response(:code => 500)) } 448 | 449 | it "should raise a Savon::HTTP::Error" do 450 | lambda { client.request :authenticate }.should raise_error(Savon::HTTP::Error) 451 | end 452 | end 453 | 454 | def new_response(options = {}) 455 | defaults = { :code => 200, :headers => {}, :body => Fixture.response(:authentication) } 456 | response = defaults.merge options 457 | 458 | HTTPI::Response.new response[:code], response[:headers], response[:body] 459 | end 460 | 461 | end 462 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 (UPCOMING) 2 | 3 | * Feature: Added [hooks](http://savonrb.com/#hook_into_the_system). 4 | 5 | * Feature: Savon now ships with [Savon::Model](http://rubygems.org/gems/savon_model). 6 | Savon::Model is a very simple abstraction on top of your domain models to wrap SOAP requests. 7 | It's been refactored and is now [even more useful](http://savonrb.com/#how_to_date_a_model) than before. 8 | 9 | ## 0.9.7 (2011-08-25) 10 | 11 | * Feature: Merged [pull request 210](https://github.com/rubiii/savon/pull/210) by 12 | [mboeh](https://github.com/mboeh) to add `Savon::SOAP::Response#doc` and 13 | `Savon::SOAP::Response#xpath`. 14 | 15 | * Feature: Merged [pull request 211](https://github.com/rubiii/savon/pull/211) by 16 | [mattkirman](https://github.com/mattkirman) to fix [issue 202](https://github.com/rubiii/savon/issues/202). 17 | 18 | * Feature: You can now pass a block to `Savon::SOAP::XML#body` and use Builder to create the XML: 19 | 20 | ``` ruby 21 | client.request(:find) do 22 | soap.body do |xml| 23 | xml.user do 24 | xml.id 601173 25 | end 26 | end 27 | end 28 | ``` 29 | 30 | * Fix: [issue 218](https://github.com/rubiii/savon/pull/218) - Savon now correctly handles namespaced 31 | Array items in a Hash passed to `Savon::SOAP::XML#body=`. 32 | 33 | * Fix: Merged [pull request 212](https://github.com/rubiii/savon/pull/212) to fix 34 | [savon_spec issue 2](https://github.com/rubiii/savon_spec/issues/2). 35 | 36 | * Improvement: [issue 222](https://github.com/rubiii/savon/issues/222) - Set the Content-Length header. 37 | 38 | ## 0.9.6 (2011-07-07) 39 | 40 | * Improvement/Fix: Updated Savon to use the latest version of [Wasabi](http://rubygems.org/gems/wasabi). 41 | This should fix [issue 155](https://github.com/rubiii/savon/issues/155) - Savon can automatically add namespaces 42 | to SOAP requests based on the WSDL. Users shouldn't need to do anything differently or even notice whether their WSDL 43 | hits this case; the intention is that this will "Just Work" and follow the WSDL. The SOAP details are that if 44 | elementFormDefault is specified as qualified, Savon will automatically prepend the correct XML namespaces to the 45 | elements in a SOAP request. Thanks to [jkingdon](https://github.com/jkingdon) for this. 46 | 47 | * Fix: [issue 143](https://github.com/rubiii/savon/issues/143) - Updating Wasabi should solve this issue. 48 | 49 | ## 0.9.5 (2011-07-03) 50 | 51 | * Refactoring: Extracted WSSE authentication out into the [akami](http://rubygems.org/gems/akami) gem. 52 | 53 | ## 0.9.4 (2011-07-03) 54 | 55 | * Refactoring: Extracted the WSDL parser out into the [wasabi](http://rubygems.org/gems/wasabi) gem. 56 | This should isolate upcoming improvements to the parser. 57 | 58 | ## 0.9.3 (2011-06-30) 59 | 60 | * Fix: [issue 138](https://github.com/rubiii/savon/issues/138) - 61 | Savon now supports setting a global SOAP header via `Savon.soap_header=`. 62 | 63 | * Fixed the namespace for wsse message timestamps from `wsse:Timestamp` 64 | to `wsu:Timestamp` as required by the specification. 65 | 66 | * Change: Removed support for NTLM authentication until it's stable. If you need it, you can still 67 | add the following line to your Gemfile: 68 | 69 | ``` ruby 70 | gem "httpi", "0.9.4" 71 | ``` 72 | 73 | * Refactoring: 74 | 75 | * `Hash#map_soap_response` and some of its helpers are moved to [Nori v1.0.0](http://rubygems.org/gems/nori/versions/1.0.0). 76 | Along with replacing core extensions with a proper implementation, Nori now contains a number of methods 77 | for [configuring its default behavior](https://github.com/rubiii/nori/blob/master/CHANGELOG.md): 78 | 79 | * The option whether to strip namespaces was moved to Nori.strip_namespaces 80 | * You can disable "advanced typecasting" for SOAP response values 81 | * And you can configure how SOAP response keys should be converted 82 | 83 | * `Savon::SOAP::XML.to_hash`, `Savon::SOAP::XML.parse` and `Savon::SOAP::XML.to_array` are gone. 84 | It wasn't worth keeping them around, because they didn't do much. You can simply parse a SOAP 85 | response and translate it to a Savon SOAP response Hash via: 86 | 87 | ``` ruby 88 | Nori.parse(xml)[:envelope][:body] 89 | ``` 90 | 91 | * `Savon::SOAP::Response#basic_hash` is now `Savon::SOAP::Response#hash`. 92 | 93 | ## 0.9.2 (2011-04-30) 94 | 95 | * Fix: [issue 154](https://github.com/rubiii/savon/pull/154) - 96 | Timezone format used by Savon now matches the XML schema spec. 97 | 98 | * Improvement: WSSE basic, digest and timestamp authentication are no longer mutually exclusive. 99 | Thanks to [mleon](https://github.com/mleon) for solving [issue #142](https://github.com/rubiii/savon/issues/142). 100 | 101 | * Improvement: Switched from using Crack to translate the SOAP response to a Hash to using 102 | [Nori](http://rubygems.org/gems/nori). It's based on Crack and comes with pluggable parsers. 103 | It defaults to REXML, but you can switch to Nokogiri via: 104 | 105 | ``` ruby 106 | Nori.parser = :nokogiri 107 | ``` 108 | 109 | * Improvement: WSDL parsing now uses Nokogiri instead of REXML. 110 | 111 | ## 0.9.1 (2011-04-06) 112 | 113 | * Improvement: if you're only setting the local or remote address of your wsdl document, you can 114 | now pass an (optional) String to `Savon::Client.new` to set `wsdl.document`. 115 | 116 | ``` ruby 117 | Savon::Client.new "http://example.com/UserService?wsdl" 118 | ``` 119 | 120 | * Improvement: instead of calling the `to_hash` method of your response again and again and again, 121 | there is now a ' #[]` shortcut for you. 122 | 123 | ``` ruby 124 | response[:authenticate_response][:return] 125 | ``` 126 | 127 | ## 0.9.0 (2011-04-05) 128 | 129 | * Feature: issues [#158](https://github.com/rubiii/savon/issues/158), 130 | [#169](https://github.com/rubiii/savon/issues/169) and [#172](https://github.com/rubiii/savon/issues/172) 131 | configurable "Hash key Symbol to lowerCamelCase" conversion by using the latest version of 132 | [Gyoku](http://rubygems.org/gems/gyoku). 133 | 134 | ``` ruby 135 | Gyoku.convert_symbols_to(:camelcase) 136 | Gyoku.xml(:first_name => "Mac") # => "" 137 | ``` 138 | 139 | You can even define your own conversion formular. 140 | 141 | ``` ruby 142 | Gyoku.convert_symbols_to { |key| key.upcase } 143 | Gyoku.xml(:first_name => "Mac") # => "" 144 | ``` 145 | 146 | This should also work for the SOAP input tag and SOAPAction header. So if you had to use a String for 147 | the SOAP action to call because your services uses CamelCase instead of lowerCamelCase, you can now 148 | change the default and use Symbols instead. 149 | 150 | ``` ruby 151 | Gyoku.convert_symbols_to(:camelcase) 152 | 153 | # pre Gyoku 0.4.0 154 | client.request(:get_user) # => " 155 | client.request("GetUser") # => "" 156 | 157 | # post Gyoku 0.4.0 158 | client.request(:get_user) # => "" 159 | ``` 160 | 161 | * Improvement: issues [#170](https://github.com/rubiii/savon/issues/170) and 162 | [#173](https://github.com/rubiii/savon/issues/173) Savon no longer rescues exceptions raised by 163 | `Crack::XML.parse`. If Crack complains about your WSDL document, you should take control and 164 | solve the problem instead of getting no response. 165 | 166 | * Improvement: issue [#172](https://github.com/rubiii/savon/issues/172) support for global env_namespace. 167 | 168 | ``` ruby 169 | Savon.configure do |config| 170 | config.env_namespace = :soapenv # changes the default :env namespace 171 | end 172 | ``` 173 | 174 | * Fix: [issue #163](https://github.com/rubiii/savon/issues/163) "Savon 0.8.6 not playing nicely 175 | with Httpi 0.9.0". Updating HTTPI to v0.9.1 should solve this problem. 176 | 177 | * And if you haven't already seen the new documentation: [savonrb.com](http://savonrb.com) 178 | 179 | ## 0.8.6 (2011-02-15) 180 | 181 | * Fix for issues [issue #147](https://github.com/rubiii/savon/issues/147) and [#151](https://github.com/rubiii/savon/issues/151) 182 | ([771194](https://github.com/rubiii/savon/commit/771194)). 183 | 184 | ## 0.8.5 (2011-01-28) 185 | 186 | * Fix for [issue #146](https://github.com/rubiii/savon/issues/146) ([98655c](https://github.com/rubiii/savon/commit/98655c)). 187 | 188 | * Fix for [issue #147](https://github.com/rubiii/savon/issues/147) ([252670](https://github.com/rubiii/savon/commit/252670)). 189 | 190 | ## 0.8.4 (2011-01-26) 191 | 192 | * Fix for issues [issue #130](https://github.com/rubiii/savon/issues/130) and [#134](https://github.com/rubiii/savon/issues/134) 193 | ([4f9847](https://github.com/rubiii/savon/commit/4f9847)). 194 | 195 | * Fix for [issue #91](https://github.com/rubiii/savon/issues/91) ([5c8ec1](https://github.com/rubiii/savon/commit/5c8ec1)). 196 | 197 | * Fix for [issue #135](https://github.com/rubiii/savon/issues/135) ([c9261d](https://github.com/rubiii/savon/commit/c9261d)). 198 | 199 | ## 0.8.3 (2011-01-11) 200 | 201 | * Moved implementation of `Savon::SOAP::Response#to_array` to a class method at `Savon::SOAP::XML.to_array` 202 | ([05a7d3](https://github.com/rubiii/savon/commit/05a7d3)). 203 | 204 | * Fix for [issue #131](https://github.com/rubiii/savon/issues/131) ([4e57b3](https://github.com/rubiii/savon/commit/4e57b3)). 205 | 206 | ## 0.8.2 (2011-01-04) 207 | 208 | * Fix for [issue #127](https://github.com/rubiii/savon/issues/127) ([0eb3da](https://github.com/rubiii/savon/commit/0eb3da4)). 209 | 210 | * Changed `Savon::WSSE` to be based on a Hash instead of relying on builder ([4cebc3](https://github.com/rubiii/savon/commit/4cebc3)). 211 | 212 | `Savon::WSSE` now supports wsse:Timestamp headers ([issue #122](https://github.com/rubiii/savon/issues/122)) by setting 213 | `Savon::WSSE#timestamp` to `true`: 214 | 215 | client.request :some_method do 216 | wsse.timestamp = true 217 | end 218 | 219 | or by setting `Savon::WSSE#created_at` or `Savon::WSSE#expires_at`: 220 | 221 | client.request :some_method do 222 | wsse.created_at = Time.now 223 | wsse.expires_at = Time.now + 60 224 | end 225 | 226 | You can also add custom tags to the WSSE header ([issue #69](https://github.com/rubiii/savon/issues/69)): 227 | 228 | client.request :some_method do 229 | wsse["wsse:Security"]["wsse:UsernameToken"] = { "Organization" => "ACME", "Domain" => "acme.com" } 230 | end 231 | 232 | ## 0.8.1 (2010-12-22) 233 | 234 | * Update to depend on HTTPI v0.7.5 which comes with a fallback to use Net::HTTP when no other adapter could be required. 235 | 236 | * Fix for [issue #72](https://github.com/rubiii/savon/issues/72) ([22074a](https://github.com/rubiii/savon/commit/22074a8)). 237 | 238 | * Loosen dependency on builder. Should be quite stable. 239 | 240 | ## 0.8.0 (2010-12-20) 241 | 242 | * Added `Savon::SOAP::XML#env_namespace` ([51fa0e](https://github.com/rubiii/savon/commit/51fa0e)) to configure 243 | the SOAP envelope namespace. It defaults to :env but can also be set to an empty String for SOAP envelope 244 | tags without a namespace. 245 | 246 | * Replaced quite a lot of core extensions by moving the Hash to XML translation into a new gem called 247 | [Gyoku](http://rubygems.org/gems/gyoku) ([bac4b4](https://github.com/rubiii/savon/commit/bac4b4)). 248 | 249 | ## 0.8.0.beta.4 (2010-11-20) 250 | 251 | * Fix for [issue #107](https://github.com/rubiii/savon/issues/107) ([1d6eda](https://github.com/rubiii/savon/commit/1d6eda)). 252 | 253 | * Fix for [issue #108](https://github.com/rubiii/savon/issues/108) 254 | ([f64400...0aaca2](https://github.com/rubiii/savon/compare/f64400...0aaca2)) Thanks [fagiani](https://github.com/fagiani). 255 | 256 | * Replaced `Savon.response_pattern` with a slightly different implementation of the `Savon::SOAP::Response#to_array` method 257 | ([6df6a6](https://github.com/rubiii/savon/commit/6df6a6)). The method now accepts multiple arguments representing the response 258 | Hash keys to traverse and returns the result as an Array or an empty Array in case the key is nil or does not exist. 259 | 260 | response.to_array :get_user_response, :return 261 | # => [{ :id => 1, :name => "foo"}, { :id => 2, :name => "bar"}] 262 | 263 | ## 0.8.0.beta.3 (2010-11-06) 264 | 265 | * Fix for [savon_spec](http://rubygems.org/gems/savon_spec) to not send nil to `Savon::SOAP::XML#body` 266 | ([c34b42](https://github.com/rubiii/savon/commit/c34b42)). 267 | 268 | ## 0.8.0.beta.2 (2010-11-05) 269 | 270 | * Added `Savon.response_pattern` ([0a12fb](https://github.com/rubiii/savon/commit/0a12fb)) to automatically walk deeper into 271 | the SOAP response Hash when a pattern (specified as an Array of Regexps and Symbols) matches the response. If for example 272 | your response always looks like ".+Response/return" as in: 273 | 274 | 275 | 276 | 277 | 278 | thing 279 | 280 | 281 | 282 | 283 | 284 | you could set the response pattern to: 285 | 286 | Savon.configure do |config| 287 | config.response_pattern = [/.+_response/, :return] 288 | end 289 | 290 | then instead of calling: 291 | 292 | response.to_hash[:authenticate_response][:return] # :some => "thing" 293 | 294 | to get the actual content, Savon::SOAP::Response#to_hash will try to apply given the pattern: 295 | 296 | response.to_hash # :some => "thing" 297 | 298 | Please notice, that if you don't specify a response pattern or if the pattern doesn't match the 299 | response, Savon will behave like it always did. 300 | 301 | * Added `Savon::SOAP::Response#to_array` (which also uses the response pattern). 302 | 303 | ## 0.8.0.beta.1 (2010-10-29) 304 | 305 | * Changed `Savon::Client.new` to accept a block instead of multiple Hash arguments. You can access the 306 | wsdl, http and wsse objects inside the block to configure your client for a particular service. 307 | 308 | # Instantiating a client to work with a WSDL document 309 | client = Savon::Client.new do 310 | wsdl.document = "http://example.com?wsdl" 311 | end 312 | 313 | # Directly accessing the SOAP endpoint 314 | client = Savon::Client.new do 315 | wsdl.endpoint = "http://example.com" 316 | wsdl.namespace = "http://v1.example.com" 317 | end 318 | 319 | * Fix for [issue #77](https://github.com/rubiii/savon/issues/77), which means you can now use 320 | local WSDL documents: 321 | 322 | client = Savon::Client.new do 323 | wsdl.document = "../wsdl/service.xml" 324 | end 325 | 326 | * Changed the way SOAP requests are being dispatched. Instead of using method_missing, you now use 327 | the new `request` method, which also accepts a block for you to access the wsdl, http, wsse and 328 | soap object. Please notice, that a new soap object is created for every request. So you can only 329 | access it inside this block. 330 | 331 | # A simple request to an :authenticate method 332 | client.request :authenticate do 333 | soap.body = { :id => 1 } 334 | end 335 | 336 | * The new `Savon::Client#request` method fixes issues [#37](https://github.com/rubiii/savon/issues/37), 337 | [#61](https://github.com/rubiii/savon/issues/61) and [#64](https://github.com/rubiii/savon/issues/64), 338 | which report problems with namespacing the SOAP input tag and attaching attributes to it. 339 | Some usage examples: 340 | 341 | client.request :get_user # Input tag: 342 | client.request :wsdl, "GetUser" # Input tag: 343 | client.request :get_user :active => true # Input tag: 344 | 345 | * Savon's new `request` method respects the given namespace. If you don't give it a namespace, 346 | Savon will set the target namespace to "xmlns:wsdl". But if you do specify a namespace, it will 347 | be set to the given Symbol. 348 | 349 | * Refactored Savon to use the new [HTTPI](http://rubygems.org/gems/httpi) gem. 350 | `HTTPI::Request` replaces the `Savon::Request`, so please make sure to have a look 351 | at the HTTPI library and let me know about any problems. Using HTTPI actually 352 | fixes the following two issues. 353 | 354 | * Savon now adds both "xmlns:xsd" and "xmlns:xsi" namespaces for you. Thanks Averell. 355 | It also properly serializes nil values as xsi:nil = "true". 356 | 357 | * Fix for [issue #24](https://github.com/rubiii/savon/issues/24). 358 | Instead of Net/HTTP, Savon now uses HTTPI to execute HTTP requests. 359 | HTTPI defaults to use HTTPClient which supports HTTP digest authentication. 360 | 361 | * Fix for [issue #76](https://github.com/rubiii/savon/issues/76). 362 | You now have to explicitly specify whether to use a WSDL document, when instantiating a client. 363 | 364 | * Fix for [issue #75](https://github.com/rubiii/savon/issues/75). 365 | Both `Savon::SOAP::Fault` and `Savon::HTTP::Error` now contain the `HTTPI::Response`. 366 | They also inherit from `Savon::Error`, making it easier to rescue both at the same time. 367 | 368 | * Fix for [issue #87](https://github.com/rubiii/savon/issues/87). 369 | Thanks to Leonardo Borges. 370 | 371 | * Fix for [issue #81](https://github.com/rubiii/savon/issues/81). 372 | Replaced `Savon::WSDL::Document#to_s` with a `to_xml` method. 373 | 374 | * Fix for issues [#85](https://github.com/rubiii/savon/issues/85) and [#88](https://github.com/rubiii/savon/issues/88). 375 | 376 | * Fix for [issue #80](https://github.com/rubiii/savon/issues/80). 377 | 378 | * Fix for [issue #60](https://github.com/rubiii/savon/issues/60). 379 | 380 | * Fix for [issue #96](https://github.com/rubiii/savon/issues/96). 381 | 382 | * Removed global WSSE credentials. Authentication needs to be set up for each client instance. 383 | 384 | * Started to remove quite a few core extensions. 385 | 386 | ## 0.7.9 (2010-06-14) 387 | 388 | * Fix for [issue #53](https://github.com/rubiii/savon/issues/53). 389 | 390 | ## 0.7.8 (2010-05-09) 391 | 392 | * Fixed gemspec to include missing files in the gem. 393 | 394 | ## 0.7.7 (2010-05-09) 395 | 396 | * SOAP requests now start with a proper XML declaration. 397 | 398 | * Added support for gzipped requests and responses (http://github.com/lucascs). While gzipped SOAP 399 | responses are decoded automatically, you have to manually instruct Savon to gzip SOAP requests: 400 | 401 | client = Savon::Client.new "http://example.com/UserService?wsdl", :gzip => true 402 | 403 | * Fix for [issue #51](https://github.com/rubiii/savon/issues/51). Added the :soap_endpoint option to 404 | `Savon::Client.new` which lets you specify a SOAP endpoint per client instance: 405 | 406 | client = Savon::Client.new "http://example.com/UserService?wsdl", 407 | :soap_endpoint => "http://localhost/UserService" 408 | 409 | * Fix for [issue #50](https://github.com/rubiii/savon/issues/50). Savon still escapes special characters 410 | in SOAP request Hash values, but you can now append an exclamation mark to Hash keys specifying that 411 | it's value should not be escaped. 412 | 413 | ## 0.7.6 (2010-03-21) 414 | 415 | * Moved documentation from the Github Wiki to the actual class files and established a much nicer 416 | documentation combining examples and implementation (using Hanna) at: http://savon.rubiii.com 417 | 418 | * Added `Savon::Client#call` as a workaround for dispatching calls to SOAP actions named after 419 | existing methods. Fix for [issue #48](https://github.com/rubiii/savon/issues/48). 420 | 421 | * Add support for specifying attributes for duplicate tags (via Hash values as Arrays). 422 | Fix for [issue #45](https://github.com/rubiii/savon/issues/45). 423 | 424 | * Fix for [issue #41](https://github.com/rubiii/savon/issues/41). 425 | 426 | * Fix for issues [#39](https://github.com/rubiii/savon/issues/39) and [#49](https://github.com/rubiii/savon/issues/49). 427 | Added `Savon::SOAP#xml` which let's you specify completely custom SOAP request XML. 428 | 429 | ## 0.7.5 (2010-02-19) 430 | 431 | * Fix for [issue #34](https://github.com/rubiii/savon/issues/34). 432 | 433 | * Fix for [issue #36](https://github.com/rubiii/savon/issues/36). 434 | 435 | * Added feature requested in [issue #35](https://github.com/rubiii/savon/issues/35). 436 | 437 | * Changed the key for specifying the order of tags from :@inorder to :order! 438 | 439 | ## 0.7.4 (2010-02-02) 440 | 441 | * Fix for [issue #33](https://github.com/rubiii/savon/issues/33). 442 | 443 | ## 0.7.3 (2010-01-31) 444 | 445 | * Added support for Geotrust-style WSDL documents (Julian Kornberger ). 446 | 447 | * Make HTTP requests include path and query only. This was breaking requests via proxy as scheme and host 448 | were repeated (Adrian Mugnolo ) 449 | 450 | * Avoid warning on 1.8.7 and 1.9.1 (Adrian Mugnolo ). 451 | 452 | * Fix for [issue #29](https://github.com/rubiii/savon/issues/29). 453 | Default to UTC to xs:dateTime value for WSSE authentication. 454 | 455 | * Fix for [issue #28](https://github.com/rubiii/savon/issues/28). 456 | 457 | * Fix for [issue #27](https://github.com/rubiii/savon/issues/27). The Content-Type now defaults to UTF-8. 458 | 459 | * Modification to allow assignment of an Array with an input name and an optional Hash of values to soap.input. 460 | Patches [issue #30](https://github.com/rubiii/savon/issues/30) (stanleydrew ). 461 | 462 | * Fix for [issue #25](https://github.com/rubiii/savon/issues/25). 463 | 464 | ## 0.7.2 (2010-01-17) 465 | 466 | * Exposed the `Net::HTTP` response (added by Kevin Ingolfsland). Use the `http` accessor (`response.http`) 467 | on your `Savon::Response` to access the `Net::HTTP` response object. 468 | 469 | * Fix for [issue #21](https://github.com/rubiii/savon/issues/21). 470 | 471 | * Fix for [issue #22](https://github.com/rubiii/savon/issues/22). 472 | 473 | * Fix for [issue #19](https://github.com/rubiii/savon/issues/19). 474 | 475 | * Added support for global header and namespaces. See [issue #9](https://github.com/rubiii/savon/issues/9). 476 | 477 | ## 0.7.1 (2010-01-10) 478 | 479 | * The Hash of HTTP headers for SOAP calls is now public via `Savon::Request#headers`. 480 | Patch for [issue #8](https://github.com/rubiii/savon/issues/8). 481 | 482 | ## 0.7.0 (2010-01-09) 483 | 484 | This version comes with several changes to the public API! 485 | Pay attention to the following list and read the updated Wiki: http://wiki.github.com/rubiii/savon 486 | 487 | * Changed how `Savon::WSDL` can be disabled. Instead of disabling the WSDL globally/per request via two 488 | different methods, you now simply append an exclamation mark (!) to your SOAP call: `client.get_all_users!` 489 | Make sure you know what you're doing because when the WSDL is disabled, Savon does not know about which 490 | SOAP actions are valid and just dispatches everything. 491 | 492 | * The `Net::HTTP` object used by `Savon::Request` to retrieve WSDL documents and execute SOAP calls is now public. 493 | While this makes the library even more flexible, it also comes with two major changes: 494 | 495 | * SSL client authentication needs to be defined directly on the `Net::HTTP` object: 496 | 497 | client.request.http.client_cert = ... 498 | 499 | I added a shortcut method for setting all options through a Hash similar to the previous implementation: 500 | 501 | client.request.http.ssl_client_auth :client_cert => ... 502 | 503 | * Open and read timeouts also need to be set on the `Net::HTTP` object: 504 | 505 | client.request.http.open_timeout = 30 506 | client.request.http.read_timeout = 30 507 | 508 | * Please refer to the `Net::HTTP` documentation for more details: 509 | http://www.ruby-doc.org/stdlib/libdoc/net/http/rdoc/index.html 510 | 511 | * Thanks to JulianMorrison, Savon now supports HTTP basic authentication: 512 | 513 | client.request.http.basic_auth "username", "password" 514 | 515 | * Julian also added a way to explicitly specify the order of Hash keys and values, so you should now be able 516 | to work with services requiring a specific order of input parameters while still using Hash input. 517 | 518 | client.find_user { |soap| soap.body = { :name => "Lucy", :id => 666, :@inorder => [:id, :name] } } 519 | 520 | * `Savon::Response#to_hash` now returns the content inside of "soap:Body" instead of trying to go one 521 | level deeper and return it's content. The previous implementation only worked when the "soap:Body" element 522 | contained a single child. See [issue #17](https://github.com/rubiii/savon/issues/17). 523 | 524 | * Added `Savon::SOAP#namespace` as a shortcut for setting the "xmlns:wsdl" namespace. 525 | 526 | soap.namespace = "http://example.com" 527 | 528 | ## 0.6.8 (2010-01-01) 529 | 530 | * Improved specifications for various kinds of WSDL documents. 531 | 532 | * Added support for SOAP endpoints which are different than the WSDL endpoint of a service. 533 | 534 | * Changed how SOAP actions and inputs are retrieved from the WSDL documents. This might break a few existing 535 | implementations, but makes Savon work well with even more services. If this change breaks your implementation, 536 | please take a look at the `action` and `input` methods of the `Savon::SOAP` object. 537 | One specific problem I know of is working with the createsend WSDL and its namespaced actions. 538 | 539 | To make it work, call the SOAP action without namespace and specify the input manually: 540 | 541 | client.get_api_key { |soap| soap.input = "User.GetApiKey" } 542 | 543 | ## 0.6.7 (2009-12-18) 544 | 545 | * Implemented support for a proxy server. The proxy URI can be set through an optional Hash of options passed 546 | to instantiating `Savon::Client` (Dave Woodward ) 547 | 548 | * Implemented support for SSL client authentication. Settings can be set through an optional Hash of arguments 549 | passed to instantiating `Savon::Client` (colonhyphenp) 550 | 551 | * Patch for [issue #10](https://github.com/rubiii/savon/issues/10). 552 | 553 | ## 0.6.6 (2009-12-14) 554 | 555 | * Default to use the name of the SOAP action (the method called in a client) in lowerCamelCase for SOAP action 556 | and input when Savon::WSDL is disabled. You still need to specify soap.action and maybe soap.input in case 557 | your SOAP actions are named any different. 558 | 559 | ## 0.6.5 (2009-12-13) 560 | 561 | * Added an `open_timeout` method to `Savon::Request`. 562 | 563 | ## 0.6.4 (2009-12-13) 564 | 565 | * Refactored specs to be less unit-like. 566 | 567 | * Added a getter for the `Savon::Request` to `Savon::Client` and a `read_timeout` setter for HTTP requests. 568 | 569 | * `wsdl.soap_actions` now returns an Array of SOAP actions. For the previous "mapping" please use `wsdl.operations`. 570 | 571 | * Replaced WSDL document with stream parsing. 572 | 573 | Benchmarks (1000 SOAP calls): 574 | 575 | user system total real 576 | 0.6.4 72.180000 8.280000 80.460000 (750.799011) 577 | 0.6.3 192.900000 19.630000 212.530000 (914.031865) 578 | 579 | ## 0.6.3 (2009-12-11) 580 | 581 | * Removing 2 ruby deprecation warnings for parenthesized arguments. (Dave Woodward ) 582 | 583 | * Added global and per request options for disabling `Savon::WSDL`. 584 | 585 | Benchmarks (1000 SOAP calls): 586 | 587 | user system total real 588 | WSDL 192.900000 19.630000 212.530000 (914.031865) 589 | disabled WSDL 5.680000 1.340000 7.020000 (298.265318) 590 | 591 | * Improved XPath expressions for parsing the WSDL document. 592 | 593 | Benchmarks (1000 SOAP calls): 594 | 595 | user system total real 596 | 0.6.3 192.900000 19.630000 212.530000 (914.031865) 597 | 0.6.2 574.720000 78.380000 653.100000 (1387.778539) 598 | 599 | ## 0.6.2 (2009-12-06) 600 | 601 | * Added support for changing the name of the SOAP input node. 602 | 603 | * Added a CHANGELOG. 604 | 605 | ## 0.6.1 (2009-12-06) 606 | 607 | * Fixed a problem with WSSE credentials, where every request contained a WSSE authentication header. 608 | 609 | ## 0.6.0 (2009-12-06) 610 | 611 | * `method_missing` now yields the SOAP and WSSE objects to a given block. 612 | 613 | * The response_process (which previously was a block passed to method_missing) was replaced by `Savon::Response`. 614 | 615 | * Improved SOAP action handling (another problem that came up with issue #1). 616 | 617 | ## 0.5.3 (2009-11-30) 618 | 619 | * Patch for [issue #2](https://github.com/rubiii/savon/issues/2). 620 | 621 | ## 0.5.2 (2009-11-30) 622 | 623 | * Patch for [issue #1](https://github.com/rubiii/savon/issues/1). 624 | 625 | ## 0.5.1 (2009-11-29) 626 | 627 | * Optimized default response process. 628 | 629 | * Added WSSE settings via defaults. 630 | 631 | * Added SOAP fault and HTTP error handling. 632 | 633 | * Improved documentation 634 | 635 | * Added specs 636 | 637 | ## 0.5.0 (2009-11-29) 638 | 639 | * Complete rewrite and public release. 640 | -------------------------------------------------------------------------------- /spec/fixtures/wsdl/taxcloud.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | TaxCloud Web Service 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 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 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 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | TaxCloud Web Service 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | --------------------------------------------------------------------------------