lolsoap 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|