wasabi 5.1.0 → 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: af912c6da83bd8794f7929d89c792716f295b9949c6f6acf30211cdf7a608d9f
4
- data.tar.gz: 7c4826039acd022be1150ddde566e3217653b594199a42b55de82a36c2225109
3
+ metadata.gz: fafbc0e7f78682f50c3f1466447f9d724e9cc00fc1dd6b8d7751c6f473c1f297
4
+ data.tar.gz: 193032ab7fcc93c7cd4f71bd69ca09e545ce5ede2167efa975fde11ce9c7ceaf
5
5
  SHA512:
6
- metadata.gz: 7ba504cc015d6b070728279f8a50bc0b4e46c5a375fca2b2a4912c20f3d731c2a37dea913380397e7d2bdc8317c3f38cbb27f4c204ca2b5954087bbb464de1cb
7
- data.tar.gz: 207e7d588b64eab57a496b4de40fa5da60b75d140f3452714415d4400155b6d8fc4633907c402726d6c3ae0bc87e88942873f18d4b5cea4960afbb1b8b4f36eb
6
+ metadata.gz: 992ad19eb27a2e31594698517147b412dc05f9aab8825009325835276fbb97a1cffac5abc10d2fea3f103a9a4cfe8ed86e56df6806e3a45b8499a0ec581414e2
7
+ data.tar.gz: bd5a22e85ebbcc5273c276dd887268edf6c6a94387663f6f12d48c00e72646e2ea680f7a3a80a363635c7f5d70f2b4a033d8939c2474d8d2b67dc8bef209adee
data/CHANGELOG.md CHANGED
@@ -1,8 +1,9 @@
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.
6
7
 
7
8
  ## 5.1.0 (2024-10-27)
8
9
 
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
 
@@ -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,15 +114,17 @@ module Wasabi
103
114
  @type_namespaces ||= begin
104
115
  namespaces = []
105
116
 
106
- parser.types.each do |ns, types|
107
- types.each do |type, info|
108
- 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]]
109
121
 
110
- element_keys(info).each do |field|
111
- namespaces << [[type, field], info[:namespace]]
122
+ element_keys(info).each do |field|
123
+ namespaces << [[type, field], info[:namespace]]
124
+ end
112
125
  end
113
126
  end
114
- end if document
127
+ end
115
128
 
116
129
  namespaces
117
130
  end
@@ -121,16 +134,18 @@ module Wasabi
121
134
  @type_definitions ||= begin
122
135
  result = []
123
136
 
124
- parser.types.each do |ns, types|
125
- types.each do |type, info|
126
- element_keys(info).each do |field|
127
- field_type = info[field][:type]
128
- 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
129
143
 
130
- result << [[type, field], tag] if user_defined(namespace)
144
+ result << [[type, field], tag] if user_defined(namespace)
145
+ end
131
146
  end
132
147
  end
133
- end if document
148
+ end
134
149
 
135
150
  result
136
151
  end
@@ -153,7 +168,12 @@ module Wasabi
153
168
  @parser ||= guard_parse && parse
154
169
  end
155
170
 
156
- 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
157
177
 
158
178
  # Raises an error if the WSDL document is missing.
159
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,85 +97,94 @@ 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
@@ -182,15 +192,15 @@ module Wasabi
182
192
 
183
193
  def process_type(namespace, type, name)
184
194
  @types[namespace] ||= {}
185
- @types[namespace][name] ||= { :namespace => namespace }
195
+ @types[namespace][name] ||= {namespace: namespace}
186
196
  @types[namespace][name][:order!] = []
187
197
 
188
- type.xpath('./xs:sequence/xs:element', 'xs' => XSD).each do |inner|
189
- element_name = inner.attribute('name').to_s
190
- @types[namespace][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}
191
201
 
192
- [ :nillable, :minOccurs, :maxOccurs ].each do |attr|
193
- if v = inner.attribute(attr.to_s)
202
+ [:nillable, :minOccurs, :maxOccurs].each do |attr|
203
+ if (v = inner.attribute(attr.to_s))
194
204
  @types[namespace][name][element_name][attr] = v.to_s
195
205
  end
196
206
  end
@@ -198,15 +208,15 @@ module Wasabi
198
208
  @types[namespace][name][:order!] << element_name
199
209
  end
200
210
 
201
- type.xpath('./xs:complexContent/xs:extension/xs:sequence/xs:element', 'xs' => XSD).each do |inner_element|
202
- element_name = inner_element.attribute('name').to_s
203
- @types[namespace][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}
204
214
 
205
215
  @types[namespace][name][:order!] << element_name
206
216
  end
207
217
 
208
- type.xpath('./xs:complexContent/xs:extension[@base]', 'xs' => XSD).each do |inherits|
209
- 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
210
220
 
211
221
  if @types[namespace][base]
212
222
  # Reverse merge because we don't want subclass attributes to be overriden by base class
@@ -214,7 +224,7 @@ module Wasabi
214
224
  @types[namespace][name][:order!] = @types[namespace][base][:order!] | @types[namespace][name][:order!]
215
225
  @types[namespace][name][:base_type] = base
216
226
  else
217
- p = Proc.new do
227
+ p = proc do
218
228
  if @types[namespace][base]
219
229
  # Reverse merge because we don't want subclass attributes to be overriden by base class
220
230
  @types[namespace][name] = @types[namespace][base].merge(@types[namespace][name])
@@ -231,100 +241,112 @@ module Wasabi
231
241
  deferred_types.each(&:call)
232
242
  end
233
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.
234
247
  def input_for(operation)
235
- input_output_for(operation, 'input')
248
+ resolve_messages(operation, "input").first || MessageRef.new(nil, operation["name"])
236
249
  end
237
250
 
238
251
  def output_for(operation)
239
- input_output_for(operation, 'output')
252
+ resolve_messages(operation, "output").first || MessageRef.new(nil, operation["name"])
240
253
  end
241
254
 
242
- # @return [namespace_id, message_type]
243
- def input_output_for(operation, input_output)
244
- operation_name = operation['name']
245
-
246
- # Look up the input by walking up to portType, then up to the message.
247
-
248
- binding_type = operation.parent['type'].to_s.split(':').last
249
- if @port_type_operations[binding_type]
250
- port_type_operation = @port_type_operations[binding_type][operation_name]
251
- end
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
252
259
 
253
- port_type_input_output = port_type_operation&.element_children&.find { |node| node.name == input_output }
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)
254
267
 
255
- # find the message for the portType operation
256
- # if there is no message, we will use the operation name as the message name
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 } || []
257
271
 
258
- # TODO: Stupid fix for missing support for imports.
259
- # Sometimes portTypes are actually included in a separate WSDL.
260
- if port_type_input_output
261
- # If the message attribute contains a colon, it means the message is namespaced.
262
- if port_type_input_output.attribute('message').to_s.include? ':'
263
- port_message_ns_id, port_message_type = port_type_input_output.attribute('message').to_s.split(':')
264
- else
265
- port_message_type = port_type_input_output.attribute('message').to_s
266
- end
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
267
276
 
268
- message_ns_id, message_type = nil
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
269
283
 
270
- # When there is a parts attribute in soap:body element, we should use that value
271
- # to look up the message part from messages array.
272
- input_output_element = operation.element_children.find { |node| node.name == input_output }
273
- if input_output_element
274
- soap_body_element = input_output_element.element_children.find { |node| node.name == 'body' }
275
- soap_body_parts = soap_body_element['parts'] if soap_body_element
276
- 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
277
306
 
278
- # look for any message part that matches the soap body parts
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)
282
- 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
283
312
 
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
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
291
319
  end
320
+ end
292
321
 
293
- # If the message is not found, we should use the operation name as the message name for document style operations
294
- # applies only to output
295
- if input_output == 'output'
296
- # if the operation is document style and theres no port_message_part, we should use the operation_name
297
- soap_operation = operation.element_children.find { |node| node.name == 'operation' }
298
- if message_type.nil? && (soap_operation.nil? || soap_operation['style'] != 'rpc')
299
- if port_message_part.nil?
300
- message_ns_id = port_message_ns_id
301
- message_type = operation_name
302
- else
303
- message_ns_id = port_message_ns_id
304
- message_type = port_message_type
305
- end
306
- 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
307
331
  end
332
+ end
308
333
 
309
- # Fall back to the name of the binding operation
310
- if message_type
311
- [message_ns_id, message_type]
312
- else
313
- [port_message_ns_id, operation_name]
314
- end
334
+ # Fall back to the name of the binding operation
335
+ if message_type
336
+ MessageRef.new(message_ns_id, message_type)
315
337
  else
316
- [nil, operation_name]
338
+ MessageRef.new(port_message_ns_id, operation_name)
317
339
  end
318
340
  end
319
341
 
320
342
  def schemas
321
- types = section('types').first
343
+ types = section("types").first
322
344
  types ? types.element_children : []
323
345
  end
324
346
 
325
347
  def service
326
- services = section('service')
327
- services.first if services # service nodes could be imported?
348
+ services = section("service")
349
+ services&.first # service nodes could be imported?
328
350
  end
329
351
 
330
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.1.0'
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.1.0
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-10-27 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.1.0
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.4.10
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: []