1use 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
14const 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#[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
38pub 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 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 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 }
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
112fn 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
122fn 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 let schema_str = node_to_string(child);
132 schemas.push(schema_str);
133 }
134 }
135
136 Ok(TypesSection { schemas })
137}
138
139fn 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
156fn 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
183fn 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
200fn 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
269fn 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 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
329fn parse_binding_style(node: roxmltree::Node) -> BindingStyle {
331 match node.attribute("style") {
332 Some("rpc") => BindingStyle::Rpc,
333 _ => BindingStyle::Document, }
335}
336
337fn 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
396fn 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
441fn 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
458fn 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 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
485fn 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 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
504fn 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 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 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 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
568fn find_prefix_for_ns(node: roxmltree::Node, ns_uri: &str) -> Option<String> {
571 if ns_uri.is_empty() {
572 return None;
573 }
574 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}