├── .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 [](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 /^ then document
35 | else File.read(document)
36 | end
37 | end
38 |
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/savon.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path("../lib", __FILE__)
2 | $:.unshift lib unless $:.include? lib
3 |
4 | require "savon/version"
5 |
6 | Gem::Specification.new do |s|
7 | s.name = "savon"
8 | s.version = Savon::Version
9 | s.authors = "Daniel Harrington"
10 | s.email = "me@rubiii.com"
11 | s.homepage = "http://savonrb.com"
12 | s.summary = "Heavy metal Ruby SOAP client"
13 | s.description = "Ruby's heavy metal SOAP client"
14 |
15 | s.rubyforge_project = s.name
16 |
17 | s.add_dependency "builder", ">= 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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------