lolsoap 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.travis.yml +7 -0
- data/.yardopts +1 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +22 -0
- data/LICENSE.txt +20 -0
- data/README.md +124 -0
- data/Rakefile +29 -0
- data/VERSION +1 -0
- data/lib/lolsoap.rb +11 -0
- data/lib/lolsoap/builder.rb +93 -0
- data/lib/lolsoap/client.rb +25 -0
- data/lib/lolsoap/envelope.rb +94 -0
- data/lib/lolsoap/errors.rb +15 -0
- data/lib/lolsoap/fault.rb +26 -0
- data/lib/lolsoap/hash_builder.rb +48 -0
- data/lib/lolsoap/request.rb +54 -0
- data/lib/lolsoap/response.rb +50 -0
- data/lib/lolsoap/wsdl.rb +98 -0
- data/lib/lolsoap/wsdl/element.rb +28 -0
- data/lib/lolsoap/wsdl/null_element.rb +15 -0
- data/lib/lolsoap/wsdl/null_type.rb +19 -0
- data/lib/lolsoap/wsdl/operation.rb +18 -0
- data/lib/lolsoap/wsdl/type.rb +38 -0
- data/lib/lolsoap/wsdl_parser.rb +121 -0
- data/lolsoap.gemspec +97 -0
- data/test/fixtures/stock_quote.wsdl +74 -0
- data/test/fixtures/stock_quote_fault.xml +16 -0
- data/test/fixtures/stock_quote_response.xml +8 -0
- data/test/helper.rb +14 -0
- data/test/integration/test_client.rb +20 -0
- data/test/integration/test_envelope.rb +45 -0
- data/test/integration/test_request.rb +19 -0
- data/test/integration/test_response.rb +15 -0
- data/test/integration/test_wsdl.rb +28 -0
- data/test/unit/test_builder.rb +95 -0
- data/test/unit/test_client.rb +12 -0
- data/test/unit/test_envelope.rb +112 -0
- data/test/unit/test_fault.rb +33 -0
- data/test/unit/test_hash_builder.rb +127 -0
- data/test/unit/test_request.rb +48 -0
- data/test/unit/test_response.rb +39 -0
- data/test/unit/test_wsdl.rb +143 -0
- data/test/unit/test_wsdl_parser.rb +105 -0
- data/test/unit/wsdl/test_element.rb +31 -0
- data/test/unit/wsdl/test_type.rb +44 -0
- metadata +152 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module LolSoap
|
2
|
+
class Fault
|
3
|
+
attr_reader :request, :node
|
4
|
+
|
5
|
+
def initialize(request, node)
|
6
|
+
@request = request
|
7
|
+
@node = node
|
8
|
+
end
|
9
|
+
|
10
|
+
def soap_namespace
|
11
|
+
request.soap_namespace
|
12
|
+
end
|
13
|
+
|
14
|
+
def code
|
15
|
+
node.at_xpath('./soap:Code/soap:Value', 'soap' => soap_namespace).text.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def reason
|
19
|
+
node.at_xpath('./soap:Reason/soap:Text', 'soap' => soap_namespace).text.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def detail
|
23
|
+
node.at_xpath('./soap:Detail/*', 'soap' => soap_namespace).to_xml
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module LolSoap
|
2
|
+
# Turns an XML node into a hash data structure. Works out which elements
|
3
|
+
# are supposed to be collections based on the type information.
|
4
|
+
class HashBuilder
|
5
|
+
attr_reader :node, :type
|
6
|
+
|
7
|
+
def initialize(node, type)
|
8
|
+
@node = node
|
9
|
+
@type = type
|
10
|
+
end
|
11
|
+
|
12
|
+
def output
|
13
|
+
if children.any?
|
14
|
+
children_hash
|
15
|
+
else
|
16
|
+
node.text.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def children
|
21
|
+
@children ||= node.children.select(&:element?)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# @private
|
27
|
+
def children_hash
|
28
|
+
hash = {}
|
29
|
+
children.each do |child|
|
30
|
+
element = type.element(child.name)
|
31
|
+
output = self.class.new(child, element.type).output
|
32
|
+
|
33
|
+
if Array === hash[child.name] || !element.singular?
|
34
|
+
hash[child.name] ||= []
|
35
|
+
hash[child.name] << output
|
36
|
+
else
|
37
|
+
if hash.include?(child.name)
|
38
|
+
hash[child.name] = [hash[child.name]]
|
39
|
+
hash[child.name] << output
|
40
|
+
else
|
41
|
+
hash[child.name] = output
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
hash
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module LolSoap
|
2
|
+
# Represents a HTTP request containing a SOAP Envelope
|
3
|
+
class Request
|
4
|
+
attr_reader :envelope
|
5
|
+
|
6
|
+
def initialize(envelope)
|
7
|
+
@envelope = envelope
|
8
|
+
end
|
9
|
+
|
10
|
+
# @see Envelope#body
|
11
|
+
def body(&block)
|
12
|
+
envelope.body(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
# @see Envelope#header
|
16
|
+
def header(&block)
|
17
|
+
envelope.header(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Namespace used for SOAP envelope tags
|
21
|
+
def soap_namespace
|
22
|
+
envelope.soap_namespace
|
23
|
+
end
|
24
|
+
|
25
|
+
# URL to be POSTed to
|
26
|
+
def url
|
27
|
+
envelope.endpoint
|
28
|
+
end
|
29
|
+
|
30
|
+
# The type of the element sent in the request body
|
31
|
+
def input_type
|
32
|
+
envelope.input_type
|
33
|
+
end
|
34
|
+
|
35
|
+
# The type of the element that will be received in the response body
|
36
|
+
def output_type
|
37
|
+
envelope.output_type
|
38
|
+
end
|
39
|
+
|
40
|
+
# Headers that must be set when making the request
|
41
|
+
def headers
|
42
|
+
{
|
43
|
+
'Content-Type' => 'application/soap+xml;charset=UTF-8',
|
44
|
+
'Content-Length' => content.bytesize.to_s,
|
45
|
+
'SOAPAction' => envelope.action
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# The content to be sent in the HTTP request
|
50
|
+
def content
|
51
|
+
@content ||= envelope.to_xml
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'lolsoap/errors'
|
2
|
+
require 'lolsoap/fault'
|
3
|
+
require 'lolsoap/hash_builder'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
module LolSoap
|
7
|
+
class Response
|
8
|
+
attr_reader :request, :doc
|
9
|
+
|
10
|
+
# Create a new instance from a raw XML string
|
11
|
+
def self.parse(request, raw)
|
12
|
+
new(request, Nokogiri::XML::Document.parse(raw))
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(request, doc)
|
16
|
+
@request = request
|
17
|
+
@doc = doc
|
18
|
+
|
19
|
+
raise FaultRaised.new(fault) if fault
|
20
|
+
end
|
21
|
+
|
22
|
+
# Namespace used for SOAP Envelope tags
|
23
|
+
def soap_namespace
|
24
|
+
request.soap_namespace
|
25
|
+
end
|
26
|
+
|
27
|
+
# The XML node for the body of the envelope
|
28
|
+
def body
|
29
|
+
@body ||= doc.at_xpath('/soap:Envelope/soap:Body/*', 'soap' => soap_namespace)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Convert the body node to a Hash, using WSDL type data to determine the structure
|
33
|
+
def body_hash(builder = HashBuilder)
|
34
|
+
builder.new(body, request.output_type).output
|
35
|
+
end
|
36
|
+
|
37
|
+
# The XML node for the header of the envelope
|
38
|
+
def header
|
39
|
+
@header ||= doc.at_xpath('/soap:Envelope/soap:Header', 'soap' => soap_namespace)
|
40
|
+
end
|
41
|
+
|
42
|
+
# SOAP fault, if any (an exception will be raised in the initializer, if there is one)
|
43
|
+
def fault
|
44
|
+
@fault ||= begin
|
45
|
+
node = doc.at_xpath('/soap:Envelope/soap:Body/soap:Fault', 'soap' => soap_namespace)
|
46
|
+
Fault.new(request, node) if node
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/lolsoap/wsdl.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'lolsoap/wsdl_parser'
|
2
|
+
|
3
|
+
module LolSoap
|
4
|
+
class WSDL
|
5
|
+
require 'lolsoap/wsdl/operation'
|
6
|
+
require 'lolsoap/wsdl/type'
|
7
|
+
require 'lolsoap/wsdl/null_type'
|
8
|
+
require 'lolsoap/wsdl/element'
|
9
|
+
require 'lolsoap/wsdl/null_element'
|
10
|
+
|
11
|
+
# Create a new instance by parsing a raw string of XML
|
12
|
+
def self.parse(raw)
|
13
|
+
new(WSDLParser.parse(raw))
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :parser
|
17
|
+
|
18
|
+
def initialize(parser)
|
19
|
+
@parser = parser
|
20
|
+
end
|
21
|
+
|
22
|
+
# Hash of operations that are supports by the SOAP service
|
23
|
+
def operations
|
24
|
+
load_operations.dup
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get a single operation
|
28
|
+
def operation(name)
|
29
|
+
load_operations[name]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Hash of types declared by the service
|
33
|
+
def types
|
34
|
+
load_types.dup
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get a single type, or a NullType if the type doesn't exist
|
38
|
+
def type(name)
|
39
|
+
load_types.fetch(name) { NullType.new }
|
40
|
+
end
|
41
|
+
|
42
|
+
# The SOAP endpoint URL
|
43
|
+
def endpoint
|
44
|
+
parser.endpoint
|
45
|
+
end
|
46
|
+
|
47
|
+
# Hash of namespaces used in the WSDL document (keys are prefixes)
|
48
|
+
def namespaces
|
49
|
+
parser.namespaces
|
50
|
+
end
|
51
|
+
|
52
|
+
# Hash of namespace prefixes used in the WSDL document (keys are namespace URIs)
|
53
|
+
def prefixes
|
54
|
+
namespaces.invert
|
55
|
+
end
|
56
|
+
|
57
|
+
# Namespaces used by the types (a subset of #namespaces)
|
58
|
+
def type_namespaces
|
59
|
+
Hash[parser.types.map { |k, t| [prefixes[t[:namespace]], t[:namespace]] }]
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
"<LolSoap::WSDL " \
|
64
|
+
"namespaces=#{namespaces.inspect} " \
|
65
|
+
"operations=#{operations.keys.inspect} " \
|
66
|
+
"types=#{types.keys.inspect}>"
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# @private
|
72
|
+
def load_operations
|
73
|
+
@operations ||= Hash[
|
74
|
+
parser.operations.map do |k, op|
|
75
|
+
[k, Operation.new(self, op[:action], type(op[:input][:name]), type(op[:output][:name]))]
|
76
|
+
end
|
77
|
+
]
|
78
|
+
end
|
79
|
+
|
80
|
+
# @private
|
81
|
+
def load_types
|
82
|
+
@types ||= Hash[
|
83
|
+
parser.types.map do |name, type|
|
84
|
+
[name, Type.new(self, name, type[:namespace], build_elements(type[:elements]))]
|
85
|
+
end
|
86
|
+
]
|
87
|
+
end
|
88
|
+
|
89
|
+
# @private
|
90
|
+
def build_elements(elements)
|
91
|
+
Hash[
|
92
|
+
elements.map do |name, el|
|
93
|
+
[name, Element.new(self, name, el[:type], el[:singular])]
|
94
|
+
end
|
95
|
+
]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class LolSoap::WSDL
|
2
|
+
class Element
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(wsdl, name, type_name, singular = true)
|
6
|
+
@wsdl = wsdl
|
7
|
+
@name = name
|
8
|
+
@type_name = type_name
|
9
|
+
@singular = singular
|
10
|
+
end
|
11
|
+
|
12
|
+
def type
|
13
|
+
wsdl.type(@type_name.split(':').last)
|
14
|
+
end
|
15
|
+
|
16
|
+
def singular?
|
17
|
+
@singular == true
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
"<LolSoap::WSDL::Element name=#{name.inspect} type=#{@type_name.inspect}>"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def wsdl; @wsdl; end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class LolSoap::WSDL
|
2
|
+
class Operation
|
3
|
+
attr_reader :wsdl, :action, :input, :output
|
4
|
+
|
5
|
+
def initialize(wsdl, action, input, output)
|
6
|
+
@wsdl = wsdl
|
7
|
+
@action = action
|
8
|
+
@input = input
|
9
|
+
@output = output
|
10
|
+
end
|
11
|
+
|
12
|
+
def inspect
|
13
|
+
"<LolSoap::WSDL::Operation " \
|
14
|
+
"action=#{action.inspect} " \
|
15
|
+
"input=#{input.inspect}>"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class LolSoap::WSDL
|
2
|
+
class Type
|
3
|
+
attr_reader :name, :namespace
|
4
|
+
|
5
|
+
def initialize(wsdl, name, namespace, elements)
|
6
|
+
@wsdl = wsdl
|
7
|
+
@name = name
|
8
|
+
@namespace = namespace
|
9
|
+
@elements = elements
|
10
|
+
end
|
11
|
+
|
12
|
+
def elements
|
13
|
+
@elements.dup
|
14
|
+
end
|
15
|
+
|
16
|
+
def element(name)
|
17
|
+
@elements.fetch(name) { NullElement.new }
|
18
|
+
end
|
19
|
+
|
20
|
+
def sub_type(name)
|
21
|
+
element(name).type
|
22
|
+
end
|
23
|
+
|
24
|
+
def prefix
|
25
|
+
wsdl.prefixes[namespace]
|
26
|
+
end
|
27
|
+
|
28
|
+
def inspect
|
29
|
+
"<LolSoap::WSDL::Type " \
|
30
|
+
"name=#{(prefix + ':' + name).inspect} " \
|
31
|
+
"elements=#{elements.inspect}>"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def wsdl; @wsdl; end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module LolSoap
|
4
|
+
# @private
|
5
|
+
class WSDLParser
|
6
|
+
NS = {
|
7
|
+
:wsdl => 'http://schemas.xmlsoap.org/wsdl/',
|
8
|
+
:soap => 'http://schemas.xmlsoap.org/wsdl/soap12/',
|
9
|
+
:xmlschema => 'http://www.w3.org/2001/XMLSchema'
|
10
|
+
}
|
11
|
+
|
12
|
+
attr_reader :doc
|
13
|
+
|
14
|
+
def self.parse(raw)
|
15
|
+
new(Nokogiri::XML::Document.parse(raw))
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(doc)
|
19
|
+
@doc = doc
|
20
|
+
end
|
21
|
+
|
22
|
+
def namespaces
|
23
|
+
@namespaces ||= begin
|
24
|
+
namespaces = Hash[doc.collect_namespaces.map { |k, v| [k.sub(/^xmlns:/, ''), v] }]
|
25
|
+
namespaces.delete('xmlns')
|
26
|
+
namespaces
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def endpoint
|
31
|
+
@endpoint ||= doc.at_xpath(
|
32
|
+
'/d:definitions/d:service/d:port/soap:address/@location',
|
33
|
+
'd' => NS[:wsdl], 'soap' => NS[:soap]
|
34
|
+
).to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def types
|
38
|
+
@types ||= begin
|
39
|
+
types = doc.xpath(
|
40
|
+
'/d:definitions/d:types/s:schema/s:element[@name]',
|
41
|
+
'/d:definitions/d:types/s:schema/s:complexType[@name]',
|
42
|
+
'd' => NS[:wsdl], 's' => NS[:xmlschema]
|
43
|
+
)
|
44
|
+
Hash[
|
45
|
+
types.map do |type|
|
46
|
+
namespace = type.at_xpath('ancestor::s:schema/@targetNamespace', 's' => NS[:xmlschema]).to_s
|
47
|
+
elements = type.xpath('.//s:element', 's' => NS[:xmlschema])
|
48
|
+
name = type.attribute('name').to_s
|
49
|
+
|
50
|
+
[
|
51
|
+
name,
|
52
|
+
{
|
53
|
+
:name => name,
|
54
|
+
:namespace => namespace,
|
55
|
+
:elements => Hash[elements.map { |e| [e.attribute('name').to_s, element_hash(e)] }]
|
56
|
+
}
|
57
|
+
]
|
58
|
+
end
|
59
|
+
]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def messages
|
64
|
+
@messages ||= Hash[
|
65
|
+
doc.xpath('/d:definitions/d:message', 'd' => NS[:wsdl]).map do |msg|
|
66
|
+
element = msg.at_xpath('./d:part/@element', 'd' => NS[:wsdl]).to_s
|
67
|
+
[msg.attribute('name').to_s, types[element.split(':').last]]
|
68
|
+
end
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
def port_type_operations
|
73
|
+
@port_type_operations ||= Hash[
|
74
|
+
doc.xpath('/d:definitions/d:portType/d:operation', 'd' => NS[:wsdl]).map do |op|
|
75
|
+
input = op.at_xpath('./d:input/@message', 'd' => NS[:wsdl]).to_s.split(':').last
|
76
|
+
output = op.at_xpath('./d:output/@message', 'd' => NS[:wsdl]).to_s.split(':').last
|
77
|
+
name = op.attribute('name').to_s
|
78
|
+
|
79
|
+
[name, { :name => name, :input => messages[input], :output => messages[output] }]
|
80
|
+
end
|
81
|
+
]
|
82
|
+
end
|
83
|
+
|
84
|
+
def operations
|
85
|
+
@operations ||= begin
|
86
|
+
binding = doc.at_xpath(
|
87
|
+
'/d:definitions/d:service/d:port/soap:address/../@binding',
|
88
|
+
'd' => NS[:wsdl], 'soap' => NS[:soap]
|
89
|
+
).to_s.split(':').last
|
90
|
+
|
91
|
+
Hash[
|
92
|
+
doc.xpath("/d:definitions/d:binding[@name='#{binding}']/d:operation", 'd' => NS[:wsdl]).map do |op|
|
93
|
+
name = op.attribute('name').to_s
|
94
|
+
action = op.at_xpath('./soap:operation/@soapAction', 'soap' => NS[:soap]).to_s
|
95
|
+
|
96
|
+
[
|
97
|
+
name,
|
98
|
+
{
|
99
|
+
:name => name,
|
100
|
+
:action => action,
|
101
|
+
:input => port_type_operations[name][:input],
|
102
|
+
:output => port_type_operations[name][:output]
|
103
|
+
}
|
104
|
+
]
|
105
|
+
end
|
106
|
+
]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def element_hash(el)
|
113
|
+
max_occurs = el.attribute('maxOccurs').to_s
|
114
|
+
{
|
115
|
+
:name => el.attribute('name').to_s,
|
116
|
+
:type => el.attribute('type').to_s,
|
117
|
+
:singular => max_occurs.empty? || max_occurs == '1'
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|