wasabi 5.0.3 → 5.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa7b2c3c621da06cf8c445204da8b3573e10bd72887524480787a447ea8d0433
4
- data.tar.gz: b74bff5bdd7ad553ab8e53ab1466e612b92485a098ab517d7c2b2bafcbcef4c7
3
+ metadata.gz: fafbc0e7f78682f50c3f1466447f9d724e9cc00fc1dd6b8d7751c6f473c1f297
4
+ data.tar.gz: 193032ab7fcc93c7cd4f71bd69ca09e545ce5ede2167efa975fde11ce9c7ceaf
5
5
  SHA512:
6
- metadata.gz: da865a6c0b63f62ba38282d19d0d3541afcbb122a9aff9a41574587e63a382911452e8abfc15c9a327c4697fe3cafe056676c3ade9a1cb040787e5568ba15ec0
7
- data.tar.gz: 1e2e5f1932252056d5e65b481f58ea85e9c17d4391bb8ee405de844da8fa391963f56bfa816dfc7b6f6f6a5a66930ca47c0c821e12d274a5398cddd7ec777fe3
6
+ metadata.gz: 992ad19eb27a2e31594698517147b412dc05f9aab8825009325835276fbb97a1cffac5abc10d2fea3f103a9a4cfe8ed86e56df6806e3a45b8499a0ec581414e2
7
+ data.tar.gz: bd5a22e85ebbcc5273c276dd887268edf6c6a94387663f6f12d48c00e72646e2ea680f7a3a80a363635c7f5d70f2b4a033d8939c2474d8d2b67dc8bef209adee
data/CHANGELOG.md CHANGED
@@ -1,8 +1,14 @@
1
1
  # CHANGELOG for Wasabi
2
2
 
3
- ## Unreleased
3
+ ## 5.2.0 (2026-09-18)
4
4
 
5
- - _Your new line here. Mind the style of prefix used in the rest of the document._
5
+ - Resolve each operation to its WSDL port's SOAP endpoint via `Wasabi::ServicePorts` and `Document#endpoint_for_operation` ([#138](https://github.com/savonrb/wasabi/pull/138)). `Document#endpoint` keeps its legacy first-port behavior. Known limitation: the lookup walks operation -> portType -> binding -> port and takes the first matching portType, while Savon's operation map is built from bindings (last binding wins). If the same operation name appears in two portTypes, the SOAP action may come from a different binding than the endpoint's port; a future pass could walk operation -> binding -> port for consistency.
6
+ - Parse operation faults and expose the raw operation name in the operation hash ([#121](https://github.com/savonrb/wasabi/pull/121)) by @ekzobrain.
7
+
8
+ ## 5.1.0 (2024-10-27)
9
+
10
+ - Segment declared types by namespace to distinguish distinct types by @RickSnyder in https://github.com/savonrb/wasabi/pull/57
11
+ - Adjust input parsing by @pcai in https://github.com/savonrb/wasabi/pull/122
6
12
 
7
13
  ## 5.0.3 (2024-07-20)
8
14
 
data/README.md CHANGED
@@ -2,10 +2,8 @@
2
2
 
3
3
  A simple WSDL parser.
4
4
 
5
- [![Test](https://github.com/savonrb/wasabi/actions/workflows/test.yml/badge.svg)](https://github.com/savonrb/wasabi/actions/workflows/test.yml)
5
+ [![CI](https://github.com/savonrb/wasabi/actions/workflows/ci.yml/badge.svg)](https://github.com/savonrb/wasabi/actions/workflows/ci.yml)
6
6
  [![Gem Version](https://badge.fury.io/rb/wasabi.svg)](http://badge.fury.io/rb/wasabi)
7
- [![Code Climate](https://codeclimate.com/github/savonrb/wasabi.svg)](https://codeclimate.com/github/savonrb/wasabi)
8
- [![Coverage Status](https://coveralls.io/repos/savonrb/wasabi/badge.svg?branch=master)](https://coveralls.io/r/savonrb/wasabi)
9
7
 
10
8
  ## Installation
11
9
 
@@ -63,3 +61,8 @@ document.operations
63
61
  # => { :create_user => { :input => "createUser", :action => "createUser" },
64
62
  # => :find_user => { :input => "findUser", :action => "findUser" } }
65
63
  ```
64
+
65
+ ## Upgrading to Wasabi 5.x
66
+
67
+ Wasabi 5.x adds support for (and defaults to) [Faraday](https://lostisland.github.io/faraday/#/) for its HTTP transport. For compatibility, it will continue to prefer
68
+ [HTTPI](https://github.com/savonrb/httpi) if it is present. Newer versions of Savon require Wasabi 5.x as part of the transition to Faraday.
@@ -3,10 +3,9 @@
3
3
  module Wasabi
4
4
  module CoreExt
5
5
  module String
6
-
7
6
  def self.snakecase(str)
8
7
  str = str.dup
9
- str.gsub!(/::/, '/')
8
+ str.gsub!("::", "/")
10
9
  str.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
11
10
  str.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
12
11
  str.tr!(".", "_")
@@ -14,7 +13,6 @@ module Wasabi
14
13
  str.downcase!
15
14
  str
16
15
  end
17
-
18
16
  end
19
17
  end
20
18
  end
@@ -5,12 +5,10 @@ require "wasabi/resolver"
5
5
  require "wasabi/parser"
6
6
 
7
7
  module Wasabi
8
-
9
8
  # = Wasabi::Document
10
9
  #
11
10
  # Represents a WSDL document.
12
11
  class Document
13
-
14
12
  ELEMENT_FORM_DEFAULTS = [:unqualified, :qualified]
15
13
 
16
14
  # Validates if a given +value+ is a valid elementFormDefault value.
@@ -18,14 +16,14 @@ module Wasabi
18
16
  def self.validate_element_form_default!(value)
19
17
  return if ELEMENT_FORM_DEFAULTS.include?(value)
20
18
 
21
- raise ArgumentError, "Invalid value for elementFormDefault: #{value}\n" +
22
- "Must be one of: #{ELEMENT_FORM_DEFAULTS.inspect}"
19
+ raise ArgumentError, "Invalid value for elementFormDefault: #{value}\n" \
20
+ "Must be one of: #{ELEMENT_FORM_DEFAULTS.inspect}"
23
21
  end
24
22
 
25
23
  # Accepts a WSDL +document+ to parse.
26
24
  def initialize(document = nil, adapter = nil)
27
25
  self.document = document
28
- self.adapter = adapter
26
+ self.adapter = adapter
29
27
  end
30
28
 
31
29
  attr_accessor :document, :request, :adapter
@@ -42,6 +40,19 @@ module Wasabi
42
40
  # Sets the SOAP endpoint.
43
41
  attr_writer :endpoint
44
42
 
43
+ # Returns the SOAP endpoint of the port serving the given SOAP operation.
44
+ #
45
+ # Walks operation -> portType -> binding -> port, so WSDLs exposing
46
+ # several ports resolve to the address of the port that actually serves
47
+ # the operation instead of the first port's address. When +soap_version+
48
+ # is given, a port for that SOAP version is preferred; otherwise the
49
+ # first SOAP port for the operation is returned. Non-SOAP ports are
50
+ # never returned. Returns +nil+ when the operation has no SOAP ports,
51
+ # so callers can fall back to #endpoint.
52
+ def endpoint_for_operation(operation_name, soap_version: nil)
53
+ service_ports.endpoint_for_operation(operation_name, soap_version: soap_version)
54
+ end
55
+
45
56
  # Returns the target namespace.
46
57
  def namespace
47
58
  @namespace ||= parser.namespace
@@ -91,7 +102,7 @@ module Wasabi
91
102
  # Returns a list of parameter names for a given +key+
92
103
  def soap_action_parameters(key)
93
104
  params = operation_input_parameters(key)
94
- params.keys if params
105
+ params&.keys
95
106
  end
96
107
 
97
108
  # Returns a list of input parameters for a given +key+.
@@ -103,13 +114,17 @@ module Wasabi
103
114
  @type_namespaces ||= begin
104
115
  namespaces = []
105
116
 
106
- parser.types.each do |type, info|
107
- namespaces << [[type], info[:namespace]]
117
+ if document
118
+ parser.types.each do |ns, types|
119
+ types.each do |type, info|
120
+ namespaces << [[type], info[:namespace]]
108
121
 
109
- element_keys(info).each do |field|
110
- namespaces << [[type, field], info[:namespace]]
122
+ element_keys(info).each do |field|
123
+ namespaces << [[type, field], info[:namespace]]
124
+ end
125
+ end
111
126
  end
112
- end if document
127
+ end
113
128
 
114
129
  namespaces
115
130
  end
@@ -119,14 +134,18 @@ module Wasabi
119
134
  @type_definitions ||= begin
120
135
  result = []
121
136
 
122
- parser.types.each do |type, info|
123
- element_keys(info).each do |field|
124
- field_type = info[field][:type]
125
- tag, namespace = field_type.split(":").reverse
137
+ if document
138
+ parser.types.each do |ns, types|
139
+ types.each do |type, info|
140
+ element_keys(info).each do |field|
141
+ field_type = info[field][:type]
142
+ tag, namespace = field_type.split(":").reverse
126
143
 
127
- result << [[type, field], tag] if user_defined(namespace)
144
+ result << [[type, field], tag] if user_defined(namespace)
145
+ end
146
+ end
128
147
  end
129
- end if document
148
+ end
130
149
 
131
150
  result
132
151
  end
@@ -149,7 +168,12 @@ module Wasabi
149
168
  @parser ||= guard_parse && parse
150
169
  end
151
170
 
152
- private
171
+ private
172
+
173
+ # Returns the service ports of the WSDL document.
174
+ def service_ports
175
+ @service_ports ||= ServicePorts.new(parser.document)
176
+ end
153
177
 
154
178
  # Raises an error if the WSDL document is missing.
155
179
  def guard_parse
data/lib/wasabi/parser.rb CHANGED
@@ -1,26 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'uri'
4
- require 'addressable/uri'
5
- require 'wasabi/core_ext/string'
3
+ require "uri"
4
+ require "addressable/uri"
5
+ require "wasabi/core_ext/string"
6
6
 
7
7
  module Wasabi
8
-
9
8
  # = Wasabi::Parser
10
9
  #
11
10
  # Parses WSDL documents and remembers their important parts.
12
11
  class Parser
12
+ XSD = "http://www.w3.org/2001/XMLSchema"
13
+ WSDL = "http://schemas.xmlsoap.org/wsdl/"
14
+ SOAP_1_1 = "http://schemas.xmlsoap.org/wsdl/soap/"
15
+ SOAP_1_2 = "http://schemas.xmlsoap.org/wsdl/soap12/"
13
16
 
14
- XSD = 'http://www.w3.org/2001/XMLSchema'
15
- WSDL = 'http://schemas.xmlsoap.org/wsdl/'
16
- SOAP_1_1 = 'http://schemas.xmlsoap.org/wsdl/soap/'
17
- SOAP_1_2 = 'http://schemas.xmlsoap.org/wsdl/soap12/'
17
+ # A wsdl message an operation input, output, or fault resolves to.
18
+ # Contains the message type name and its namespace identifier.
19
+ MessageRef = Struct.new(:namespace_id, :type)
18
20
 
19
21
  def initialize(document)
20
22
  self.document = document
21
23
  self.operations = {}
22
24
  self.namespaces = {}
23
- self.service_name = ''
25
+ self.service_name = ""
24
26
  self.types = {}
25
27
  self.deferred_types = []
26
28
  self.element_form_default = :unqualified
@@ -67,22 +69,21 @@ module Wasabi
67
69
  end
68
70
 
69
71
  def parse_namespaces
70
- element_form_default = schemas.first && schemas.first['elementFormDefault']
72
+ element_form_default = schemas.first && schemas.first["elementFormDefault"]
71
73
  @element_form_default = element_form_default.to_s.to_sym if element_form_default
72
74
 
73
- namespace = document.root['targetNamespace']
75
+ namespace = document.root["targetNamespace"]
74
76
  @namespace = namespace.to_s if namespace
75
77
 
76
- @namespaces = @document.namespaces.inject({}) do |memo, (key, value)|
77
- memo[key.sub('xmlns:', '')] = value
78
- memo
78
+ @namespaces = @document.namespaces.each_with_object({}) do |(key, value), memo|
79
+ memo[key.sub("xmlns:", "")] = value
79
80
  end
80
81
  end
81
82
 
82
83
  def parse_endpoint
83
- if service_node = service
84
- endpoint = service_node.at_xpath('.//soap11:address/@location', 'soap11' => SOAP_1_1)
85
- endpoint ||= service_node.at_xpath(service_node, './/soap12:address/@location', 'soap12' => SOAP_1_2)
84
+ if (service_node = service)
85
+ endpoint = service_node.at_xpath(".//soap11:address/@location", "soap11" => SOAP_1_1)
86
+ endpoint ||= service_node.at_xpath(service_node, ".//soap12:address/@location", "soap12" => SOAP_1_2)
86
87
  end
87
88
 
88
89
  @endpoint = parse_url(endpoint) if endpoint
@@ -96,129 +97,139 @@ module Wasabi
96
97
  end
97
98
 
98
99
  def parse_service_name
99
- service_name = document.root['name']
100
+ service_name = document.root["name"]
100
101
  @service_name = service_name.to_s if service_name
101
102
  end
102
103
 
103
104
  def parse_messages
104
- messages = document.root.element_children.select { |node| node.name == 'message' }
105
- @messages = Hash[messages.map { |node| [node['name'], node] }]
105
+ messages = document.root.element_children.select { |node| node.name == "message" }
106
+ @messages = messages.map { |node| [node["name"], node] }.to_h
106
107
  end
107
108
 
108
109
  def parse_port_types
109
- port_types = document.root.element_children.select { |node| node.name == 'portType' }
110
- @port_types = Hash[port_types.map { |node| [node['name'], node] }]
110
+ port_types = document.root.element_children.select { |node| node.name == "portType" }
111
+ @port_types = port_types.map { |node| [node["name"], node] }.to_h
111
112
  end
112
113
 
113
114
  def parse_port_type_operations
114
115
  @port_type_operations = {}
115
116
 
116
117
  @port_types.each do |port_type_name, port_type|
117
- operations = port_type.element_children.select { |node| node.name == 'operation' }
118
- @port_type_operations[port_type_name] = Hash[operations.map { |node| [node['name'], node] }]
118
+ operations = port_type.element_children.select { |node| node.name == "operation" }
119
+ @port_type_operations[port_type_name] = operations.map { |node| [node["name"], node] }.to_h
119
120
  end
120
121
  end
121
122
 
122
123
  def parse_operations_parameters
123
- document.xpath("wsdl:definitions/wsdl:types/*[local-name()='schema']/*[local-name()='element']", 'wsdl' => WSDL).each do |element|
124
- name = Wasabi::CoreExt::String.snakecase(element.attribute('name').to_s).to_sym
124
+ document.xpath("wsdl:definitions/wsdl:types/*[local-name()='schema']/*[local-name()='element']", "wsdl" => WSDL).each do |element|
125
+ name = Wasabi::CoreExt::String.snakecase(element.attribute("name").to_s).to_sym
125
126
 
126
- if operation = @operations[name]
127
+ if (operation = @operations[name])
127
128
  element.xpath("*[local-name() ='complexType']/*[local-name() ='sequence']/*[local-name() ='element']").each do |child_element|
128
- attr_name = child_element.attribute('name').to_s
129
- attr_type = (attr_type = child_element.attribute('type').to_s.split(':')).size > 1 ? attr_type[1] : attr_type[0]
129
+ attr_name = child_element.attribute("name").to_s
130
+ attr_type = ((attr_type = child_element.attribute("type").to_s.split(":")).size > 1) ? attr_type[1] : attr_type[0]
130
131
 
131
132
  operation[:parameters] ||= {}
132
- operation[:parameters][attr_name.to_sym] = { :name => attr_name, :type => attr_type }
133
+ operation[:parameters][attr_name.to_sym] = {name: attr_name, type: attr_type}
133
134
  end
134
135
  end
135
136
  end
136
137
  end
137
138
 
138
139
  def parse_operations
139
- operations = document.xpath('wsdl:definitions/wsdl:binding/wsdl:operation', 'wsdl' => WSDL)
140
+ operations = document.xpath("wsdl:definitions/wsdl:binding/wsdl:operation", "wsdl" => WSDL)
140
141
  operations.each do |operation|
141
- name = operation.attribute('name').to_s
142
+ name = operation.attribute("name").to_s
142
143
  snakecase_name = Wasabi::CoreExt::String.snakecase(name).to_sym
143
144
 
144
145
  # TODO: check for soap namespace?
145
- soap_operation = operation.element_children.find { |node| node.name == 'operation' }
146
- soap_action = soap_operation['soapAction'] if soap_operation
147
- soap_document = soap_operation['style'] == 'document' if soap_operation
146
+ soap_operation = operation.element_children.find { |node| node.name == "operation" }
147
+ soap_action = soap_operation["soapAction"] if soap_operation
148
+ soap_document = soap_operation["style"] == "document" if soap_operation
148
149
 
149
150
  if soap_action || soap_document
150
151
  soap_action = soap_action.to_s
151
- action = soap_action && !soap_action.empty? ? soap_action : name
152
+ action = (soap_action && !soap_action.empty?) ? soap_action : name
152
153
 
153
154
  # There should be a matching portType for each binding, so we will lookup the input from there.
154
- namespace_id, output = output_for(operation)
155
- namespace_id, input = input_for(operation)
155
+ input = input_for(operation)
156
+ output = output_for(operation)
157
+ faults = faults_for(operation)
156
158
 
157
159
  # Store namespace identifier so this operation can be mapped to the proper namespace.
158
- @operations[snakecase_name] = { :action => action, :input => input, :output => output, :namespace_identifier => namespace_id}
160
+ operation_entry = {
161
+ name: name,
162
+ action: action,
163
+ input: input.type,
164
+ output: output.type,
165
+ namespace_identifier: input.namespace_id
166
+ }
167
+ operation_entry[:fault] = faults.map(&:type) if faults.any?
168
+ @operations[snakecase_name] = operation_entry
159
169
  elsif !@operations[snakecase_name]
160
- @operations[snakecase_name] = { :action => name, :input => name }
170
+ @operations[snakecase_name] = {action: name, input: name}
161
171
  end
162
172
  end
163
173
  end
164
174
 
165
175
  def parse_types
166
176
  schemas.each do |schema|
167
- schema_namespace = schema['targetNamespace']
177
+ schema_namespace = schema["targetNamespace"]
168
178
 
169
179
  schema.element_children.each do |node|
170
180
  namespace = schema_namespace || @namespace
171
181
 
172
182
  case node.name
173
- when 'element'
174
- complex_type = node.at_xpath('./xs:complexType', 'xs' => XSD)
175
- process_type namespace, complex_type, node['name'].to_s if complex_type
176
- when 'complexType'
177
- process_type namespace, node, node['name'].to_s
183
+ when "element"
184
+ complex_type = node.at_xpath("./xs:complexType", "xs" => XSD)
185
+ process_type namespace, complex_type, node["name"].to_s if complex_type
186
+ when "complexType"
187
+ process_type namespace, node, node["name"].to_s
178
188
  end
179
189
  end
180
190
  end
181
191
  end
182
192
 
183
193
  def process_type(namespace, type, name)
184
- @types[name] ||= { :namespace => namespace }
185
- @types[name][:order!] = []
194
+ @types[namespace] ||= {}
195
+ @types[namespace][name] ||= {namespace: namespace}
196
+ @types[namespace][name][:order!] = []
186
197
 
187
- type.xpath('./xs:sequence/xs:element', 'xs' => XSD).each do |inner|
188
- element_name = inner.attribute('name').to_s
189
- @types[name][element_name] = { :type => inner.attribute('type').to_s }
198
+ type.xpath("./xs:sequence/xs:element", "xs" => XSD).each do |inner|
199
+ element_name = inner.attribute("name").to_s
200
+ @types[namespace][name][element_name] = {type: inner.attribute("type").to_s}
190
201
 
191
- [ :nillable, :minOccurs, :maxOccurs ].each do |attr|
192
- if v = inner.attribute(attr.to_s)
193
- @types[name][element_name][attr] = v.to_s
202
+ [:nillable, :minOccurs, :maxOccurs].each do |attr|
203
+ if (v = inner.attribute(attr.to_s))
204
+ @types[namespace][name][element_name][attr] = v.to_s
194
205
  end
195
206
  end
196
207
 
197
- @types[name][:order!] << element_name
208
+ @types[namespace][name][:order!] << element_name
198
209
  end
199
210
 
200
- type.xpath('./xs:complexContent/xs:extension/xs:sequence/xs:element', 'xs' => XSD).each do |inner_element|
201
- element_name = inner_element.attribute('name').to_s
202
- @types[name][element_name] = { :type => inner_element.attribute('type').to_s }
211
+ type.xpath("./xs:complexContent/xs:extension/xs:sequence/xs:element", "xs" => XSD).each do |inner_element|
212
+ element_name = inner_element.attribute("name").to_s
213
+ @types[namespace][name][element_name] = {type: inner_element.attribute("type").to_s}
203
214
 
204
- @types[name][:order!] << element_name
215
+ @types[namespace][name][:order!] << element_name
205
216
  end
206
217
 
207
- type.xpath('./xs:complexContent/xs:extension[@base]', 'xs' => XSD).each do |inherits|
208
- base = inherits.attribute('base').value.match(/\w+$/).to_s
218
+ type.xpath("./xs:complexContent/xs:extension[@base]", "xs" => XSD).each do |inherits|
219
+ base = inherits.attribute("base").value.match(/\w+$/).to_s
209
220
 
210
- if @types[base]
221
+ if @types[namespace][base]
211
222
  # Reverse merge because we don't want subclass attributes to be overriden by base class
212
- @types[name] = types[base].merge(types[name])
213
- @types[name][:order!] = @types[base][:order!] | @types[name][:order!]
214
- @types[name][:base_type] = base
223
+ @types[namespace][name] = types[namespace][base].merge(types[namespace][name])
224
+ @types[namespace][name][:order!] = @types[namespace][base][:order!] | @types[namespace][name][:order!]
225
+ @types[namespace][name][:base_type] = base
215
226
  else
216
- p = Proc.new do
217
- if @types[base]
227
+ p = proc do
228
+ if @types[namespace][base]
218
229
  # Reverse merge because we don't want subclass attributes to be overriden by base class
219
- @types[name] = @types[base].merge(@types[name])
220
- @types[name][:order!] = @types[base][:order!] | @types[name][:order!]
221
- @types[name][:base_type] = base
230
+ @types[namespace][name] = @types[namespace][base].merge(@types[namespace][name])
231
+ @types[namespace][name][:order!] = @types[namespace][base][:order!] | @types[namespace][name][:order!]
232
+ @types[namespace][name][:base_type] = base
222
233
  end
223
234
  end
224
235
  deferred_types << p
@@ -230,85 +241,112 @@ module Wasabi
230
241
  deferred_types.each(&:call)
231
242
  end
232
243
 
244
+ # wasabi reports a single input and a single output per operation.
245
+ # When the portType or its message cannot be resolved or doesn't exist
246
+ # like with a one-way operation, it falls back to the operation name.
233
247
  def input_for(operation)
234
- input_output_for(operation, 'input')
248
+ resolve_messages(operation, "input").first || MessageRef.new(nil, operation["name"])
235
249
  end
236
250
 
237
251
  def output_for(operation)
238
- input_output_for(operation, 'output')
252
+ resolve_messages(operation, "output").first || MessageRef.new(nil, operation["name"])
239
253
  end
240
254
 
241
- def input_output_for(operation, input_output)
242
- operation_name = operation['name']
243
-
244
- # Look up the input by walking up to portType, then up to the message.
255
+ # An operation may declare any number of faults, or none at all.
256
+ def faults_for(operation)
257
+ resolve_messages(operation, "fault")
258
+ end
245
259
 
246
- binding_type = operation.parent['type'].to_s.split(':').last
247
- if @port_type_operations[binding_type]
248
- port_type_operation = @port_type_operations[binding_type][operation_name]
249
- end
260
+ # Resolves the wsdl messages an operation's input, output, or fault elements
261
+ # point at, by walking up to the portType and down into the messages. Returns
262
+ # one MessageRef per matching element, so none when there are no matches.
263
+ #
264
+ # @return [Array<MessageRef>]
265
+ def resolve_messages(operation, element_name)
266
+ port_type_operation = port_type_operation_for(operation)
250
267
 
251
- port_type_input_output = port_type_operation&.element_children&.find { |node| node.name == input_output }
268
+ # Sometimes portTypes are actually included in a separate WSDL,
269
+ # so the portType operation cannot be resolved here.
270
+ port_type_elements = port_type_operation&.element_children&.select { |node| node.name == element_name } || []
252
271
 
253
- # TODO: Stupid fix for missing support for imports.
254
- # Sometimes portTypes are actually included in a separate WSDL.
255
- if port_type_input_output
256
- if port_type_input_output.attribute('message').to_s.include? ':'
257
- port_message_ns_id, port_message_type = port_type_input_output.attribute('message').to_s.split(':')
258
- else
259
- port_message_type = port_type_input_output.attribute('message').to_s
260
- end
261
-
262
- message_ns_id, message_type = nil
272
+ port_type_elements.each_with_index.map do |port_type_element, index|
273
+ message_ref_for(operation, port_type_element, element_name, index)
274
+ end
275
+ end
263
276
 
264
- soap_operation = operation.element_children.find { |node| node.name == 'operation' }
277
+ # Looks up the portType operation matching a binding operation,
278
+ # walking up to the portType via the binding's type attribute.
279
+ def port_type_operation_for(operation)
280
+ binding_type = operation.parent["type"].to_s.split(":").last
281
+ @port_type_operations.dig(binding_type, operation["name"])
282
+ end
265
283
 
266
- if soap_operation.nil? || soap_operation['style'] != 'rpc'
267
- message_ns_id = port_message_ns_id
268
- message_type = port_message_type
269
- end
284
+ # Resolves a single portType input/output/fault element to the message it
285
+ # points at, walking portType -> message -> part -> element. Falls back to
286
+ # the operation name when the message cannot be resolved.
287
+ #
288
+ # @return [MessageRef]
289
+ def message_ref_for(operation, port_type_element, element_name, index)
290
+ operation_name = operation["name"]
291
+
292
+ # If the message attribute contains a colon, the message is namespaced.
293
+ parts = port_type_element.attribute("message").to_s.split(":", 2)
294
+ port_message_ns_id, port_message_type = (parts.size == 2) ? parts : [nil, *parts]
295
+
296
+ message_ns_id = nil
297
+ message_type = nil
298
+
299
+ # When there is a parts attribute in the soap:body element, use that value
300
+ # to look up the message part from the messages array.
301
+ input_output_element = operation.element_children.select { |node| node.name == element_name }.at(index)
302
+ if input_output_element
303
+ soap_body_element = input_output_element.element_children.find { |node| node.name == "body" }
304
+ soap_body_parts = soap_body_element["parts"] if soap_body_element
305
+ end
270
306
 
271
- # When there is a parts attribute in soap:body element, we should use that value
272
- # to look up the message part from messages array.
273
- input_output_element = operation.element_children.find { |node| node.name == input_output }
274
- if input_output_element
275
- soap_body_element = input_output_element.element_children.find { |node| node.name == 'body' }
276
- soap_body_parts = soap_body_element['parts'] if soap_body_element
277
- end
307
+ # look for any message part that matches the soap body parts
308
+ message = @messages[port_message_type]
309
+ port_message_part = message&.element_children&.find do |node|
310
+ soap_body_parts.nil? ? (node.name == "part") : (node.name == "part" && node["name"] == soap_body_parts)
311
+ end
278
312
 
279
- message = @messages[port_message_type]
280
- port_message_part = message&.element_children&.find do |node|
281
- soap_body_parts.nil? ? (node.name == "part") : (node.name == "part" && node["name"] == soap_body_parts)
313
+ if port_message_part && (port_element = port_message_part.attribute("element"))
314
+ port_message_part = port_element.to_s
315
+ if port_message_part.include?(":")
316
+ message_ns_id, message_type = port_message_part.split(":")
317
+ else
318
+ message_type = port_message_part
282
319
  end
320
+ end
283
321
 
284
- if port_message_part && port_element = port_message_part.attribute('element')
285
- port_message_part = port_element.to_s
286
- if port_message_part.include?(':')
287
- message_ns_id, message_type = port_message_part.split(':')
288
- else
289
- message_type = port_message_part
290
- end
322
+ # If the message is not found, we should use the operation name as the message name
323
+ # for document style operations applies only to output
324
+ if element_name == "output"
325
+ # if the operation is document style and theres no port_message_part, we should
326
+ # use the operation_name
327
+ soap_operation = operation.element_children.find { |node| node.name == "operation" }
328
+ if message_type.nil? && (soap_operation.nil? || soap_operation["style"] != "rpc")
329
+ message_ns_id = port_message_ns_id
330
+ message_type = port_message_part.nil? ? operation_name : port_message_type
291
331
  end
332
+ end
292
333
 
293
- # Fall back to the name of the binding operation
294
- if message_type
295
- [message_ns_id, message_type]
296
- else
297
- [port_message_ns_id, operation_name]
298
- end
334
+ # Fall back to the name of the binding operation
335
+ if message_type
336
+ MessageRef.new(message_ns_id, message_type)
299
337
  else
300
- [nil, operation_name]
338
+ MessageRef.new(port_message_ns_id, operation_name)
301
339
  end
302
340
  end
303
341
 
304
342
  def schemas
305
- types = section('types').first
343
+ types = section("types").first
306
344
  types ? types.element_children : []
307
345
  end
308
346
 
309
347
  def service
310
- services = section('service')
311
- services.first if services # service nodes could be imported?
348
+ services = section("service")
349
+ services&.first # service nodes could be imported?
312
350
  end
313
351
 
314
352
  def section(section_name)
@@ -3,27 +3,25 @@
3
3
  require "faraday"
4
4
 
5
5
  module Wasabi
6
-
7
6
  # = Wasabi::Resolver
8
7
  #
9
8
  # Resolves local and remote WSDL documents.
10
9
  class Resolver
11
-
12
10
  class HTTPError < StandardError
13
- def initialize(message, response=nil)
11
+ def initialize(message, response = nil)
14
12
  super(message)
15
13
  @response = response
16
14
  end
17
15
  attr_reader :response
18
16
  end
19
17
 
20
- URL = /^http[s]?:/
18
+ URL = /^https?:/
21
19
  XML = /^</
22
20
 
23
21
  def initialize(document, request = nil, adapter = nil)
24
22
  @document = document
25
- @request = request || Faraday.new
26
- @adapter = adapter
23
+ @request = request || Faraday.new
24
+ @adapter = adapter
27
25
  end
28
26
 
29
27
  attr_reader :document, :request, :adapter
@@ -32,9 +30,9 @@ module Wasabi
32
30
  raise ArgumentError, "Unable to resolve: #{document.inspect}" unless document
33
31
 
34
32
  case document
35
- when URL then load_from_remote
36
- when XML then document
37
- else load_from_disc
33
+ when URL then load_from_remote
34
+ when XML then document
35
+ else load_from_disc
38
36
  end
39
37
  end
40
38
 
@@ -50,7 +48,7 @@ module Wasabi
50
48
 
51
49
  raise HTTPError.new("Error: #{response.code} for url #{request.url}", response) if response.error?
52
50
  else
53
- request.adapter *adapter if adapter
51
+ request.adapter(*adapter) if adapter
54
52
  response = request.get(document)
55
53
 
56
54
  raise HTTPError.new("Error: #{response.status} for url #{document}", response) unless response.success?
@@ -62,6 +60,5 @@ module Wasabi
62
60
  def load_from_disc
63
61
  File.read(document)
64
62
  end
65
-
66
63
  end
67
64
  end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "addressable/uri"
5
+ require "wasabi/core_ext/string"
6
+
7
+ module Wasabi
8
+ # = Wasabi::ServicePorts
9
+ #
10
+ # Parses the +wsdl:service+ ports and +wsdl:binding+ elements of a WSDL
11
+ # document, so callers can resolve the SOAP endpoint of the port that
12
+ # serves a given operation instead of always using the first port's
13
+ # address.
14
+ class ServicePorts
15
+ SOAP_1_1 = "http://schemas.xmlsoap.org/wsdl/soap/"
16
+ SOAP_1_2 = "http://schemas.xmlsoap.org/wsdl/soap12/"
17
+
18
+ def initialize(document)
19
+ @document = document
20
+ end
21
+
22
+ # Returns the parsed service ports as an Array of Hashes with +:name+,
23
+ # +:binding+ (local binding name), +:address+ (URI) and +:soap_version+
24
+ # (1, 2 or nil when the address is not a SOAP address) keys, in
25
+ # document order.
26
+ def ports
27
+ @ports ||= parse_ports
28
+ end
29
+
30
+ # Returns the SOAP endpoint (address) of the port serving the given
31
+ # SOAP operation.
32
+ #
33
+ # Walks operation -> portType -> binding -> port, so WSDLs exposing
34
+ # several ports resolve to the address of the port that actually serves
35
+ # the operation instead of the first port's address. When +soap_version+
36
+ # is given, a port for that SOAP version is preferred, but the first
37
+ # SOAP port for the operation is returned when no port matches the
38
+ # requested version — routing degrades to the operation's best
39
+ # available SOAP port instead of silently falling back to the WSDL's
40
+ # first port. Non-SOAP ports (e.g. +http:address+) are never returned.
41
+ # Returns +nil+ when the operation has no SOAP ports at all, so
42
+ # callers can fall back to the default endpoint.
43
+ def endpoint_for_operation(operation_name, soap_version: nil)
44
+ port = preferred_port(operation_name, soap_version)
45
+ port && port[:address]
46
+ end
47
+
48
+ private
49
+
50
+ # Returns the preferred SOAP port serving the given SOAP operation.
51
+ # Ports for the requested SOAP version win; otherwise the first SOAP
52
+ # port in document order. Returns nil when the operation has no SOAP
53
+ # ports at all.
54
+ def preferred_port(operation_name, soap_version)
55
+ candidates = soap_ports_for_operation(operation_name)
56
+
57
+ candidates.find { |port| port[:soap_version] == soap_version } || candidates.first
58
+ end
59
+
60
+ # Returns the SOAP ports serving the given SOAP operation, in document
61
+ # order. Non-SOAP ports (e.g. +http:address+) and ports with
62
+ # unparsable addresses never qualify as routing candidates.
63
+ def soap_ports_for_operation(operation_name)
64
+ port_type = port_type_for_operation(operation_name)
65
+ return [] unless port_type
66
+
67
+ bindings = parse_bindings
68
+
69
+ ports.select { |port|
70
+ !port[:soap_version].nil? && !port[:address].nil? &&
71
+ bindings[port[:binding]] == port_type
72
+ }
73
+ end
74
+
75
+ # Returns the name of the portType defining the given operation.
76
+ # Operation names are compared snake-cased, like the keys of
77
+ # Wasabi::Parser#operations.
78
+ def port_type_for_operation(operation_name)
79
+ target = operation_name.to_sym
80
+
81
+ port_type_operations.each do |port_type_name, operations|
82
+ match = operations.any? { |name| Wasabi::CoreExt::String.snakecase(name.to_s).to_sym == target }
83
+ return port_type_name if match
84
+ end
85
+
86
+ nil
87
+ end
88
+
89
+ # Returns a map from portType name to its operation names.
90
+ def port_type_operations
91
+ @port_type_operations ||= @document.root.element_children.each_with_object({}) do |node, memo|
92
+ next unless node.name == "portType"
93
+
94
+ operations = node.element_children.select { |child| child.name == "operation" }
95
+ memo[node["name"]] = operations.map { |operation| operation["name"] }
96
+ end
97
+ end
98
+
99
+ # Parses every +wsdl:port+ of the service in document order. Address
100
+ # elements are matched by local name so any namespace prefix works, and
101
+ # the SOAP version is derived from the address element's namespace.
102
+ def parse_ports
103
+ service_node = @document.root.element_children.find { |node| node.name == "service" }
104
+ return [] unless service_node
105
+
106
+ service_node.element_children.each_with_object([]) do |port, memo|
107
+ next unless port.name == "port"
108
+
109
+ address = port.element_children.find { |node| node.name == "address" }
110
+ next unless address && address["location"]
111
+
112
+ memo << {
113
+ name: port["name"].to_s,
114
+ binding: local_name(port["binding"]),
115
+ address: parse_url(address["location"]),
116
+ soap_version: soap_version_of(address)
117
+ }
118
+ end
119
+ end
120
+
121
+ # Parses every +wsdl:binding+ into a map from the local binding name
122
+ # to the local portType name.
123
+ def parse_bindings
124
+ @document.root.element_children.each_with_object({}) do |node, memo|
125
+ next unless node.name == "binding"
126
+
127
+ memo[node["name"].to_s] = local_name(node["type"])
128
+ end
129
+ end
130
+
131
+ # Returns the local part of a QName ("tns:Foo" -> "Foo").
132
+ def local_name(qname)
133
+ qname.to_s.split(":").last.to_s
134
+ end
135
+
136
+ # Returns 1 for SOAP 1.1 addresses, 2 for SOAP 1.2 addresses and nil
137
+ # for anything else (e.g. http:address ports).
138
+ def soap_version_of(address)
139
+ href = address.namespace&.href
140
+
141
+ if href == SOAP_1_2
142
+ 2
143
+ elsif href == SOAP_1_1
144
+ 1
145
+ end
146
+ end
147
+
148
+ def parse_url(url)
149
+ unescaped_url = Addressable::URI.unescape(url.to_s)
150
+ escaped_url = Addressable::URI.escape(unescaped_url)
151
+ URI(escaped_url)
152
+ rescue URI::InvalidURIError, Addressable::URI::InvalidURIError
153
+ nil
154
+ end
155
+ end
156
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wasabi
4
- VERSION = '5.0.3'
4
+ VERSION = "5.2.0"
5
5
  end
data/lib/wasabi.rb CHANGED
@@ -3,12 +3,11 @@
3
3
  require "wasabi/version"
4
4
  require "wasabi/document"
5
5
  require "wasabi/resolver"
6
+ require "wasabi/service_ports"
6
7
 
7
8
  module Wasabi
8
-
9
9
  # Expects a WSDL document and returns a <tt>Wasabi::Document</tt>.
10
10
  def self.document(document)
11
11
  Document.new(document)
12
12
  end
13
-
14
13
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wasabi
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.3
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Harrington
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-07-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: faraday
@@ -73,17 +72,17 @@ files:
73
72
  - lib/wasabi/document.rb
74
73
  - lib/wasabi/parser.rb
75
74
  - lib/wasabi/resolver.rb
75
+ - lib/wasabi/service_ports.rb
76
76
  - lib/wasabi/version.rb
77
77
  homepage: https://github.com/savonrb/wasabi
78
78
  licenses:
79
79
  - MIT
80
80
  metadata:
81
- changelog_uri: https://github.com/savonrb/wasabi/blob/master/CHANGELOG.md
82
- documentation_uri: https://www.rubydoc.info/gems/wasabi/5.0.3
81
+ changelog_uri: https://github.com/savonrb/wasabi/blob/main/CHANGELOG.md
82
+ documentation_uri: https://www.rubydoc.info/gems/wasabi/5.2.0
83
83
  source_code_uri: https://github.com/savonrb/wasabi
84
84
  bug_tracker_uri: https://github.com/savonrb/wasabi/issues
85
85
  rubygems_mfa_required: 'true'
86
- post_install_message:
87
86
  rdoc_options: []
88
87
  require_paths:
89
88
  - lib
@@ -98,8 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
97
  - !ruby/object:Gem::Version
99
98
  version: '0'
100
99
  requirements: []
101
- rubygems_version: 3.3.26
102
- signing_key:
100
+ rubygems_version: 3.6.9
103
101
  specification_version: 4
104
102
  summary: A simple WSDL parser
105
103
  test_files: []