smacks-savon 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,11 @@
1
+ = Savon
2
+
3
+ Savon is a lightweight SOAP client.
4
+
5
+ == Install
6
+
7
+ $ sudo gem install smacks-savon --source http://gems.github.com
8
+
9
+ == Dependencies
10
+
11
+ Hpricot 0.6.164 (also available for JRuby)
data/lib/savon.rb ADDED
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ $:.unshift(File.join(File.dirname(__FILE__), 'savon'))
3
+ require 'response'
4
+ require 'wsdl'
5
+ require 'service'
@@ -0,0 +1,29 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rubygems'
3
+ require 'apricoteatsgorilla'
4
+
5
+ module Savon
6
+
7
+ # Savon::Response represents the SOAP response and offers different methods
8
+ # to handle the response.
9
+ class Response
10
+
11
+ # Initializer to set the SOAP response.
12
+ def initialize(response)
13
+ @response = response
14
+ end
15
+
16
+ # Returns the SOAP response message as a Hash. Call with XPath expession
17
+ # (Hpricot search) as parameter to define a custom root node. The root node
18
+ # itself will not be included in the Hash.
19
+ def to_hash(root_node = "//return")
20
+ ApricotEatsGorilla(@response.body, root_node)
21
+ end
22
+
23
+ # Returns the raw XML response.
24
+ def to_s
25
+ @response.body
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,109 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rubygems'
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'apricoteatsgorilla'
6
+
7
+ module Savon
8
+
9
+ # Savon::Service is the actual SOAP client implementation to use.
10
+ #
11
+ # Instantiate Savon::Service and pass in the WSDL of the service you would
12
+ # like to work with. Then simply call the SOAP service method on your
13
+ # instance (which will be catched via method_missing) and pass in a Hash
14
+ # of options you would like to send.
15
+ #
16
+ # Example:
17
+ # proxy = Savon::Service.new "http://example.com/ExampleService?wsdl"
18
+ # response = proxy.findExampleById(:id => "123")
19
+ #
20
+ # Get the raw response XML:
21
+ # response.to_s
22
+ #
23
+ # Get it as a Hash (offers optional XPath expression to set a custom root node):
24
+ # response.to_hash
25
+ # response.to_hash("//return")
26
+ #
27
+ # Or as a Mash object (also offers specifying a custom root node):
28
+ # response.to_mash
29
+ # response.to_mash("//user/email")
30
+ class Service
31
+
32
+ # Sets the HTTP connection instance.
33
+ attr_writer :http
34
+
35
+ # Initializer sets the endpoint URI.
36
+ def initialize(endpoint)
37
+ @uri = URI(endpoint)
38
+ end
39
+
40
+ # Returns an Wsdl instance.
41
+ def wsdl
42
+ @wsdl = Savon::Wsdl.new(@uri, http) if @wsdl.nil?
43
+ @wsdl
44
+ end
45
+
46
+ private
47
+
48
+ # Sets up the request headers and body, makes the request and returns a
49
+ # Savon::Response object.
50
+ def call_service
51
+ headers = { 'Content-Type' => 'text/xml; charset=utf-8', 'SOAPAction' => @action }
52
+ body = ApricotEatsGorilla.soap_envelope("wsdl" => wsdl.namespace_uri) do
53
+ ApricotEatsGorilla("wsdl:#{@action}" => namespaced_options)
54
+ end
55
+ response = @http.request_post(@uri.path, body, headers)
56
+ Savon::Response.new(response)
57
+ end
58
+
59
+ # Returns an HTTP connection instance.
60
+ def http
61
+ if @http.nil?
62
+ raise ArgumentError, "Invalid endpoint URI" unless @uri.scheme
63
+ @http = Net::HTTP.new(@uri.host, @uri.port)
64
+ #@http.set_debug_output(STDOUT)
65
+ #@http.read_timeout = 5
66
+ end
67
+ @http
68
+ end
69
+
70
+ # Checks if the requestion SOAP action is available.
71
+ # Raises an ArgumentError in case it isn't.
72
+ def validate_action
73
+ unless wsdl.service_methods.include? @action
74
+ raise ArgumentError, "Invalid service method '#{@action}'"
75
+ end
76
+ end
77
+
78
+ # Checks if there were any choice elements found in the wsdl and namespaces
79
+ # the corresponding keys from the passed in Hash of options.
80
+ def namespaced_options
81
+ return @options if wsdl.choice_elements.empty?
82
+
83
+ options = {}
84
+ @options.each do |key, value|
85
+ key = "wsdl:#{key}" if wsdl.choice_elements.include? key.to_s
86
+
87
+ current = options[key]
88
+ case current
89
+ when Array
90
+ options[key] << value
91
+ when nil
92
+ options[key] = value
93
+ else
94
+ options[key] = [current.dup, value]
95
+ end
96
+ end
97
+ options
98
+ end
99
+
100
+ # Catches calls to SOAP service methods.
101
+ def method_missing(method, options = {})
102
+ @action = method.to_s
103
+ @options = options
104
+ validate_action
105
+ call_service
106
+ end
107
+
108
+ end
109
+ end
data/lib/savon/wsdl.rb ADDED
@@ -0,0 +1,60 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rubygems'
3
+ require 'net/http'
4
+ require 'hpricot'
5
+
6
+ module Savon
7
+
8
+ # Savon::Wsdl gets, parses and represents the SOAP-WSDL.
9
+ class Wsdl
10
+
11
+ # The namespace URI.
12
+ attr_reader :namespace_uri
13
+
14
+ # Available service methods.
15
+ attr_reader :service_methods
16
+
17
+ # Choice elements.
18
+ attr_reader :choice_elements
19
+
20
+ # Initializer expects an endpoint URI and an HTTP connection instance.
21
+ # Gets and parses the WSDL at the given URI.
22
+ def initialize(uri, http)
23
+ @uri = uri
24
+ @http = http
25
+ get_wsdl
26
+ parse_wsdl
27
+ end
28
+
29
+ # Returns the response body from the WSDL request.
30
+ def to_s
31
+ @response.body
32
+ end
33
+
34
+ private
35
+
36
+ # Gets the WSDL at the given URI.
37
+ def get_wsdl
38
+ @response = @http.get("#{@uri.path}?#{@uri.query}")
39
+ @doc = Hpricot.XML(@response.body)
40
+ end
41
+
42
+ # Parses the WSDL for the namespace URI, available service methods
43
+ # and choice elements.
44
+ def parse_wsdl
45
+ @namespace_uri = @doc.at('//wsdl:definitions').get_attribute('targetNamespace')
46
+
47
+ @service_methods = []
48
+ @doc.search('//soap:operation').each do |operation|
49
+ service_methods << operation.parent.get_attribute('name')
50
+ end
51
+
52
+ @choice_elements = []
53
+ @doc.search('//xs:choice//xs:element').each do |choice|
54
+ name = choice.get_attribute('ref').sub(/(.+):/, '')
55
+ choice_elements << name unless @choice_elements.include? name
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,127 @@
1
+ class WsdlFactory
2
+ attr_accessor :namespace_uri, :service_methods, :choice_elements
3
+
4
+ def initialize(new_options = {})
5
+ options = {
6
+ :namespace_uri => "http://some.example.com",
7
+ :service_methods => {"findById" => ["id"]},
8
+ :choice_elements => {}
9
+ }.update(new_options)
10
+
11
+ @namespace_uri = options[:namespace_uri]
12
+ @service_methods = options[:service_methods]
13
+ @choice_elements = options[:choice_elements]
14
+ end
15
+
16
+ def build
17
+ wsdl = '<wsdl:definitions name="SomeService" targetNamespace="' << namespace_uri << '"
18
+ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://example.com"
19
+ xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
20
+ <wsdl:types>
21
+ <xs:schema attributeFormDefault="unqualified" elementFormDefault="unqualified"
22
+ targetNamespace="http://example.com" xmlns:tns="http://example.com"
23
+ xmlns:xs="http://www.w3.org/2001/XMLSchema">'
24
+ wsdl << build_elements
25
+ wsdl << '<xs:element name="result" type="tns:result" />'
26
+ wsdl << build_complex_types
27
+ wsdl << '<xs:complexType name="result">
28
+ <xs:sequence><xs:element name="token" type="xs:token" /></xs:sequence>
29
+ </xs:complexType>
30
+ </xs:schema>
31
+ </wsdl:types>'
32
+ wsdl << build_messages
33
+ wsdl << '<wsdl:portType name="SomeWebService">'
34
+ wsdl << build_operation_input_output
35
+ wsdl << '</wsdl:portType>
36
+ <wsdl:binding name="SomeServiceSoapBinding" type="tns:SomeService">
37
+ <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />'
38
+ wsdl << build_operation_input_output_body
39
+ wsdl << '</wsdl:binding>
40
+ <wsdl:service name="SomeService">
41
+ <wsdl:port binding="tns:SomeServiceSoapBinding" name="SomeServicePort">
42
+ <soap:address location="http://example.com/SomeService" />
43
+ </wsdl:port>
44
+ </wsdl:service>
45
+ </wsdl:definitions>'
46
+ end
47
+
48
+ def build_elements
49
+ wsdl = service_methods.keys.map { |method|
50
+ '<xs:element name="' << method << '" type="tns:' << method << '" />
51
+ <xs:element name="' << method << 'Response" type="tns:' << method << 'Response" />'
52
+ }.to_s
53
+ wsdl << choice_elements.map { |c_method, c_elements|
54
+ c_elements.map { |c_element|
55
+ '<xs:element name="' << c_element << '" type="tns:' << c_element << 'Value" />'
56
+ }.to_s
57
+ }.to_s
58
+ wsdl
59
+ end
60
+
61
+ def build_complex_types
62
+ service_methods.map { |method, inputs|
63
+ wsdl = '<xs:complexType name="' << method << '"><xs:sequence>'
64
+ inputs.each do |input|
65
+ if choice_elements.keys.include? input
66
+ wsdl << '<xs:choice>'
67
+ wsdl << choice_elements[input].map { |element|
68
+ '<xs:element ref="tns:' << element << '"/>'
69
+ }.to_s
70
+ wsdl << '</xs:choice>'
71
+ else
72
+ wsdl << '<xs:element minOccurs="0" name="' << input << '" type="xs:string" />'
73
+ end
74
+ end
75
+ wsdl << '</xs:sequence></xs:complexType>'
76
+ wsdl << build_complex_types_choice_elements
77
+ wsdl << '<xs:complexType name="' << method << 'Response"><xs:sequence>
78
+ <xs:element minOccurs="0" name="return" type="tns:result" />
79
+ </xs:sequence></xs:complexType>'
80
+ }.to_s
81
+ end
82
+
83
+ def build_complex_types_choice_elements
84
+ choice_elements.map { |c_method, c_elements|
85
+ c_elements.map { |c_element|
86
+ '<xs:complexType name="' << c_element << 'Value"><xs:sequence>
87
+ <xs:element minOccurs="0" name="' << c_element << '" type="xs:string" />
88
+ </xs:sequence></xs:complexType>'
89
+ }.to_s
90
+ }.to_s
91
+ end
92
+
93
+ def build_messages
94
+ service_methods.keys.map { |method|
95
+ '<wsdl:message name="' << method << '">
96
+ <wsdl:part element="tns:' << method << '" name="parameters"> </wsdl:part>
97
+ </wsdl:message>
98
+ <wsdl:message name="' << method << 'Response">
99
+ <wsdl:part element="tns:' << method << 'Response" name="parameters"> </wsdl:part>
100
+ </wsdl:message>'
101
+ }.to_s
102
+ end
103
+
104
+ def build_operation_input_output
105
+ service_methods.keys.map { |method|
106
+ '<wsdl:operation name="' << method << '">
107
+ <wsdl:input message="tns:' << method << '" name="' << method << '"> </wsdl:input>
108
+ <wsdl:output message="tns:' << method << 'Response" name="' << method << 'Response"> </wsdl:output>
109
+ </wsdl:operation>'
110
+ }.to_s
111
+ end
112
+
113
+ def build_operation_input_output_body
114
+ service_methods.keys.map { |method|
115
+ '<wsdl:operation name="' << method << '">
116
+ <soap:operation soapAction="" style="document" />
117
+ <wsdl:input name="' << method << '">
118
+ <soap:body use="literal" />
119
+ </wsdl:input>
120
+ <wsdl:output name="' << method << 'Response">
121
+ <soap:body use="literal" />
122
+ </wsdl:output>
123
+ </wsdl:operation>'
124
+ }.to_s
125
+ end
126
+
127
+ end
@@ -0,0 +1,15 @@
1
+ module SoapResponseFixture
2
+
3
+ def some_soap_response
4
+ '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
5
+ <soap:Body>
6
+ <ns2:result xmlns:ns2="http://example.com/">
7
+ <return>
8
+ <token>secret</token>
9
+ </return>
10
+ </ns2:result>
11
+ </soap:Body>
12
+ </soap:Envelope>'
13
+ end
14
+
15
+ end
data/tests/helper.rb ADDED
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'mocha'
4
+ require 'shoulda'
5
+ require "apricoteatsgorilla"
6
+
7
+ ["service", "wsdl", "response"].each do |file|
8
+ require File.join(File.dirname(__FILE__), "..", "lib", "savon", file)
9
+ end
10
+
11
+ require File.join(File.dirname(__FILE__), "factories", "wsdl")
12
+ require File.join(File.dirname(__FILE__), "fixtures", "soap_response")
13
+
14
+ module TestHelper
15
+
16
+ def some_url
17
+ "http://example.com"
18
+ end
19
+
20
+ def some_uri
21
+ URI(some_url)
22
+ end
23
+
24
+ def service_http_mock(response_body)
25
+ http_mock = mock()
26
+ http_mock.expects(:get).returns(response_mock(WsdlFactory.new.build))
27
+ http_mock.expects(:request_post).returns(response_mock(response_body))
28
+ http_mock
29
+ end
30
+
31
+ def http_mock(response_body)
32
+ http_mock = mock()
33
+ http_mock.expects(:get).returns(response_mock(response_body))
34
+ http_mock
35
+ end
36
+
37
+ def response_mock(response_body)
38
+ response_mock = mock('Net::HTTPResponse')
39
+ response_mock.stubs(
40
+ :code => '200', :message => "OK", :content_type => "text/html",
41
+ :body => response_body
42
+ )
43
+ response_mock
44
+ end
45
+
46
+ end
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.join(File.dirname(__FILE__), "..", "helper")
3
+
4
+ class SavonResponseTest < Test::Unit::TestCase
5
+
6
+ include TestHelper
7
+ include SoapResponseFixture
8
+
9
+ context "Savon::Response with some SOAP response" do
10
+ setup do
11
+ ApricotEatsGorilla.sort_keys = true
12
+ @response = Savon::Response.new(response_mock(some_soap_response))
13
+ end
14
+
15
+ should "return a Hash on to_hash" do
16
+ assert_kind_of Hash, @response.to_hash
17
+ end
18
+
19
+ should "return a Hash equal to the response on to_hash" do
20
+ assert_equal ApricotEatsGorilla(some_soap_response, "//return"), @response.to_hash
21
+ end
22
+
23
+ should "return the raw XML response on to_s" do
24
+ assert_equal some_soap_response, @response.to_s
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,32 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.join(File.dirname(__FILE__), "..", "helper")
3
+
4
+ class SavonServiceTest < Test::Unit::TestCase
5
+
6
+ include TestHelper
7
+ include SoapResponseFixture
8
+
9
+ context "Savon::Service" do
10
+ setup do
11
+ @service = Savon::Service.new(some_url)
12
+ @service.http = service_http_mock(some_soap_response)
13
+ @result = @service.findById
14
+ end
15
+
16
+ should "return a Savon::Response object containing the given response" do
17
+ assert_kind_of Savon::Response, @result
18
+ assert_equal some_soap_response, @result.to_s
19
+ end
20
+
21
+ should "return an instance of Savon::Wsdl on wsdl" do
22
+ assert_kind_of Savon::Wsdl, @service.wsdl
23
+ end
24
+
25
+ should "raise an ArgumentError when called with an invalid action" do
26
+ assert_raise ArgumentError do
27
+ @service.somethingCrazy
28
+ end
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.join(File.dirname(__FILE__), "..", "helper")
3
+
4
+ class SavonWsdlTest < Test::Unit::TestCase
5
+
6
+ include TestHelper
7
+
8
+ context "Savon::Wsdl without choice elements" do
9
+ setup do
10
+ @some_factory = WsdlFactory.new
11
+ @some_wsdl = Savon::Wsdl.new(some_uri, http_mock(@some_factory.build))
12
+ end
13
+
14
+ should "return the namespace_uri" do
15
+ assert_equal @some_factory.namespace_uri, @some_wsdl.namespace_uri
16
+ end
17
+
18
+ should "return the available service_methods" do
19
+ assert_equal @some_factory.service_methods.keys, @some_wsdl.service_methods
20
+ end
21
+
22
+ should "not find any choice elements, so choice_elements returns []" do
23
+ assert_equal @some_factory.choice_elements.keys, @some_wsdl.choice_elements
24
+ end
25
+ end
26
+
27
+ context "Savon::Wsdl with choice elements" do
28
+ setup do
29
+ @choice_factory = WsdlFactory.new(
30
+ :service_methods => {"findUser" => ["credential"]},
31
+ :choice_elements => {"credential" => ["id", "email"]}
32
+ )
33
+ @choice_wsdl = Savon::Wsdl.new(some_uri, http_mock(@choice_factory.build))
34
+ end
35
+
36
+ should "return the available choice elements" do
37
+ assert_equal @choice_factory.choice_elements["credential"], @choice_wsdl.choice_elements
38
+ end
39
+
40
+ should "return the raw SOAP response on to_s" do
41
+ assert_equal @choice_factory.build, @choice_wsdl.to_s
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ $:.unshift(File.join(File.dirname(__FILE__), 'savon'))
3
+ require "service_test"
4
+ require "wsdl_test"
5
+ require "response_test"
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smacks-savon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Harrington
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-16 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hpricot
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.6.164
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: smacks-apricoteatsgorilla
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.6
34
+ version:
35
+ description: Savon is a lightweight SOAP client.
36
+ email:
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.rdoc
43
+ files:
44
+ - README.rdoc
45
+ - lib/savon.rb
46
+ - lib/savon/service.rb
47
+ - lib/savon/wsdl.rb
48
+ - lib/savon/response.rb
49
+ has_rdoc: true
50
+ homepage: http://github.com/smacks/savon
51
+ post_install_message:
52
+ rdoc_options:
53
+ - --inline-source
54
+ - --charset=UTF-8
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements:
70
+ - mocha for testing
71
+ - shoulda for testing
72
+ - apricoteatsgorilla for testing
73
+ rubyforge_project:
74
+ rubygems_version: 1.2.0
75
+ signing_key:
76
+ specification_version: 2
77
+ summary: Savon is a lightweight SOAP client.
78
+ test_files:
79
+ - tests/savon_test.rb
80
+ - tests/helper.rb
81
+ - tests/factories/wsdl.rb
82
+ - tests/fixtures/soap_response.rb
83
+ - tests/savon/service_test.rb
84
+ - tests/savon/wsdl_test.rb
85
+ - tests/savon/response_test.rb