lolsoap 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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