Skip to main content

soap_server/wsdl/
parser.rs

1// WSDL 1.1 Pass 1 parser — DOM traversal via roxmltree
2// Produces WsdlDefinition with unresolved QName strings.
3// Pass 2 (resolver.rs) wires cross-references.
4
5use crate::qname::QName;
6use crate::wsdl::definitions::{
7    Binding, BindingMessage, BindingOperation, BindingStyle, Message, MessagePart, Operation,
8    OperationFault, OperationMessage, OperationStyle, Port, PortType, Service, SoapBinding,
9    SoapBody, SoapHeader, SoapVersion, TypesSection, UseStyle, WsdlDefinition, WsdlImport,
10};
11use std::collections::HashMap;
12use thiserror::Error;
13
14// WSDL/SOAP namespaces
15const WSDL_NS: &str = "http://schemas.xmlsoap.org/wsdl/";
16const SOAP11_BINDING_NS: &str = "http://schemas.xmlsoap.org/wsdl/soap/";
17const SOAP12_BINDING_NS: &str = "http://schemas.xmlsoap.org/wsdl/soap12/";
18const XSD_NS: &str = "http://www.w3.org/2001/XMLSchema";
19const SOAP_HTTP_TRANSPORT: &str = "http://schemas.xmlsoap.org/soap/http";
20
21/// Error type for WSDL parsing failures.
22#[derive(Debug, Error)]
23pub enum WsdlError {
24    #[error("Malformed WSDL XML: {0}")]
25    MalformedXml(String),
26    #[error("Missing required attribute: {0}")]
27    MissingAttribute(String),
28    #[error("Unknown namespace prefix: {0}")]
29    UnknownNamespace(String),
30}
31
32impl From<roxmltree::Error> for WsdlError {
33    fn from(e: roxmltree::Error) -> Self {
34        WsdlError::MalformedXml(e.to_string())
35    }
36}
37
38/// Pass 1: parse WSDL bytes into WsdlDefinition with unresolved QName strings.
39/// Called with raw WSDL bytes (file contents or embedded bytes).
40pub fn parse_wsdl(bytes: &[u8]) -> Result<WsdlDefinition, WsdlError> {
41    let text = std::str::from_utf8(bytes)
42        .map_err(|e| WsdlError::MalformedXml(format!("UTF-8 error: {e}")))?;
43    let doc = roxmltree::Document::parse(text)?;
44    let root = doc.root_element();
45
46    // Validate root element
47    if root.tag_name().name() != "definitions" {
48        return Err(WsdlError::MalformedXml(format!(
49            "Expected root element 'definitions', got '{}'",
50            root.tag_name().name()
51        )));
52    }
53
54    let target_namespace = root.attribute("targetNamespace").unwrap_or("").to_string();
55
56    let mut imports: Vec<WsdlImport> = Vec::new();
57    let mut types = TypesSection::default();
58    let mut messages: HashMap<String, Message> = HashMap::new();
59    let mut port_types: HashMap<String, PortType> = HashMap::new();
60    let mut bindings: HashMap<String, Binding> = HashMap::new();
61    let mut services: HashMap<String, Service> = HashMap::new();
62
63    for child in root.children().filter(|n| n.is_element()) {
64        let local = child.tag_name().name();
65        let ns = child.tag_name().namespace().unwrap_or("");
66
67        if ns != WSDL_NS {
68            // Skip non-WSDL elements silently
69            continue;
70        }
71
72        match local {
73            "import" => {
74                imports.push(parse_import(child)?);
75            }
76            "types" => {
77                types = parse_types(child)?;
78            }
79            "message" => {
80                let msg = parse_message(child)?;
81                messages.insert(msg.name.clone(), msg);
82            }
83            "portType" => {
84                let pt = parse_port_type(child)?;
85                port_types.insert(pt.name.clone(), pt);
86            }
87            "binding" => {
88                let b = parse_binding(child)?;
89                bindings.insert(b.name.clone(), b);
90            }
91            "service" => {
92                let svc = parse_service(child)?;
93                services.insert(svc.name.clone(), svc);
94            }
95            _ => {
96                // Silently skip unknown WSDL elements
97            }
98        }
99    }
100
101    Ok(WsdlDefinition {
102        target_namespace,
103        imports,
104        types,
105        messages,
106        port_types,
107        bindings,
108        services,
109    })
110}
111
112/// Parse a <wsdl:import> element.
113fn parse_import(node: roxmltree::Node) -> Result<WsdlImport, WsdlError> {
114    let namespace = node.attribute("namespace").unwrap_or("").to_string();
115    let location = node.attribute("location").map(|s| s.to_string());
116    Ok(WsdlImport {
117        namespace,
118        location,
119    })
120}
121
122/// Parse the <wsdl:types> section, collecting inline xs:schema nodes.
123fn parse_types(node: roxmltree::Node) -> Result<TypesSection, WsdlError> {
124    let mut schemas: Vec<String> = Vec::new();
125
126    for child in node.children().filter(|n| n.is_element()) {
127        let ns = child.tag_name().namespace().unwrap_or("");
128        let local = child.tag_name().name();
129        if ns == XSD_NS && local == "schema" {
130            // Serialize the schema node back to string for the resolver
131            let schema_str = node_to_string(child);
132            schemas.push(schema_str);
133        }
134    }
135
136    Ok(TypesSection { schemas })
137}
138
139/// Parse a <wsdl:message> element.
140fn parse_message(node: roxmltree::Node) -> Result<Message, WsdlError> {
141    let name = node
142        .attribute("name")
143        .ok_or_else(|| WsdlError::MissingAttribute("message/@name".to_string()))?
144        .to_string();
145
146    let mut parts: Vec<MessagePart> = Vec::new();
147    for child in node.children().filter(|n| n.is_element()) {
148        if child.tag_name().name() == "part" {
149            parts.push(parse_message_part(child, node)?);
150        }
151    }
152
153    Ok(Message { name, parts })
154}
155
156/// Parse a <wsdl:part> element.
157fn parse_message_part(
158    node: roxmltree::Node,
159    parent: roxmltree::Node,
160) -> Result<MessagePart, WsdlError> {
161    let name = node
162        .attribute("name")
163        .ok_or_else(|| WsdlError::MissingAttribute("part/@name".to_string()))?
164        .to_string();
165
166    let element = node
167        .attribute("element")
168        .map(|v| resolve_qname_str(v, parent))
169        .transpose()?;
170
171    let type_ref = node
172        .attribute("type")
173        .map(|v| resolve_qname_str(v, parent))
174        .transpose()?;
175
176    Ok(MessagePart {
177        name,
178        element,
179        type_ref,
180    })
181}
182
183/// Parse a <wsdl:portType> element.
184fn parse_port_type(node: roxmltree::Node) -> Result<PortType, WsdlError> {
185    let name = node
186        .attribute("name")
187        .ok_or_else(|| WsdlError::MissingAttribute("portType/@name".to_string()))?
188        .to_string();
189
190    let mut operations: Vec<Operation> = Vec::new();
191    for child in node.children().filter(|n| n.is_element()) {
192        if child.tag_name().name() == "operation" {
193            operations.push(parse_operation(child)?);
194        }
195    }
196
197    Ok(PortType { name, operations })
198}
199
200/// Parse a <wsdl:operation> within a portType.
201fn parse_operation(node: roxmltree::Node) -> Result<Operation, WsdlError> {
202    let name = node
203        .attribute("name")
204        .ok_or_else(|| WsdlError::MissingAttribute("operation/@name".to_string()))?
205        .to_string();
206
207    let mut input: Option<OperationMessage> = None;
208    let mut output: Option<OperationMessage> = None;
209    let mut faults: Vec<OperationFault> = Vec::new();
210
211    for child in node.children().filter(|n| n.is_element()) {
212        match child.tag_name().name() {
213            "input" => {
214                let msg_attr = child
215                    .attribute("message")
216                    .ok_or_else(|| WsdlError::MissingAttribute("input/@message".to_string()))?;
217                let message = resolve_qname_str(msg_attr, child)?;
218                let op_name = child.attribute("name").map(|s| s.to_string());
219                input = Some(OperationMessage {
220                    name: op_name,
221                    message,
222                });
223            }
224            "output" => {
225                let msg_attr = child
226                    .attribute("message")
227                    .ok_or_else(|| WsdlError::MissingAttribute("output/@message".to_string()))?;
228                let message = resolve_qname_str(msg_attr, child)?;
229                let op_name = child.attribute("name").map(|s| s.to_string());
230                output = Some(OperationMessage {
231                    name: op_name,
232                    message,
233                });
234            }
235            "fault" => {
236                let fault_name = child
237                    .attribute("name")
238                    .ok_or_else(|| WsdlError::MissingAttribute("fault/@name".to_string()))?
239                    .to_string();
240                let msg_attr = child
241                    .attribute("message")
242                    .ok_or_else(|| WsdlError::MissingAttribute("fault/@message".to_string()))?;
243                let message = resolve_qname_str(msg_attr, child)?;
244                faults.push(OperationFault {
245                    name: fault_name,
246                    message,
247                });
248            }
249            _ => {}
250        }
251    }
252
253    let style = match (input.is_some(), output.is_some()) {
254        (true, true) => OperationStyle::RequestResponse,
255        (true, false) => OperationStyle::OneWay,
256        (false, true) => OperationStyle::Notification,
257        (false, false) => OperationStyle::OneWay,
258    };
259
260    Ok(Operation {
261        name,
262        input,
263        output,
264        faults,
265        style,
266    })
267}
268
269/// Parse a <wsdl:binding> element.
270fn parse_binding(node: roxmltree::Node) -> Result<Binding, WsdlError> {
271    let name = node
272        .attribute("name")
273        .ok_or_else(|| WsdlError::MissingAttribute("binding/@name".to_string()))?
274        .to_string();
275
276    let port_type_str = node
277        .attribute("type")
278        .ok_or_else(|| WsdlError::MissingAttribute("binding/@type".to_string()))?;
279    let port_type = resolve_qname_str(port_type_str, node)?;
280
281    // Find the soap:binding element to determine SOAP version and style
282    let mut soap_binding = SoapBinding {
283        style: BindingStyle::Document,
284        transport: SOAP_HTTP_TRANSPORT.to_string(),
285        soap_version: SoapVersion::Soap11,
286    };
287
288    let mut operations: Vec<BindingOperation> = Vec::new();
289
290    for child in node.children().filter(|n| n.is_element()) {
291        let child_ns = child.tag_name().namespace().unwrap_or("");
292        let child_local = child.tag_name().name();
293
294        match (child_local, child_ns) {
295            ("binding", SOAP11_BINDING_NS) => {
296                soap_binding.soap_version = SoapVersion::Soap11;
297                soap_binding.style = parse_binding_style(child);
298                soap_binding.transport = child
299                    .attribute("transport")
300                    .unwrap_or(SOAP_HTTP_TRANSPORT)
301                    .to_string();
302            }
303            ("binding", SOAP12_BINDING_NS) => {
304                soap_binding.soap_version = SoapVersion::Soap12;
305                soap_binding.style = parse_binding_style(child);
306                soap_binding.transport = child
307                    .attribute("transport")
308                    .unwrap_or(SOAP_HTTP_TRANSPORT)
309                    .to_string();
310            }
311            ("operation", WSDL_NS) => {
312                operations.push(parse_binding_operation(
313                    child,
314                    soap_binding.soap_version.clone(),
315                )?);
316            }
317            _ => {}
318        }
319    }
320
321    Ok(Binding {
322        name,
323        port_type,
324        soap_binding,
325        operations,
326    })
327}
328
329/// Parse the style attribute from a soap:binding element.
330fn parse_binding_style(node: roxmltree::Node) -> BindingStyle {
331    match node.attribute("style") {
332        Some("rpc") => BindingStyle::Rpc,
333        _ => BindingStyle::Document, // default per spec
334    }
335}
336
337/// Parse a <wsdl:operation> within a binding.
338fn parse_binding_operation(
339    node: roxmltree::Node,
340    soap_version: SoapVersion,
341) -> Result<BindingOperation, WsdlError> {
342    let name = node
343        .attribute("name")
344        .ok_or_else(|| WsdlError::MissingAttribute("binding/operation/@name".to_string()))?
345        .to_string();
346
347    let mut soap_action = String::new();
348    let mut input = BindingMessage {
349        body: SoapBody {
350            use_attr: UseStyle::Literal,
351            namespace: None,
352            encoding_style: None,
353        },
354        headers: Vec::new(),
355    };
356    let mut output = BindingMessage {
357        body: SoapBody {
358            use_attr: UseStyle::Literal,
359            namespace: None,
360            encoding_style: None,
361        },
362        headers: Vec::new(),
363    };
364
365    let soap_op_ns = match soap_version {
366        SoapVersion::Soap11 => SOAP11_BINDING_NS,
367        SoapVersion::Soap12 => SOAP12_BINDING_NS,
368    };
369
370    for child in node.children().filter(|n| n.is_element()) {
371        let child_ns = child.tag_name().namespace().unwrap_or("");
372        let child_local = child.tag_name().name();
373
374        match (child_local, child_ns) {
375            ("operation", ns) if ns == soap_op_ns => {
376                soap_action = child.attribute("soapAction").unwrap_or("").to_string();
377            }
378            ("input", WSDL_NS) => {
379                input = parse_binding_message(child, soap_op_ns)?;
380            }
381            ("output", WSDL_NS) => {
382                output = parse_binding_message(child, soap_op_ns)?;
383            }
384            _ => {}
385        }
386    }
387
388    Ok(BindingOperation {
389        name,
390        soap_action,
391        input,
392        output,
393    })
394}
395
396/// Parse a binding input/output message element.
397fn parse_binding_message(
398    node: roxmltree::Node,
399    soap_ns: &str,
400) -> Result<BindingMessage, WsdlError> {
401    let mut body = SoapBody {
402        use_attr: UseStyle::Literal,
403        namespace: None,
404        encoding_style: None,
405    };
406    let mut headers: Vec<SoapHeader> = Vec::new();
407
408    for child in node.children().filter(|n| n.is_element()) {
409        let child_ns = child.tag_name().namespace().unwrap_or("");
410        let child_local = child.tag_name().name();
411
412        if child_ns == soap_ns {
413            match child_local {
414                "body" => {
415                    body.use_attr = match child.attribute("use") {
416                        Some("encoded") => UseStyle::Encoded,
417                        _ => UseStyle::Literal,
418                    };
419                    body.namespace = child.attribute("namespace").map(|s| s.to_string());
420                    body.encoding_style = child.attribute("encodingStyle").map(|s| s.to_string());
421                }
422                "header" => {
423                    if let (Some(msg_str), Some(part)) =
424                        (child.attribute("message"), child.attribute("part"))
425                    {
426                        let message = resolve_qname_str(msg_str, child)?;
427                        headers.push(SoapHeader {
428                            message,
429                            part: part.to_string(),
430                        });
431                    }
432                }
433                _ => {}
434            }
435        }
436    }
437
438    Ok(BindingMessage { body, headers })
439}
440
441/// Parse a <wsdl:service> element.
442fn parse_service(node: roxmltree::Node) -> Result<Service, WsdlError> {
443    let name = node
444        .attribute("name")
445        .ok_or_else(|| WsdlError::MissingAttribute("service/@name".to_string()))?
446        .to_string();
447
448    let mut ports: Vec<Port> = Vec::new();
449    for child in node.children().filter(|n| n.is_element()) {
450        if child.tag_name().name() == "port" {
451            ports.push(parse_port(child)?);
452        }
453    }
454
455    Ok(Service { name, ports })
456}
457
458/// Parse a <wsdl:port> element.
459fn parse_port(node: roxmltree::Node) -> Result<Port, WsdlError> {
460    let name = node
461        .attribute("name")
462        .ok_or_else(|| WsdlError::MissingAttribute("port/@name".to_string()))?
463        .to_string();
464
465    let binding_str = node
466        .attribute("binding")
467        .ok_or_else(|| WsdlError::MissingAttribute("port/@binding".to_string()))?;
468    let binding = resolve_qname_str(binding_str, node)?;
469
470    // Find the soap:address child
471    let address = node
472        .children()
473        .filter(|n| n.is_element() && n.tag_name().name() == "address")
474        .find_map(|n| n.attribute("location"))
475        .unwrap_or("")
476        .to_string();
477
478    Ok(Port {
479        name,
480        binding,
481        address,
482    })
483}
484
485/// Resolve a "prefix:local" QName string using in-scope namespace bindings from `context_node`.
486fn resolve_qname_str(qname_str: &str, context_node: roxmltree::Node) -> Result<QName, WsdlError> {
487    match qname_str.split_once(':') {
488        Some((prefix, local)) => {
489            let ns = context_node
490                .lookup_namespace_uri(Some(prefix))
491                .ok_or_else(|| WsdlError::UnknownNamespace(prefix.to_string()))?;
492            Ok(QName::new(ns, local))
493        }
494        None => {
495            // No prefix — check for default namespace
496            match context_node.lookup_namespace_uri(None) {
497                Some(ns) => Ok(QName::new(ns, qname_str)),
498                None => Ok(QName::local(qname_str)),
499            }
500        }
501    }
502}
503
504/// Serialize a roxmltree::Node back to an XML string (best-effort, for schema extraction).
505fn node_to_string(node: roxmltree::Node) -> String {
506    let mut out = String::new();
507    serialize_node(node, &mut out);
508    out
509}
510
511fn serialize_node(node: roxmltree::Node, out: &mut String) {
512    if node.is_element() {
513        let tag_name = node.tag_name();
514        let local = tag_name.name();
515        let ns_uri = tag_name.namespace().unwrap_or("");
516
517        // Find the prefix for this element's namespace so we emit a qualified name.
518        // This is needed because the serialized fragment must be parseable standalone —
519        // the parent document's default namespace (e.g. WSDL namespace) must not bleed in.
520        let prefix = find_prefix_for_ns(node, ns_uri);
521        let qualified_name = match &prefix {
522            Some(p) if !p.is_empty() => format!("{p}:{local}"),
523            _ => local.to_string(),
524        };
525
526        out.push('<');
527        out.push_str(&qualified_name);
528
529        // Emit namespace declarations (only those declared on THIS node to avoid duplication)
530        for ns in node.namespaces() {
531            match ns.name() {
532                Some(p) => {
533                    out.push_str(&format!(" xmlns:{}=\"{}\"", p, ns.uri()));
534                }
535                None => {
536                    out.push_str(&format!(" xmlns=\"{}\"", ns.uri()));
537                }
538            }
539        }
540
541        // Emit attributes
542        for attr in node.attributes() {
543            out.push(' ');
544            out.push_str(attr.name());
545            out.push_str("=\"");
546            out.push_str(attr.value());
547            out.push('"');
548        }
549
550        if node.has_children() {
551            out.push('>');
552            for child in node.children() {
553                serialize_node(child, out);
554            }
555            out.push_str("</");
556            out.push_str(&qualified_name);
557            out.push('>');
558        } else {
559            out.push_str("/>");
560        }
561    } else if node.is_text() {
562        if let Some(text) = node.text() {
563            out.push_str(text);
564        }
565    }
566}
567
568/// Find the prefix bound to the given namespace URI in scope at `node`.
569/// Returns Some(prefix) where prefix may be empty string for default namespace.
570fn find_prefix_for_ns(node: roxmltree::Node, ns_uri: &str) -> Option<String> {
571    if ns_uri.is_empty() {
572        return None;
573    }
574    // Walk the in-scope namespaces to find one matching this URI
575    for ns in node.namespaces() {
576        if ns.uri() == ns_uri {
577            return Some(ns.name().unwrap_or("").to_string());
578        }
579    }
580    None
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use crate::wsdl::definitions::{BindingStyle, SoapVersion, UseStyle};
587
588    // Minimal WSDL with SOAP 1.2 binding, one service, one port, one binding, one portType, one operation
589    const MINIMAL_WSDL_SOAP12: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
590<definitions
591  xmlns="http://schemas.xmlsoap.org/wsdl/"
592  xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
593  xmlns:tns="http://example.com/device"
594  xmlns:xs="http://www.w3.org/2001/XMLSchema"
595  targetNamespace="http://example.com/device"
596  name="DeviceService">
597
598  <types>
599    <xs:schema targetNamespace="http://example.com/device">
600      <xs:element name="GetStatus"/>
601      <xs:element name="GetStatusResponse"/>
602    </xs:schema>
603  </types>
604
605  <message name="GetStatusRequest">
606    <part name="parameters" element="tns:GetStatus"/>
607  </message>
608  <message name="GetStatusResponse">
609    <part name="parameters" element="tns:GetStatusResponse"/>
610  </message>
611
612  <portType name="DevicePortType">
613    <operation name="GetStatus">
614      <input message="tns:GetStatusRequest"/>
615      <output message="tns:GetStatusResponse"/>
616    </operation>
617  </portType>
618
619  <binding name="DeviceBinding" type="tns:DevicePortType">
620    <soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
621    <operation name="GetStatus">
622      <soap12:operation soapAction="http://example.com/device/GetStatus"/>
623      <input>
624        <soap12:body use="literal"/>
625      </input>
626      <output>
627        <soap12:body use="literal"/>
628      </output>
629    </operation>
630  </binding>
631
632  <service name="DeviceService">
633    <port name="DevicePort" binding="tns:DeviceBinding">
634      <soap12:address location="http://192.168.1.1/onvif/device_service"/>
635    </port>
636  </service>
637</definitions>"#;
638
639    const MINIMAL_WSDL_SOAP11: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
640<definitions
641  xmlns="http://schemas.xmlsoap.org/wsdl/"
642  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
643  xmlns:tns="http://example.com/service"
644  targetNamespace="http://example.com/service"
645  name="TestService">
646
647  <message name="EchoRequest">
648    <part name="body" element="tns:Echo"/>
649  </message>
650  <message name="EchoResponse">
651    <part name="body" element="tns:EchoResponse"/>
652  </message>
653
654  <portType name="TestPortType">
655    <operation name="Echo">
656      <input message="tns:EchoRequest"/>
657      <output message="tns:EchoResponse"/>
658    </operation>
659  </portType>
660
661  <binding name="TestBinding" type="tns:TestPortType">
662    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
663    <operation name="Echo">
664      <soap:operation soapAction="http://example.com/Echo"/>
665      <input><soap:body use="literal"/></input>
666      <output><soap:body use="literal"/></output>
667    </operation>
668  </binding>
669
670  <service name="TestService">
671    <port name="TestPort" binding="tns:TestBinding">
672      <soap:address location="http://localhost/test"/>
673    </port>
674  </service>
675</definitions>"#;
676
677    const WSDL_WITH_IMPORT: &str = r#"<?xml version="1.0"?>
678<definitions
679  xmlns="http://schemas.xmlsoap.org/wsdl/"
680  xmlns:tns="http://example.com/svc"
681  targetNamespace="http://example.com/svc">
682  <import namespace="http://example.com/types" location="types.xsd"/>
683  <message name="Req"><part name="p" element="tns:ReqElem"/></message>
684  <portType name="PT"><operation name="Op"><input message="tns:Req"/></operation></portType>
685  <binding name="B" type="tns:PT">
686    <soap12:binding xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
687    <operation name="Op">
688      <soap12:operation xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" soapAction="http://example.com/Op"/>
689      <input><soap12:body xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" use="literal"/></input>
690      <output><soap12:body xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" use="literal"/></output>
691    </operation>
692  </binding>
693  <service name="Svc">
694    <port name="SvcPort" binding="tns:B">
695      <soap12:address xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" location="http://localhost/svc"/>
696    </port>
697  </service>
698</definitions>"#;
699
700    const WSDL_MULTIPLE_OPERATIONS: &str = r#"<?xml version="1.0"?>
701<definitions
702  xmlns="http://schemas.xmlsoap.org/wsdl/"
703  xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
704  xmlns:tns="http://example.com/multi"
705  targetNamespace="http://example.com/multi">
706  <message name="OpAReq"><part name="p" element="tns:OpAReqElem"/></message>
707  <message name="OpARes"><part name="p" element="tns:OpAResElem"/></message>
708  <message name="OpBReq"><part name="p" element="tns:OpBReqElem"/></message>
709  <message name="OpBRes"><part name="p" element="tns:OpBResElem"/></message>
710  <portType name="MultiPT">
711    <operation name="OpA">
712      <input message="tns:OpAReq"/>
713      <output message="tns:OpARes"/>
714    </operation>
715    <operation name="OpB">
716      <input message="tns:OpBReq"/>
717      <output message="tns:OpBRes"/>
718    </operation>
719  </portType>
720  <binding name="MultiB" type="tns:MultiPT">
721    <soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
722    <operation name="OpA">
723      <soap12:operation soapAction="http://example.com/OpA"/>
724      <input><soap12:body use="literal"/></input>
725      <output><soap12:body use="literal"/></output>
726    </operation>
727    <operation name="OpB">
728      <soap12:operation soapAction="http://example.com/OpB"/>
729      <input><soap12:body use="literal"/></input>
730      <output><soap12:body use="literal"/></output>
731    </operation>
732  </binding>
733  <service name="MultiSvc">
734    <port name="MultiPort" binding="tns:MultiB">
735      <soap12:address location="http://localhost/multi"/>
736    </port>
737  </service>
738</definitions>"#;
739
740    // ---- Basic parsing tests ----
741
742    #[test]
743    fn parse_minimal_wsdl_produces_definition() {
744        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
745        assert_eq!(def.target_namespace, "http://example.com/device");
746        assert!(def.services.contains_key("DeviceService"));
747        assert!(def.port_types.contains_key("DevicePortType"));
748        assert!(def.bindings.contains_key("DeviceBinding"));
749        assert!(def.messages.contains_key("GetStatusRequest"));
750        assert!(def.messages.contains_key("GetStatusResponse"));
751    }
752
753    #[test]
754    fn service_port_contains_address() {
755        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
756        let svc = def.services.get("DeviceService").unwrap();
757        assert_eq!(svc.ports.len(), 1);
758        let port = &svc.ports[0];
759        assert_eq!(port.name, "DevicePort");
760        assert_eq!(port.address, "http://192.168.1.1/onvif/device_service");
761    }
762
763    #[test]
764    fn port_binding_qname_is_resolved() {
765        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
766        let port = &def.services["DeviceService"].ports[0];
767        assert_eq!(
768            port.binding.namespace.as_deref(),
769            Some("http://example.com/device")
770        );
771        assert_eq!(port.binding.local_name, "DeviceBinding");
772    }
773
774    // ---- SOAP version detection tests ----
775
776    #[test]
777    fn soap12_binding_detected() {
778        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
779        let binding = def.bindings.get("DeviceBinding").unwrap();
780        assert_eq!(binding.soap_binding.soap_version, SoapVersion::Soap12);
781    }
782
783    #[test]
784    fn soap11_binding_detected() {
785        let def = parse_wsdl(MINIMAL_WSDL_SOAP11.as_bytes()).unwrap();
786        let binding = def.bindings.get("TestBinding").unwrap();
787        assert_eq!(binding.soap_binding.soap_version, SoapVersion::Soap11);
788    }
789
790    #[test]
791    fn binding_style_document() {
792        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
793        let binding = def.bindings.get("DeviceBinding").unwrap();
794        assert_eq!(binding.soap_binding.style, BindingStyle::Document);
795    }
796
797    // ---- PortType operation tests ----
798
799    #[test]
800    fn port_type_has_operation_with_input_and_output() {
801        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
802        let pt = def.port_types.get("DevicePortType").unwrap();
803        assert_eq!(pt.operations.len(), 1);
804        let op = &pt.operations[0];
805        assert_eq!(op.name, "GetStatus");
806        assert!(op.input.is_some());
807        assert!(op.output.is_some());
808    }
809
810    #[test]
811    fn operation_input_message_qname_resolved() {
812        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
813        let op = &def.port_types["DevicePortType"].operations[0];
814        let input = op.input.as_ref().unwrap();
815        assert_eq!(
816            input.message.namespace.as_deref(),
817            Some("http://example.com/device")
818        );
819        assert_eq!(input.message.local_name, "GetStatusRequest");
820    }
821
822    #[test]
823    fn operation_output_message_qname_resolved() {
824        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
825        let op = &def.port_types["DevicePortType"].operations[0];
826        let output = op.output.as_ref().unwrap();
827        assert_eq!(output.message.local_name, "GetStatusResponse");
828    }
829
830    // ---- Binding operation tests ----
831
832    #[test]
833    fn binding_operation_soap_action_extracted() {
834        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
835        let binding = def.bindings.get("DeviceBinding").unwrap();
836        assert_eq!(binding.operations.len(), 1);
837        assert_eq!(
838            binding.operations[0].soap_action,
839            "http://example.com/device/GetStatus"
840        );
841    }
842
843    #[test]
844    fn binding_operation_use_literal() {
845        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
846        let op = &def.bindings["DeviceBinding"].operations[0];
847        assert_eq!(op.input.body.use_attr, UseStyle::Literal);
848        assert_eq!(op.output.body.use_attr, UseStyle::Literal);
849    }
850
851    // ---- Message part tests ----
852
853    #[test]
854    fn message_part_element_qname_resolved() {
855        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
856        let msg = def.messages.get("GetStatusRequest").unwrap();
857        assert_eq!(msg.parts.len(), 1);
858        let part = &msg.parts[0];
859        assert_eq!(part.name, "parameters");
860        let elem = part.element.as_ref().unwrap();
861        assert_eq!(elem.namespace.as_deref(), Some("http://example.com/device"));
862        assert_eq!(elem.local_name, "GetStatus");
863    }
864
865    // ---- Import tests ----
866
867    #[test]
868    fn wsdl_import_with_location_recorded() {
869        let def = parse_wsdl(WSDL_WITH_IMPORT.as_bytes()).unwrap();
870        assert_eq!(def.imports.len(), 1);
871        assert_eq!(def.imports[0].namespace, "http://example.com/types");
872        assert_eq!(def.imports[0].location.as_deref(), Some("types.xsd"));
873    }
874
875    // ---- Inline schema tests ----
876
877    #[test]
878    fn inline_schema_nodes_collected() {
879        let def = parse_wsdl(MINIMAL_WSDL_SOAP12.as_bytes()).unwrap();
880        assert!(
881            !def.types.schemas.is_empty(),
882            "Expected inline schema to be collected"
883        );
884        assert!(
885            def.types.schemas[0].contains("xs:schema") || def.types.schemas[0].contains("schema")
886        );
887    }
888
889    // ---- Multiple operations test ----
890
891    #[test]
892    fn multiple_operations_all_present() {
893        let def = parse_wsdl(WSDL_MULTIPLE_OPERATIONS.as_bytes()).unwrap();
894        let pt = def.port_types.get("MultiPT").unwrap();
895        assert_eq!(pt.operations.len(), 2);
896        let names: Vec<&str> = pt.operations.iter().map(|o| o.name.as_str()).collect();
897        assert!(names.contains(&"OpA"));
898        assert!(names.contains(&"OpB"));
899    }
900
901    #[test]
902    fn multiple_binding_operations_all_present() {
903        let def = parse_wsdl(WSDL_MULTIPLE_OPERATIONS.as_bytes()).unwrap();
904        let b = def.bindings.get("MultiB").unwrap();
905        assert_eq!(b.operations.len(), 2);
906    }
907
908    // ---- Unknown element silently skipped ----
909
910    #[test]
911    fn unknown_wsdl_elements_silently_skipped() {
912        let wsdl = r#"<?xml version="1.0"?>
913<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://example.com" targetNamespace="http://example.com">
914  <documentation>This is a doc</documentation>
915  <message name="M1"><part name="p" element="tns:E1"/></message>
916</definitions>"#;
917        let result = parse_wsdl(wsdl.as_bytes());
918        assert!(
919            result.is_ok(),
920            "Should not error on unknown elements: {result:?}"
921        );
922        let def = result.unwrap();
923        assert!(def.messages.contains_key("M1"));
924    }
925
926    // ---- Error cases ----
927
928    #[test]
929    fn malformed_xml_returns_error() {
930        let result = parse_wsdl(b"<not valid xml");
931        assert!(result.is_err());
932    }
933
934    #[test]
935    fn wrong_root_element_returns_error() {
936        let result = parse_wsdl(b"<schema xmlns=\"http://www.w3.org/2001/XMLSchema\"/>");
937        assert!(result.is_err());
938        match result.unwrap_err() {
939            WsdlError::MalformedXml(_) => {}
940            e => panic!("Expected MalformedXml, got: {e:?}"),
941        }
942    }
943}