Class: Wasabi::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/wasabi/parser.rb

Overview

Wasabi::Parser

Parses WSDL documents and remembers their important parts.

Defined Under Namespace

Classes: MessageRef

Constant Summary collapse

XSD =
"http://www.w3.org/2001/XMLSchema"
WSDL =
"http://schemas.xmlsoap.org/wsdl/"
SOAP_1_1 =
"http://schemas.xmlsoap.org/wsdl/soap/"
SOAP_1_2 =
"http://schemas.xmlsoap.org/wsdl/soap12/"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ Parser

Returns a new instance of Parser.



21
22
23
24
25
26
27
28
29
# File 'lib/wasabi/parser.rb', line 21

def initialize(document)
  self.document = document
  self.operations = {}
  self.namespaces = {}
  self.service_name = ""
  self.types = {}
  self.deferred_types = []
  self.element_form_default = :unqualified
end

Instance Attribute Details

#deferred_typesObject

Returns a map of deferred type Proc objects.



47
48
49
# File 'lib/wasabi/parser.rb', line 47

def deferred_types
  @deferred_types
end

#documentObject

Returns the Nokogiri document.



32
33
34
# File 'lib/wasabi/parser.rb', line 32

def document
  @document
end

#element_form_defaultObject

Returns the elementFormDefault value.



56
57
58
# File 'lib/wasabi/parser.rb', line 56

def element_form_default
  @element_form_default
end

#endpointObject

Returns the SOAP endpoint.



50
51
52
# File 'lib/wasabi/parser.rb', line 50

def endpoint
  @endpoint
end

#namespaceObject

Returns the target namespace.



35
36
37
# File 'lib/wasabi/parser.rb', line 35

def namespace
  @namespace
end

#namespacesObject

Returns a map from namespace identifier to namespace URI.



38
39
40
# File 'lib/wasabi/parser.rb', line 38

def namespaces
  @namespaces
end

#operationsObject

Returns the SOAP operations.



41
42
43
# File 'lib/wasabi/parser.rb', line 41

def operations
  @operations
end

#service_nameObject

Returns the SOAP Service Name



53
54
55
# File 'lib/wasabi/parser.rb', line 53

def service_name
  @service_name
end

#typesObject

Returns a map from a type name to a Hash with type information.



44
45
46
# File 'lib/wasabi/parser.rb', line 44

def types
  @types
end

Instance Method Details

#faults_for(operation) ⇒ Object

An operation may declare any number of faults, or none at all.



256
257
258
# File 'lib/wasabi/parser.rb', line 256

def faults_for(operation)
  resolve_messages(operation, "fault")
end

#input_for(operation) ⇒ Object

wasabi reports a single input and a single output per operation. When the portType or its message cannot be resolved or doesn't exist like with a one-way operation, it falls back to the operation name.



247
248
249
# File 'lib/wasabi/parser.rb', line 247

def input_for(operation)
  resolve_messages(operation, "input").first || MessageRef.new(nil, operation["name"])
end

#message_ref_for(operation, port_type_element, element_name, index) ⇒ MessageRef

Resolves a single portType input/output/fault element to the message it points at, walking portType -> message -> part -> element. Falls back to the operation name when the message cannot be resolved.

Returns:



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/wasabi/parser.rb', line 289

def message_ref_for(operation, port_type_element, element_name, index)
  operation_name = operation["name"]

  # If the message attribute contains a colon, the message is namespaced.
  parts = port_type_element.attribute("message").to_s.split(":", 2)
  port_message_ns_id, port_message_type = (parts.size == 2) ? parts : [nil, *parts]

  message_ns_id = nil
  message_type = nil

  # When there is a parts attribute in the soap:body element, use that value
  # to look up the message part from the messages array.
  input_output_element = operation.element_children.select { |node| node.name == element_name }.at(index)
  if input_output_element
    soap_body_element = input_output_element.element_children.find { |node| node.name == "body" }
    soap_body_parts = soap_body_element["parts"] if soap_body_element
  end

  # look for any message part that matches the soap body parts
  message = @messages[port_message_type]
  port_message_part = message&.element_children&.find do |node|
    soap_body_parts.nil? ? (node.name == "part") : (node.name == "part" && node["name"] == soap_body_parts)
  end

  if port_message_part && (port_element = port_message_part.attribute("element"))
    port_message_part = port_element.to_s
    if port_message_part.include?(":")
      message_ns_id, message_type = port_message_part.split(":")
    else
      message_type = port_message_part
    end
  end

  # If the message is not found, we should use the operation name as the message name
  # for document style operations applies only to output
  if element_name == "output"
    # if the operation is document style and theres no port_message_part, we should
    # use the operation_name
    soap_operation = operation.element_children.find { |node| node.name == "operation" }
    if message_type.nil? && (soap_operation.nil? || soap_operation["style"] != "rpc")
      message_ns_id = port_message_ns_id
      message_type = port_message_part.nil? ? operation_name : port_message_type
    end
  end

  # Fall back to the name of the binding operation
  if message_type
    MessageRef.new(message_ns_id, message_type)
  else
    MessageRef.new(port_message_ns_id, operation_name)
  end
end

#output_for(operation) ⇒ Object



251
252
253
# File 'lib/wasabi/parser.rb', line 251

def output_for(operation)
  resolve_messages(operation, "output").first || MessageRef.new(nil, operation["name"])
end

#parseObject



58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/wasabi/parser.rb', line 58

def parse
  parse_namespaces
  parse_endpoint
  parse_service_name
  parse_messages
  parse_port_types
  parse_port_type_operations
  parse_operations
  parse_operations_parameters
  parse_types
  parse_deferred_types
end

#parse_deferred_typesObject



240
241
242
# File 'lib/wasabi/parser.rb', line 240

def parse_deferred_types
  deferred_types.each(&:call)
end

#parse_endpointObject



83
84
85
86
87
88
89
90
# File 'lib/wasabi/parser.rb', line 83

def parse_endpoint
  if (service_node = service)
    endpoint = service_node.at_xpath(".//soap11:address/@location", "soap11" => SOAP_1_1)
    endpoint ||= service_node.at_xpath(service_node, ".//soap12:address/@location", "soap12" => SOAP_1_2)
  end

  @endpoint = parse_url(endpoint) if endpoint
end

#parse_messagesObject



104
105
106
107
# File 'lib/wasabi/parser.rb', line 104

def parse_messages
  messages = document.root.element_children.select { |node| node.name == "message" }
  @messages = messages.map { |node| [node["name"], node] }.to_h
end

#parse_namespacesObject



71
72
73
74
75
76
77
78
79
80
81
# File 'lib/wasabi/parser.rb', line 71

def parse_namespaces
  element_form_default = schemas.first && schemas.first["elementFormDefault"]
  @element_form_default = element_form_default.to_s.to_sym if element_form_default

  namespace = document.root["targetNamespace"]
  @namespace = namespace.to_s if namespace

  @namespaces = @document.namespaces.each_with_object({}) do |(key, value), memo|
    memo[key.sub("xmlns:", "")] = value
  end
end

#parse_operationsObject



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/wasabi/parser.rb', line 139

def parse_operations
  operations = document.xpath("wsdl:definitions/wsdl:binding/wsdl:operation", "wsdl" => WSDL)
  operations.each do |operation|
    name = operation.attribute("name").to_s
    snakecase_name = Wasabi::CoreExt::String.snakecase(name).to_sym

    # TODO: check for soap namespace?
    soap_operation = operation.element_children.find { |node| node.name == "operation" }
    soap_action = soap_operation["soapAction"] if soap_operation
    soap_document = soap_operation["style"] == "document" if soap_operation

    if soap_action || soap_document
      soap_action = soap_action.to_s
      action = (soap_action && !soap_action.empty?) ? soap_action : name

      # There should be a matching portType for each binding, so we will lookup the input from there.
      input = input_for(operation)
      output = output_for(operation)
      faults = faults_for(operation)

      # Store namespace identifier so this operation can be mapped to the proper namespace.
      operation_entry = {
        name: name,
        action: action,
        input: input.type,
        output: output.type,
        namespace_identifier: input.namespace_id
      }
      operation_entry[:fault] = faults.map(&:type) if faults.any?
      @operations[snakecase_name] = operation_entry
    elsif !@operations[snakecase_name]
      @operations[snakecase_name] = {action: name, input: name}
    end
  end
end

#parse_operations_parametersObject



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/wasabi/parser.rb', line 123

def parse_operations_parameters
  document.xpath("wsdl:definitions/wsdl:types/*[local-name()='schema']/*[local-name()='element']", "wsdl" => WSDL).each do |element|
    name = Wasabi::CoreExt::String.snakecase(element.attribute("name").to_s).to_sym

    if (operation = @operations[name])
      element.xpath("*[local-name() ='complexType']/*[local-name() ='sequence']/*[local-name() ='element']").each do |child_element|
        attr_name = child_element.attribute("name").to_s
        attr_type = ((attr_type = child_element.attribute("type").to_s.split(":")).size > 1) ? attr_type[1] : attr_type[0]

        operation[:parameters] ||= {}
        operation[:parameters][attr_name.to_sym] = {name: attr_name, type: attr_type}
      end
    end
  end
end

#parse_port_type_operationsObject



114
115
116
117
118
119
120
121
# File 'lib/wasabi/parser.rb', line 114

def parse_port_type_operations
  @port_type_operations = {}

  @port_types.each do |port_type_name, port_type|
    operations = port_type.element_children.select { |node| node.name == "operation" }
    @port_type_operations[port_type_name] = operations.map { |node| [node["name"], node] }.to_h
  end
end

#parse_port_typesObject



109
110
111
112
# File 'lib/wasabi/parser.rb', line 109

def parse_port_types
  port_types = document.root.element_children.select { |node| node.name == "portType" }
  @port_types = port_types.map { |node| [node["name"], node] }.to_h
end

#parse_service_nameObject



99
100
101
102
# File 'lib/wasabi/parser.rb', line 99

def parse_service_name
  service_name = document.root["name"]
  @service_name = service_name.to_s if service_name
end

#parse_typesObject



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/wasabi/parser.rb', line 175

def parse_types
  schemas.each do |schema|
    schema_namespace = schema["targetNamespace"]

    schema.element_children.each do |node|
      namespace = schema_namespace || @namespace

      case node.name
      when "element"
        complex_type = node.at_xpath("./xs:complexType", "xs" => XSD)
        process_type namespace, complex_type, node["name"].to_s if complex_type
      when "complexType"
        process_type namespace, node, node["name"].to_s
      end
    end
  end
end

#parse_url(url) ⇒ Object



92
93
94
95
96
97
# File 'lib/wasabi/parser.rb', line 92

def parse_url(url)
  unescaped_url = Addressable::URI.unescape(url.to_s)
  escaped_url = Addressable::URI.escape(unescaped_url)
  URI(escaped_url)
rescue URI::InvalidURIError, Addressable::URI::InvalidURIError
end

#port_type_operation_for(operation) ⇒ Object

Looks up the portType operation matching a binding operation, walking up to the portType via the binding's type attribute.



279
280
281
282
# File 'lib/wasabi/parser.rb', line 279

def port_type_operation_for(operation)
  binding_type = operation.parent["type"].to_s.split(":").last
  @port_type_operations.dig(binding_type, operation["name"])
end

#process_type(namespace, type, name) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/wasabi/parser.rb', line 193

def process_type(namespace, type, name)
  @types[namespace] ||= {}
  @types[namespace][name] ||= {namespace: namespace}
  @types[namespace][name][:order!] = []

  type.xpath("./xs:sequence/xs:element", "xs" => XSD).each do |inner|
    element_name = inner.attribute("name").to_s
    @types[namespace][name][element_name] = {type: inner.attribute("type").to_s}

    [:nillable, :minOccurs, :maxOccurs].each do |attr|
      if (v = inner.attribute(attr.to_s))
        @types[namespace][name][element_name][attr] = v.to_s
      end
    end

    @types[namespace][name][:order!] << element_name
  end

  type.xpath("./xs:complexContent/xs:extension/xs:sequence/xs:element", "xs" => XSD).each do |inner_element|
    element_name = inner_element.attribute("name").to_s
    @types[namespace][name][element_name] = {type: inner_element.attribute("type").to_s}

    @types[namespace][name][:order!] << element_name
  end

  type.xpath("./xs:complexContent/xs:extension[@base]", "xs" => XSD).each do |inherits|
    base = inherits.attribute("base").value.match(/\w+$/).to_s

    if @types[namespace][base]
      # Reverse merge because we don't want subclass attributes to be overriden by base class
      @types[namespace][name] = types[namespace][base].merge(types[namespace][name])
      @types[namespace][name][:order!] = @types[namespace][base][:order!] | @types[namespace][name][:order!]
      @types[namespace][name][:base_type] = base
    else
      p = proc do
        if @types[namespace][base]
          # Reverse merge because we don't want subclass attributes to be overriden by base class
          @types[namespace][name] = @types[namespace][base].merge(@types[namespace][name])
          @types[namespace][name][:order!] = @types[namespace][base][:order!] | @types[namespace][name][:order!]
          @types[namespace][name][:base_type] = base
        end
      end
      deferred_types << p
    end
  end
end

#resolve_messages(operation, element_name) ⇒ Array<MessageRef>

Resolves the wsdl messages an operation's input, output, or fault elements point at, by walking up to the portType and down into the messages. Returns one MessageRef per matching element, so none when there are no matches.

Returns:



265
266
267
268
269
270
271
272
273
274
275
# File 'lib/wasabi/parser.rb', line 265

def resolve_messages(operation, element_name)
  port_type_operation = port_type_operation_for(operation)

  # Sometimes portTypes are actually included in a separate WSDL,
  # so the portType operation cannot be resolved here.
  port_type_elements = port_type_operation&.element_children&.select { |node| node.name == element_name } || []

  port_type_elements.each_with_index.map do |port_type_element, index|
    message_ref_for(operation, port_type_element, element_name, index)
  end
end

#schemasObject



342
343
344
345
# File 'lib/wasabi/parser.rb', line 342

def schemas
  types = section("types").first
  types ? types.element_children : []
end

#section(section_name) ⇒ Object



352
353
354
# File 'lib/wasabi/parser.rb', line 352

def section(section_name)
  sections[section_name] || []
end

#sectionsObject



356
357
358
# File 'lib/wasabi/parser.rb', line 356

def sections
  @sections ||= document.root.element_children.group_by { |node| node.name }
end

#serviceObject



347
348
349
350
# File 'lib/wasabi/parser.rb', line 347

def service
  services = section("service")
  services&.first  # service nodes could be imported?
end