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 +4 -4
- data/CHANGELOG.md +3 -2
- data/README.md +1 -3
- data/lib/wasabi/core_ext/string.rb +1 -3
- data/lib/wasabi/document.rb +40 -20
- data/lib/wasabi/parser.rb +148 -126
- data/lib/wasabi/resolver.rb +8 -11
- data/lib/wasabi/service_ports.rb +156 -0
- data/lib/wasabi/version.rb +1 -1
- data/lib/wasabi.rb +1 -2
- metadata +6 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fafbc0e7f78682f50c3f1466447f9d724e9cc00fc1dd6b8d7751c6f473c1f297
|
|
4
|
+
data.tar.gz: 193032ab7fcc93c7cd4f71bd69ca09e545ce5ede2167efa975fde11ce9c7ceaf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
##
|
|
3
|
+
## 5.2.0 (2026-09-18)
|
|
4
4
|
|
|
5
|
-
-
|
|
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
|
-
[](https://github.com/savonrb/wasabi/actions/workflows/ci.yml)
|
|
6
6
|
[](http://badge.fury.io/rb/wasabi)
|
|
7
|
-
[](https://codeclimate.com/github/savonrb/wasabi)
|
|
8
|
-
[](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
|
data/lib/wasabi/document.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
107
|
-
types.each do |
|
|
108
|
-
|
|
117
|
+
if document
|
|
118
|
+
parser.types.each do |ns, types|
|
|
119
|
+
types.each do |type, info|
|
|
120
|
+
namespaces << [[type], info[:namespace]]
|
|
109
121
|
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
element_keys(info).each do |field|
|
|
123
|
+
namespaces << [[type, field], info[:namespace]]
|
|
124
|
+
end
|
|
112
125
|
end
|
|
113
126
|
end
|
|
114
|
-
end
|
|
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
|
-
|
|
125
|
-
types.each do |
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
144
|
+
result << [[type, field], tag] if user_defined(namespace)
|
|
145
|
+
end
|
|
131
146
|
end
|
|
132
147
|
end
|
|
133
|
-
end
|
|
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
|
-
|
|
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
|
|
4
|
-
require
|
|
5
|
-
require
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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[
|
|
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[
|
|
75
|
+
namespace = document.root["targetNamespace"]
|
|
74
76
|
@namespace = namespace.to_s if namespace
|
|
75
77
|
|
|
76
|
-
@namespaces = @document.namespaces.
|
|
77
|
-
memo[key.sub(
|
|
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(
|
|
85
|
-
endpoint ||= service_node.at_xpath(service_node,
|
|
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[
|
|
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 ==
|
|
105
|
-
@messages =
|
|
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 ==
|
|
110
|
-
@port_types =
|
|
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 ==
|
|
118
|
-
@port_type_operations[port_type_name] =
|
|
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
|
-
|
|
124
|
-
name = Wasabi::CoreExt::String.snakecase(element.attribute(
|
|
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(
|
|
129
|
-
attr_type = (attr_type = child_element.attribute(
|
|
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] = {
|
|
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(
|
|
140
|
+
operations = document.xpath("wsdl:definitions/wsdl:binding/wsdl:operation", "wsdl" => WSDL)
|
|
140
141
|
operations.each do |operation|
|
|
141
|
-
name = operation.attribute(
|
|
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 ==
|
|
146
|
-
soap_action = soap_operation[
|
|
147
|
-
soap_document = 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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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] = {
|
|
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[
|
|
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
|
|
174
|
-
complex_type = node.at_xpath(
|
|
175
|
-
process_type namespace, complex_type, node[
|
|
176
|
-
when
|
|
177
|
-
process_type namespace, node, node[
|
|
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] ||= {
|
|
195
|
+
@types[namespace][name] ||= {namespace: namespace}
|
|
186
196
|
@types[namespace][name][:order!] = []
|
|
187
197
|
|
|
188
|
-
type.xpath(
|
|
189
|
-
element_name = inner.attribute(
|
|
190
|
-
@types[namespace][name][element_name] = {
|
|
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
|
-
[
|
|
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(
|
|
202
|
-
element_name = inner_element.attribute(
|
|
203
|
-
@types[namespace][name][element_name] = {
|
|
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(
|
|
209
|
-
base = inherits.attribute(
|
|
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 =
|
|
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
|
-
|
|
248
|
+
resolve_messages(operation, "input").first || MessageRef.new(nil, operation["name"])
|
|
236
249
|
end
|
|
237
250
|
|
|
238
251
|
def output_for(operation)
|
|
239
|
-
|
|
252
|
+
resolve_messages(operation, "output").first || MessageRef.new(nil, operation["name"])
|
|
240
253
|
end
|
|
241
254
|
|
|
242
|
-
#
|
|
243
|
-
def
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
256
|
-
#
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
338
|
+
MessageRef.new(port_message_ns_id, operation_name)
|
|
317
339
|
end
|
|
318
340
|
end
|
|
319
341
|
|
|
320
342
|
def schemas
|
|
321
|
-
types = section(
|
|
343
|
+
types = section("types").first
|
|
322
344
|
types ? types.element_children : []
|
|
323
345
|
end
|
|
324
346
|
|
|
325
347
|
def service
|
|
326
|
-
services = section(
|
|
327
|
-
services
|
|
348
|
+
services = section("service")
|
|
349
|
+
services&.first # service nodes could be imported?
|
|
328
350
|
end
|
|
329
351
|
|
|
330
352
|
def section(section_name)
|
data/lib/wasabi/resolver.rb
CHANGED
|
@@ -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 = /^
|
|
18
|
+
URL = /^https?:/
|
|
21
19
|
XML = /^</
|
|
22
20
|
|
|
23
21
|
def initialize(document, request = nil, adapter = nil)
|
|
24
22
|
@document = document
|
|
25
|
-
@request
|
|
26
|
-
@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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
data/lib/wasabi/version.rb
CHANGED
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.
|
|
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:
|
|
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/
|
|
82
|
-
documentation_uri: https://www.rubydoc.info/gems/wasabi/5.
|
|
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.
|
|
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: []
|