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.
Files changed (47) hide show
  1. data/.document +5 -0
  2. data/.travis.yml +7 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +22 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.md +124 -0
  8. data/Rakefile +29 -0
  9. data/VERSION +1 -0
  10. data/lib/lolsoap.rb +11 -0
  11. data/lib/lolsoap/builder.rb +93 -0
  12. data/lib/lolsoap/client.rb +25 -0
  13. data/lib/lolsoap/envelope.rb +94 -0
  14. data/lib/lolsoap/errors.rb +15 -0
  15. data/lib/lolsoap/fault.rb +26 -0
  16. data/lib/lolsoap/hash_builder.rb +48 -0
  17. data/lib/lolsoap/request.rb +54 -0
  18. data/lib/lolsoap/response.rb +50 -0
  19. data/lib/lolsoap/wsdl.rb +98 -0
  20. data/lib/lolsoap/wsdl/element.rb +28 -0
  21. data/lib/lolsoap/wsdl/null_element.rb +15 -0
  22. data/lib/lolsoap/wsdl/null_type.rb +19 -0
  23. data/lib/lolsoap/wsdl/operation.rb +18 -0
  24. data/lib/lolsoap/wsdl/type.rb +38 -0
  25. data/lib/lolsoap/wsdl_parser.rb +121 -0
  26. data/lolsoap.gemspec +97 -0
  27. data/test/fixtures/stock_quote.wsdl +74 -0
  28. data/test/fixtures/stock_quote_fault.xml +16 -0
  29. data/test/fixtures/stock_quote_response.xml +8 -0
  30. data/test/helper.rb +14 -0
  31. data/test/integration/test_client.rb +20 -0
  32. data/test/integration/test_envelope.rb +45 -0
  33. data/test/integration/test_request.rb +19 -0
  34. data/test/integration/test_response.rb +15 -0
  35. data/test/integration/test_wsdl.rb +28 -0
  36. data/test/unit/test_builder.rb +95 -0
  37. data/test/unit/test_client.rb +12 -0
  38. data/test/unit/test_envelope.rb +112 -0
  39. data/test/unit/test_fault.rb +33 -0
  40. data/test/unit/test_hash_builder.rb +127 -0
  41. data/test/unit/test_request.rb +48 -0
  42. data/test/unit/test_response.rb +39 -0
  43. data/test/unit/test_wsdl.rb +143 -0
  44. data/test/unit/test_wsdl_parser.rb +105 -0
  45. data/test/unit/wsdl/test_element.rb +31 -0
  46. data/test/unit/wsdl/test_type.rb +44 -0
  47. metadata +152 -0
@@ -0,0 +1,15 @@
1
+ module LolSoap
2
+ class Error < StandardError; end
3
+
4
+ class FaultRaised < Error
5
+ attr_reader :fault
6
+
7
+ def initialize(fault)
8
+ @fault = fault
9
+ end
10
+
11
+ def message
12
+ "#{fault.reason}\n#{fault.detail}"
13
+ end
14
+ end
15
+ end
@@ -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
@@ -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,15 @@
1
+ class LolSoap::WSDL
2
+ class NullElement
3
+ def type
4
+ NullType.new
5
+ end
6
+
7
+ def singular?
8
+ true
9
+ end
10
+
11
+ def ==(other)
12
+ self.class === other
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ class LolSoap::WSDL
2
+ class NullType
3
+ def prefix
4
+ nil
5
+ end
6
+
7
+ def elements
8
+ {}
9
+ end
10
+
11
+ def element(name)
12
+ NullType.new
13
+ end
14
+
15
+ def ==(other)
16
+ self.class === other
17
+ end
18
+ end
19
+ 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