1use crate::qname::QName;
5use crate::wsdl::definitions::WsdlDefinition;
6use crate::wsdl::parser::{parse_wsdl, WsdlError};
7use crate::xsd::parser::{parse_schema, SchemaError};
8use crate::xsd::resolver::{resolve_schema, SchemaLoader};
9use crate::xsd::types::{TypeRegistry, XsdType};
10use std::collections::{HashMap, HashSet};
11
12#[derive(Debug)]
14pub struct ResolvedWsdl {
15 pub definition: WsdlDefinition,
17 pub type_registry: TypeRegistry,
21 pub element_type_map: HashMap<QName, QName>,
27 pub raw_bytes: Vec<u8>,
29}
30
31pub trait WsdlLoader: Send + Sync {
33 fn load(&self, location: &str) -> Result<Vec<u8>, WsdlError>;
34}
35
36pub fn resolve_wsdl(
43 bytes: &[u8],
44 loader: &dyn WsdlLoader,
45 visited: &mut HashSet<String>,
46) -> Result<ResolvedWsdl, WsdlError> {
47 let mut already_loaded_schemas: HashMap<String, ()> = HashMap::new();
48 let mut accumulated_types: HashMap<QName, XsdType> = HashMap::new();
49 let mut accumulated_element_type_map: HashMap<QName, QName> = HashMap::new();
50 resolve_wsdl_inner(
51 bytes,
52 loader,
53 visited,
54 &mut already_loaded_schemas,
55 &mut accumulated_types,
56 &mut accumulated_element_type_map,
57 )
58}
59
60fn resolve_wsdl_inner(
61 bytes: &[u8],
62 loader: &dyn WsdlLoader,
63 visited: &mut HashSet<String>,
64 already_loaded_schemas: &mut HashMap<String, ()>,
65 accumulated_types: &mut HashMap<QName, XsdType>,
66 accumulated_element_type_map: &mut HashMap<QName, QName>,
67) -> Result<ResolvedWsdl, WsdlError> {
68 let raw_bytes = bytes.to_vec();
69
70 let mut root_def = parse_wsdl(bytes)?;
72
73 for import in root_def.imports.clone() {
75 let location = match &import.location {
76 Some(loc) => loc.clone(),
77 None => continue, };
79
80 if visited.contains(&location) {
81 continue;
83 }
84
85 visited.insert(location.clone());
88
89 let imported_bytes = loader.load(&location)?;
90 let imported = resolve_wsdl_inner(
91 &imported_bytes,
92 loader,
93 visited,
94 already_loaded_schemas,
95 accumulated_types,
96 accumulated_element_type_map,
97 )?;
98
99 for (k, v) in imported.definition.messages {
102 root_def.messages.entry(k).or_insert(v);
103 }
104 for (k, v) in imported.definition.port_types {
105 root_def.port_types.entry(k).or_insert(v);
106 }
107 for (k, v) in imported.definition.bindings {
108 root_def.bindings.entry(k).or_insert(v);
109 }
110 for (k, v) in imported.definition.services {
111 root_def.services.entry(k).or_insert(v);
112 }
113 }
114
115 let schema_loader = WsdlSchemaLoaderAdapter {
117 wsdl_loader: loader,
118 };
119
120 for schema_str in &root_def.types.schemas {
121 let doc = roxmltree::Document::parse(schema_str)
122 .map_err(|e| WsdlError::MalformedXml(format!("inline schema parse error: {e}")))?;
123 let raw_schema = parse_schema(doc.root_element())
124 .map_err(|e| WsdlError::MalformedXml(format!("inline schema xsd parse error: {e}")))?;
125
126 for (elem_qname, xsd_elem) in &raw_schema.elements {
131 if xsd_elem.inline_type.is_some() {
132 accumulated_element_type_map
134 .entry(elem_qname.clone())
135 .or_insert_with(|| elem_qname.clone());
136 } else if let Some(type_ref) = &xsd_elem.type_ref {
137 accumulated_element_type_map
139 .entry(elem_qname.clone())
140 .or_insert_with(|| type_ref.clone());
141 }
142 }
144
145 let partial = resolve_schema(raw_schema, &schema_loader, already_loaded_schemas)
146 .map_err(|e| WsdlError::MalformedXml(format!("schema resolution error: {e}")))?;
147 for (qname, xsd_type) in partial {
148 accumulated_types.entry(qname).or_insert(xsd_type);
149 }
150 }
151
152 let mut type_registry = TypeRegistry::new();
155 for (qname, xsd_type) in accumulated_types.iter() {
156 type_registry.insert(qname.clone(), xsd_type.clone());
157 }
158
159 let element_type_map = accumulated_element_type_map.clone();
161
162 Ok(ResolvedWsdl {
163 definition: root_def,
164 type_registry,
165 element_type_map,
166 raw_bytes,
167 })
168}
169
170struct WsdlSchemaLoaderAdapter<'a> {
173 wsdl_loader: &'a dyn WsdlLoader,
174}
175
176impl<'a> SchemaLoader for WsdlSchemaLoaderAdapter<'a> {
177 fn load(&self, _namespace: Option<&str>, location: &str) -> Result<String, SchemaError> {
178 let bytes = self.wsdl_loader.load(location).map_err(|e| {
179 SchemaError::UnknownRef(format!("WsdlLoader error for {location}: {e}"))
180 })?;
181 String::from_utf8(bytes).map_err(|e| SchemaError::MalformedXml(format!("UTF-8 error: {e}")))
182 }
183}
184
185pub fn rewrite_wsdl_address_for_service(
194 bytes: &[u8],
195 new_url: &str,
196 service_name: &str,
197) -> Vec<u8> {
198 if service_name.is_empty() {
199 return rewrite_wsdl_address(bytes, new_url);
200 }
201
202 use quick_xml::events::{BytesStart, Event};
203 use quick_xml::reader::Reader;
204 use quick_xml::writer::Writer;
205
206 let target_port_names: std::collections::HashSet<String> = {
210 let mut reader = Reader::from_reader(bytes);
211 reader.config_mut().trim_text(false);
212 let mut port_names = std::collections::HashSet::new();
213 let mut in_target_service = false;
214 let mut service_depth = 0i32;
215
216 loop {
217 match reader.read_event() {
218 Ok(Event::Start(e)) => {
219 let local_bytes = e.local_name().as_ref().to_vec();
220 let local = std::str::from_utf8(&local_bytes).unwrap_or("").to_string();
221 if local == "service" {
222 let name_attr = e
224 .attributes()
225 .flatten()
226 .find(|a| std::str::from_utf8(a.key.as_ref()).unwrap_or("") == "name")
227 .and_then(|a| String::from_utf8(a.value.to_vec()).ok())
228 .unwrap_or_default();
229 if name_attr == service_name {
230 in_target_service = true;
231 service_depth = 1;
232 }
233 } else if in_target_service {
234 service_depth += 1;
235 if local == "port" && service_depth == 2 {
236 let name_attr = e
238 .attributes()
239 .flatten()
240 .find(|a| {
241 std::str::from_utf8(a.key.as_ref()).unwrap_or("") == "name"
242 })
243 .and_then(|a| String::from_utf8(a.value.to_vec()).ok())
244 .unwrap_or_default();
245 if !name_attr.is_empty() {
246 port_names.insert(name_attr);
247 }
248 }
249 }
250 }
251 Ok(Event::Empty(e)) => {
252 let local_bytes = e.local_name().as_ref().to_vec();
253 let local = std::str::from_utf8(&local_bytes).unwrap_or("").to_string();
254 if in_target_service && local == "port" && service_depth == 1 {
255 let name_attr = e
256 .attributes()
257 .flatten()
258 .find(|a| std::str::from_utf8(a.key.as_ref()).unwrap_or("") == "name")
259 .and_then(|a| String::from_utf8(a.value.to_vec()).ok())
260 .unwrap_or_default();
261 if !name_attr.is_empty() {
262 port_names.insert(name_attr);
263 }
264 }
265 }
266 Ok(Event::End(_)) => {
267 if in_target_service {
268 service_depth -= 1;
269 if service_depth == 0 {
270 in_target_service = false;
271 }
272 }
273 }
274 Ok(Event::Eof) | Err(_) => break,
275 _ => {}
276 }
277 }
278 port_names
279 };
280
281 let mut reader = Reader::from_reader(bytes);
283 reader.config_mut().trim_text(false);
284 let mut writer = Writer::new(Vec::new());
285
286 let mut in_matched_port = false;
287 let mut port_depth = 0i32;
288
289 loop {
290 match reader.read_event() {
291 Ok(Event::Start(e)) => {
292 let local_bytes = e.local_name().as_ref().to_vec();
293 let local = std::str::from_utf8(&local_bytes).unwrap_or("").to_string();
294 if local == "port" {
295 let name_attr = e
297 .attributes()
298 .flatten()
299 .find(|a| std::str::from_utf8(a.key.as_ref()).unwrap_or("") == "name")
300 .and_then(|a| String::from_utf8(a.value.to_vec()).ok())
301 .unwrap_or_default();
302 if target_port_names.contains(&name_attr) {
303 in_matched_port = true;
304 port_depth = 1;
305 } else if in_matched_port {
306 port_depth += 1;
307 }
308 let _ = writer.write_event(Event::Start(e));
309 } else if in_matched_port && local == "address" {
310 port_depth += 1;
311 let name_bytes = e.name().as_ref().to_vec();
312 let name_str =
313 String::from_utf8(name_bytes).unwrap_or_else(|_| "address".to_string());
314 let mut new_start = BytesStart::new(name_str.as_str());
315 for attr in e.attributes().flatten() {
316 let attr_key = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
317 if attr_key == "location" {
318 new_start.push_attribute(("location", new_url));
319 } else {
320 new_start.push_attribute(attr);
321 }
322 }
323 let _ = writer.write_event(Event::Start(new_start));
324 } else {
325 if in_matched_port {
326 port_depth += 1;
327 }
328 let _ = writer.write_event(Event::Start(e));
329 }
330 }
331 Ok(Event::Empty(e)) => {
332 let local_bytes = e.local_name().as_ref().to_vec();
333 let local = std::str::from_utf8(&local_bytes).unwrap_or("").to_string();
334 if local == "port" {
335 let name_attr = e
336 .attributes()
337 .flatten()
338 .find(|a| std::str::from_utf8(a.key.as_ref()).unwrap_or("") == "name")
339 .and_then(|a| String::from_utf8(a.value.to_vec()).ok())
340 .unwrap_or_default();
341 if target_port_names.contains(&name_attr) {
342 in_matched_port = true;
343 port_depth = 0;
344 }
345 let _ = writer.write_event(Event::Empty(e));
346 } else if in_matched_port && local == "address" {
347 let name_bytes = e.name().as_ref().to_vec();
348 let name_str =
349 String::from_utf8(name_bytes).unwrap_or_else(|_| "address".to_string());
350 let mut new_empty = BytesStart::new(name_str.as_str());
351 for attr in e.attributes().flatten() {
352 let attr_key = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
353 if attr_key == "location" {
354 new_empty.push_attribute(("location", new_url));
355 } else {
356 new_empty.push_attribute(attr);
357 }
358 }
359 let _ = writer.write_event(Event::Empty(new_empty));
360 } else {
361 let _ = writer.write_event(Event::Empty(e));
362 }
363 }
364 Ok(Event::End(e)) => {
365 if in_matched_port {
366 port_depth -= 1;
367 if port_depth <= 0 {
368 in_matched_port = false;
369 port_depth = 0;
370 }
371 }
372 let _ = writer.write_event(Event::End(e));
373 }
374 Ok(Event::Eof) => break,
375 Ok(other) => {
376 let _ = writer.write_event(other);
377 }
378 Err(_) => break,
379 }
380 }
381
382 writer.into_inner()
383}
384
385pub fn rewrite_wsdl_address(bytes: &[u8], new_url: &str) -> Vec<u8> {
390 use quick_xml::events::{BytesStart, Event};
391 use quick_xml::reader::Reader;
392 use quick_xml::writer::Writer;
393
394 let mut reader = Reader::from_reader(bytes);
395 reader.config_mut().trim_text(false);
396
397 let mut writer = Writer::new(Vec::new());
398
399 loop {
400 match reader.read_event() {
401 Ok(Event::Start(e)) => {
402 let local_name = e.local_name();
403 let local_str = std::str::from_utf8(local_name.as_ref()).unwrap_or("");
404
405 if local_str == "address" {
406 let name_bytes = e.name().as_ref().to_vec();
408 let name_str =
409 String::from_utf8(name_bytes).unwrap_or_else(|_| "address".to_string());
410 let mut new_start = BytesStart::new(name_str.as_str());
411 for attr in e.attributes().flatten() {
412 let attr_key = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
413 if attr_key == "location" {
414 new_start.push_attribute(("location", new_url));
415 } else {
416 new_start.push_attribute(attr);
417 }
418 }
419 let _ = writer.write_event(Event::Start(new_start));
420 } else {
421 let _ = writer.write_event(Event::Start(e));
422 }
423 }
424 Ok(Event::Empty(e)) => {
425 let local_name = e.local_name();
426 let local_str = std::str::from_utf8(local_name.as_ref()).unwrap_or("");
427
428 if local_str == "address" {
429 let name_bytes = e.name().as_ref().to_vec();
430 let name_str =
431 String::from_utf8(name_bytes).unwrap_or_else(|_| "address".to_string());
432 let mut new_empty = BytesStart::new(name_str.as_str());
433 for attr in e.attributes().flatten() {
434 let attr_key = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
435 if attr_key == "location" {
436 new_empty.push_attribute(("location", new_url));
437 } else {
438 new_empty.push_attribute(attr);
439 }
440 }
441 let _ = writer.write_event(Event::Empty(new_empty));
442 } else {
443 let _ = writer.write_event(Event::Empty(e));
444 }
445 }
446 Ok(Event::Eof) => break,
447 Ok(other) => {
448 let _ = writer.write_event(other);
449 }
450 Err(_) => break,
451 }
452 }
453
454 writer.into_inner()
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 const ROOT_WSDL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
465<definitions
466 xmlns="http://schemas.xmlsoap.org/wsdl/"
467 xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
468 xmlns:tns="http://example.com/root"
469 targetNamespace="http://example.com/root"
470 name="RootService">
471
472 <import namespace="http://example.com/imported" location="imported.wsdl"/>
473
474 <types>
475 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/root">
476 <xs:element name="RootElement" type="xs:string"/>
477 </xs:schema>
478 </types>
479
480 <message name="RootMsg"><part name="p" element="tns:RootElement"/></message>
481
482 <portType name="RootPT">
483 <operation name="RootOp">
484 <input message="tns:RootMsg"/>
485 </operation>
486 </portType>
487
488 <binding name="RootBinding" type="tns:RootPT">
489 <soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
490 <operation name="RootOp">
491 <soap12:operation soapAction="http://example.com/RootOp"/>
492 <input><soap12:body use="literal"/></input>
493 <output><soap12:body use="literal"/></output>
494 </operation>
495 </binding>
496
497 <service name="RootService">
498 <port name="RootPort" binding="tns:RootBinding">
499 <soap12:address location="http://old-server/soap"/>
500 </port>
501 </service>
502</definitions>"#;
503
504 const IMPORTED_WSDL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
506<definitions
507 xmlns="http://schemas.xmlsoap.org/wsdl/"
508 xmlns:tns="http://example.com/imported"
509 targetNamespace="http://example.com/imported"
510 name="ImportedService">
511
512 <message name="ImportedMsg"><part name="p" element="tns:ImportedElem"/></message>
513
514 <portType name="ImportedPT">
515 <operation name="ImportedOp">
516 <input message="tns:ImportedMsg"/>
517 </operation>
518 </portType>
519</definitions>"#;
520
521 const STANDALONE_WSDL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
523<definitions
524 xmlns="http://schemas.xmlsoap.org/wsdl/"
525 xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
526 xmlns:tns="http://example.com/standalone"
527 xmlns:xs="http://www.w3.org/2001/XMLSchema"
528 targetNamespace="http://example.com/standalone"
529 name="StandaloneService">
530
531 <types>
532 <xs:schema targetNamespace="http://example.com/standalone">
533 <xs:complexType name="StandaloneType">
534 <xs:sequence>
535 <xs:element name="field1" type="xs:string"/>
536 <xs:element name="field2" type="xs:int"/>
537 </xs:sequence>
538 </xs:complexType>
539 </xs:schema>
540 </types>
541
542 <message name="Req"><part name="p" element="tns:StandaloneElem"/></message>
543 <portType name="StandalonePT">
544 <operation name="StandaloneOp">
545 <input message="tns:Req"/>
546 </operation>
547 </portType>
548 <binding name="StandaloneBinding" type="tns:StandalonePT">
549 <soap12:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
550 <operation name="StandaloneOp">
551 <soap12:operation soapAction="http://example.com/StandaloneOp"/>
552 <input><soap12:body use="literal"/></input>
553 <output><soap12:body use="literal"/></output>
554 </operation>
555 </binding>
556 <service name="StandaloneService">
557 <port name="StandalonePort" binding="tns:StandaloneBinding">
558 <soap12:address location="http://old-server/soap"/>
559 </port>
560 </service>
561</definitions>"#;
562
563 const DIAMOND_A_WSDL: &str = r#"<?xml version="1.0"?>
565<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
566 xmlns:tns="http://example.com/a" targetNamespace="http://example.com/a">
567 <import namespace="http://example.com/b" location="b.wsdl"/>
568 <import namespace="http://example.com/c" location="c.wsdl"/>
569 <types>
570 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/a">
571 <xs:complexType name="AType"><xs:sequence/></xs:complexType>
572 </xs:schema>
573 </types>
574</definitions>"#;
575
576 const DIAMOND_B_WSDL: &str = r#"<?xml version="1.0"?>
577<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
578 xmlns:tns="http://example.com/b" targetNamespace="http://example.com/b">
579 <import namespace="http://example.com/d" location="d.wsdl"/>
580 <types>
581 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/b">
582 <xs:complexType name="BType"><xs:sequence/></xs:complexType>
583 </xs:schema>
584 </types>
585</definitions>"#;
586
587 const DIAMOND_C_WSDL: &str = r#"<?xml version="1.0"?>
588<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
589 xmlns:tns="http://example.com/c" targetNamespace="http://example.com/c">
590 <import namespace="http://example.com/d" location="d.wsdl"/>
591 <types>
592 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/c">
593 <xs:complexType name="CType"><xs:sequence/></xs:complexType>
594 </xs:schema>
595 </types>
596</definitions>"#;
597
598 const DIAMOND_D_WSDL: &str = r#"<?xml version="1.0"?>
599<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
600 xmlns:tns="http://example.com/d" targetNamespace="http://example.com/d">
601 <types>
602 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/d">
603 <xs:complexType name="DType"><xs:sequence/></xs:complexType>
604 </xs:schema>
605 </types>
606</definitions>"#;
607
608 const WSDL_WITH_SOAP11_ADDRESS: &str = r#"<?xml version="1.0"?>
610<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
611 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
612 xmlns:tns="http://example.com/svc" targetNamespace="http://example.com/svc">
613 <service name="Svc">
614 <port name="SvcPort" binding="tns:B">
615 <soap:address location="http://old-server/soap"/>
616 </port>
617 </service>
618</definitions>"#;
619
620 struct NullWsdlLoader;
623 impl WsdlLoader for NullWsdlLoader {
624 fn load(&self, location: &str) -> Result<Vec<u8>, WsdlError> {
625 Err(WsdlError::MalformedXml(format!(
626 "NullWsdlLoader cannot load: {location}"
627 )))
628 }
629 }
630
631 struct TwoFileLoader;
632 impl WsdlLoader for TwoFileLoader {
633 fn load(&self, location: &str) -> Result<Vec<u8>, WsdlError> {
634 match location {
635 "imported.wsdl" => Ok(IMPORTED_WSDL.as_bytes().to_vec()),
636 _ => Err(WsdlError::MalformedXml(format!(
637 "Unknown location: {location}"
638 ))),
639 }
640 }
641 }
642
643 struct DiamondLoader {
644 load_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
645 }
646 impl WsdlLoader for DiamondLoader {
647 fn load(&self, location: &str) -> Result<Vec<u8>, WsdlError> {
648 self.load_count
649 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
650 match location {
651 "b.wsdl" => Ok(DIAMOND_B_WSDL.as_bytes().to_vec()),
652 "c.wsdl" => Ok(DIAMOND_C_WSDL.as_bytes().to_vec()),
653 "d.wsdl" => Ok(DIAMOND_D_WSDL.as_bytes().to_vec()),
654 _ => Err(WsdlError::MalformedXml(format!("Unknown: {location}"))),
655 }
656 }
657 }
658
659 struct CycleLoader;
660 impl WsdlLoader for CycleLoader {
661 fn load(&self, location: &str) -> Result<Vec<u8>, WsdlError> {
662 match location {
663 "b.wsdl" => {
664 let b = r#"<?xml version="1.0"?>
666<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
667 xmlns:tns="http://example.com/b" targetNamespace="http://example.com/b">
668 <import namespace="http://example.com/a" location="a.wsdl"/>
669</definitions>"#;
670 Ok(b.as_bytes().to_vec())
671 }
672 _ => Err(WsdlError::MalformedXml(format!("Unknown: {location}"))),
673 }
674 }
675 }
676
677 #[test]
680 fn two_file_wsdl_merges_operations() {
681 let mut visited = HashSet::new();
682 let result = resolve_wsdl(ROOT_WSDL.as_bytes(), &TwoFileLoader, &mut visited);
683 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
684 let resolved = result.unwrap();
685
686 assert!(
688 resolved.definition.messages.contains_key("RootMsg"),
689 "RootMsg should be in merged definition"
690 );
691
692 assert!(
694 resolved.definition.messages.contains_key("ImportedMsg"),
695 "ImportedMsg from imported WSDL should be merged"
696 );
697 assert!(
698 resolved.definition.port_types.contains_key("ImportedPT"),
699 "ImportedPT from imported WSDL should be merged"
700 );
701 }
702
703 #[test]
704 fn standalone_wsdl_resolves_inline_schema() {
705 let mut visited = HashSet::new();
706 let result = resolve_wsdl(STANDALONE_WSDL.as_bytes(), &NullWsdlLoader, &mut visited);
707 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
708 let resolved = result.unwrap();
709
710 use crate::qname::QName;
712 let qname = QName::new("http://example.com/standalone", "StandaloneType");
713 assert!(
714 resolved.type_registry.lookup(&qname).is_some(),
715 "StandaloneType from inline xs:schema should be in TypeRegistry"
716 );
717 }
718
719 #[test]
720 fn diamond_import_loads_d_once() {
721 let load_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
722 let loader = DiamondLoader {
723 load_count: load_count.clone(),
724 };
725
726 let mut visited = HashSet::new();
727 let result = resolve_wsdl(DIAMOND_A_WSDL.as_bytes(), &loader, &mut visited);
728 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
729
730 let count = load_count.load(std::sync::atomic::Ordering::SeqCst);
732 assert_eq!(count, 3, "Expected 3 loader calls (b, c, d). Got: {count}");
733 }
734
735 #[test]
736 fn diamond_import_types_deduplicated_in_registry() {
737 let load_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
738 let loader = DiamondLoader {
739 load_count: load_count.clone(),
740 };
741
742 let mut visited = HashSet::new();
743 let resolved =
744 resolve_wsdl(DIAMOND_A_WSDL.as_bytes(), &loader, &mut visited).expect("resolve ok");
745
746 use crate::qname::QName;
747 assert!(
749 resolved
750 .type_registry
751 .lookup(&QName::new("http://example.com/d", "DType"))
752 .is_some(),
753 "DType from d.wsdl should appear in TypeRegistry"
754 );
755 }
756
757 #[test]
758 fn cycle_import_returns_err_without_stack_overflow() {
759 let a_wsdl = r#"<?xml version="1.0"?>
760<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
761 xmlns:tns="http://example.com/a" targetNamespace="http://example.com/a">
762 <import namespace="http://example.com/b" location="b.wsdl"/>
763</definitions>"#;
764
765 let mut visited = HashSet::new();
766 visited.insert("a.wsdl".to_string());
768
769 let result = resolve_wsdl(a_wsdl.as_bytes(), &CycleLoader, &mut visited);
770 assert!(
774 result.is_ok(),
775 "Cycle guard should prevent infinite recursion: {result:?}"
776 );
777 }
778
779 #[test]
780 fn rewrite_wsdl_address_replaces_location_soap12() {
781 let result = rewrite_wsdl_address(ROOT_WSDL.as_bytes(), "http://new-server/soap");
782 let output = String::from_utf8(result).expect("valid utf8");
783
784 assert!(
785 output.contains("http://new-server/soap"),
786 "Output should contain new URL"
787 );
788 assert!(
789 !output.contains("http://old-server/soap"),
790 "Output should not contain old URL"
791 );
792 }
793
794 #[test]
795 fn rewrite_wsdl_address_replaces_location_soap11() {
796 let result = rewrite_wsdl_address(
797 WSDL_WITH_SOAP11_ADDRESS.as_bytes(),
798 "http://new-server/soap",
799 );
800 let output = String::from_utf8(result).expect("valid utf8");
801
802 assert!(
803 output.contains("http://new-server/soap"),
804 "Output should contain new URL for SOAP 1.1 address"
805 );
806 assert!(
807 !output.contains("http://old-server/soap"),
808 "Output should not contain old URL"
809 );
810 }
811
812 #[test]
813 fn rewrite_wsdl_address_preserves_other_content() {
814 let result = rewrite_wsdl_address(ROOT_WSDL.as_bytes(), "http://new-server/soap");
815 let output = String::from_utf8(result).expect("valid utf8");
816
817 assert!(
819 output.contains("RootService"),
820 "Service name should be preserved"
821 );
822 assert!(output.contains("RootPort"), "Port name should be preserved");
824 assert!(
826 output.contains("RootBinding"),
827 "Binding reference should be preserved"
828 );
829 }
830
831 #[test]
832 fn raw_bytes_preserved_in_resolved_wsdl() {
833 let mut visited = HashSet::new();
834 let resolved = resolve_wsdl(STANDALONE_WSDL.as_bytes(), &NullWsdlLoader, &mut visited)
835 .expect("resolve ok");
836
837 assert_eq!(
838 resolved.raw_bytes,
839 STANDALONE_WSDL.as_bytes(),
840 "raw_bytes should match original input bytes"
841 );
842 }
843
844 #[test]
845 fn resolve_wsdl_no_imports_succeeds() {
846 let mut visited = HashSet::new();
847 let result = resolve_wsdl(STANDALONE_WSDL.as_bytes(), &NullWsdlLoader, &mut visited);
848 assert!(
849 result.is_ok(),
850 "Standalone WSDL with no imports should succeed: {:?}",
851 result.err()
852 );
853 }
854
855 const MULTI_SERVICE_WSDL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
859<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
860 xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
861 xmlns:tns="http://example.com/multi"
862 targetNamespace="http://example.com/multi">
863 <service name="ServiceA">
864 <port name="PortA" binding="tns:BindingA">
865 <soap12:address location="http://old-server/soap/a"/>
866 </port>
867 </service>
868 <service name="ServiceB">
869 <port name="PortB" binding="tns:BindingB">
870 <soap12:address location="http://old-server/soap/b"/>
871 </port>
872 </service>
873</definitions>"#;
874
875 #[test]
879 fn rewrite_wsdl_address_for_service_only_rewrites_matched_service() {
880 let result = rewrite_wsdl_address_for_service(
881 MULTI_SERVICE_WSDL.as_bytes(),
882 "http://new-server/soap/a",
883 "ServiceA",
884 );
885 let output = String::from_utf8(result).expect("valid utf8");
886
887 assert!(
889 output.contains("http://new-server/soap/a"),
890 "ServiceA address should be rewritten to new URL, got: {output}"
891 );
892
893 assert!(
895 output.contains("http://old-server/soap/b"),
896 "ServiceB address must NOT be rewritten when serving ServiceA, got: {output}"
897 );
898
899 assert!(
901 !output.contains("http://old-server/soap/a"),
902 "Old ServiceA address must be replaced, got: {output}"
903 );
904 }
905
906 #[test]
908 fn rewrite_wsdl_address_for_service_b_leaves_a_unchanged() {
909 let result = rewrite_wsdl_address_for_service(
910 MULTI_SERVICE_WSDL.as_bytes(),
911 "http://new-server/soap/b",
912 "ServiceB",
913 );
914 let output = String::from_utf8(result).expect("valid utf8");
915
916 assert!(
918 output.contains("http://new-server/soap/b"),
919 "ServiceB address should be rewritten, got: {output}"
920 );
921
922 assert!(
924 output.contains("http://old-server/soap/a"),
925 "ServiceA address must NOT be rewritten when serving ServiceB, got: {output}"
926 );
927 }
928
929 #[test]
931 fn rewrite_wsdl_address_for_service_empty_name_rewrites_all() {
932 let result = rewrite_wsdl_address_for_service(
933 MULTI_SERVICE_WSDL.as_bytes(),
934 "http://new-server/soap",
935 "",
936 );
937 let output = String::from_utf8(result).expect("valid utf8");
938
939 assert!(
941 !output.contains("http://old-server/soap/a"),
942 "Old ServiceA address should be rewritten, got: {output}"
943 );
944 assert!(
945 !output.contains("http://old-server/soap/b"),
946 "Old ServiceB address should be rewritten, got: {output}"
947 );
948 }
949}