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 +4 -4
- data/CHANGELOG.md +8 -2
- data/README.md +6 -3
- data/lib/wasabi/core_ext/string.rb +1 -3
- data/lib/wasabi/document.rb +42 -18
- data/lib/wasabi/parser.rb +161 -123
- 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,14 @@
|
|
|
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.
|
|
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
|
-
[](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
|
|
|
@@ -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
|
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,13 +114,17 @@ module Wasabi
|
|
|
103
114
|
@type_namespaces ||= begin
|
|
104
115
|
namespaces = []
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
if document
|
|
118
|
+
parser.types.each do |ns, types|
|
|
119
|
+
types.each do |type, info|
|
|
120
|
+
namespaces << [[type], info[:namespace]]
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
element_keys(info).each do |field|
|
|
123
|
+
namespaces << [[type, field], info[:namespace]]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
111
126
|
end
|
|
112
|
-
end
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
144
|
+
result << [[type, field], tag] if user_defined(namespace)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
128
147
|
end
|
|
129
|
-
end
|
|
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
|
-
|
|
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
|
|
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,129 +97,139 @@ 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
|
|
181
191
|
end
|
|
182
192
|
|
|
183
193
|
def process_type(namespace, type, name)
|
|
184
|
-
@types[
|
|
185
|
-
@types[
|
|
194
|
+
@types[namespace] ||= {}
|
|
195
|
+
@types[namespace][name] ||= {namespace: namespace}
|
|
196
|
+
@types[namespace][name][:order!] = []
|
|
186
197
|
|
|
187
|
-
type.xpath(
|
|
188
|
-
element_name = inner.attribute(
|
|
189
|
-
@types[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}
|
|
190
201
|
|
|
191
|
-
[
|
|
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(
|
|
201
|
-
element_name = inner_element.attribute(
|
|
202
|
-
@types[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}
|
|
203
214
|
|
|
204
|
-
@types[name][:order!] << element_name
|
|
215
|
+
@types[namespace][name][:order!] << element_name
|
|
205
216
|
end
|
|
206
217
|
|
|
207
|
-
type.xpath(
|
|
208
|
-
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
|
|
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 =
|
|
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
|
-
|
|
248
|
+
resolve_messages(operation, "input").first || MessageRef.new(nil, operation["name"])
|
|
235
249
|
end
|
|
236
250
|
|
|
237
251
|
def output_for(operation)
|
|
238
|
-
|
|
252
|
+
resolve_messages(operation, "output").first || MessageRef.new(nil, operation["name"])
|
|
239
253
|
end
|
|
240
254
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
280
|
-
port_message_part =
|
|
281
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
338
|
+
MessageRef.new(port_message_ns_id, operation_name)
|
|
301
339
|
end
|
|
302
340
|
end
|
|
303
341
|
|
|
304
342
|
def schemas
|
|
305
|
-
types = section(
|
|
343
|
+
types = section("types").first
|
|
306
344
|
types ? types.element_children : []
|
|
307
345
|
end
|
|
308
346
|
|
|
309
347
|
def service
|
|
310
|
-
services = section(
|
|
311
|
-
services
|
|
348
|
+
services = section("service")
|
|
349
|
+
services&.first # service nodes could be imported?
|
|
312
350
|
end
|
|
313
351
|
|
|
314
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.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:
|
|
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.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.
|
|
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: []
|