Skip to main content

soap_server/wsdl/
resolver.rs

1// WSDL Pass 2 resolver — wire cross-references, load imports, delegate schemas to XSD layer
2// Also provides rewrite_wsdl_address() for GET ?wsdl serving.
3
4use 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/// Output of WSDL resolution: the wired definition + all type information.
13#[derive(Debug)]
14pub struct ResolvedWsdl {
15    /// The original WsdlDefinition with QName strings (for WSDL serving)
16    pub definition: WsdlDefinition,
17    /// All types from inline and imported schemas, fully resolved.
18    /// For document/literal operations, top-level element declarations with inline
19    /// anonymous complexTypes are also registered here under the element's own QName.
20    pub type_registry: TypeRegistry,
21    /// Maps top-level schema element QName → the QName to use for TypeRegistry lookup.
22    ///
23    /// For elements with inline complexType: element QName → element QName (registered in type_registry).
24    /// For elements with type= reference: element QName → named type QName.
25    /// Elements with no type information are absent from this map.
26    pub element_type_map: HashMap<QName, QName>,
27    /// Original WSDL bytes (for serving on GET ?wsdl)
28    pub raw_bytes: Vec<u8>,
29}
30
31/// Abstracts file/network I/O for loading WSDL files during recursive import resolution.
32pub trait WsdlLoader: Send + Sync {
33    fn load(&self, location: &str) -> Result<Vec<u8>, WsdlError>;
34}
35
36/// Resolve WSDL bytes into a fully-wired ResolvedWsdl.
37///
38/// Pass 1: parse_wsdl() → WsdlDefinition with raw QName strings
39/// Pass 2: wire cross-references, resolve imports, delegate inline schemas to XSD layer
40///
41/// `visited` tracks WSDL locations already loaded; prevents diamond-import double-loading.
42pub 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    // Pass 1: parse WSDL
71    let mut root_def = parse_wsdl(bytes)?;
72
73    // Process wsdl:import — recursively resolve imported WSDLs and merge their definitions
74    for import in root_def.imports.clone() {
75        let location = match &import.location {
76            Some(loc) => loc.clone(),
77            None => continue, // No location attribute — skip (namespace-only import)
78        };
79
80        if visited.contains(&location) {
81            // Diamond import guard — already processed this WSDL
82            continue;
83        }
84
85        // Cycle detection: location about to be loaded must not be in-flight
86        // We mark it visited before recursing, which covers the A→B→A cycle case
87        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        // Merge imported definition into root: messages, port_types, bindings, services
100        // (Types are already merged into accumulated_types via the recursive call above)
101        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    // Collect and resolve inline xs:schema nodes from wsdl:types
116    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        // Extract element→type mappings from the raw schema BEFORE consuming it in resolve_schema.
127        // For each top-level element:
128        //   - inline anonymous complexType → element QName maps to itself (registered in TypeRegistry)
129        //   - explicit type= reference → element QName maps to the named type QName
130        for (elem_qname, xsd_elem) in &raw_schema.elements {
131            if xsd_elem.inline_type.is_some() {
132                // Inline type — element QName will be registered in TypeRegistry after resolve_schema.
133                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                // Named type reference — validation type is the named type QName.
138                accumulated_element_type_map
139                    .entry(elem_qname.clone())
140                    .or_insert_with(|| type_ref.clone());
141            }
142            // Elements with no type info (empty/any) are left out — no schema expected.
143        }
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    // Build a TypeRegistry snapshot of all accumulated types so far.
153    // The root call's snapshot will contain everything (own + all imports transitively).
154    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    // Build the element type map snapshot.
160    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
170/// Adapter that wraps a WsdlLoader to implement SchemaLoader.
171/// Allows external XSD files referenced from inline schemas to be loaded via the same loader.
172struct 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
185/// Rewrite the `location` attribute value on `soap:address` / `soap12:address` elements
186/// in WSDL bytes, but ONLY for the port belonging to `service_name`.
187///
188/// Other services' addresses are left unchanged.  If `service_name` is `None` or empty,
189/// falls back to rewriting ALL addresses (single-service backward-compat).
190///
191/// Uses a two-pass approach: first pass finds which ports belong to `service_name` by
192/// parsing the WSDL structure; second pass rewrites only those addresses.
193pub 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    // Pass 1: find port names that belong to service_name.
207    // We track whether we're inside the target <service name="service_name"> element,
208    // and collect port names from <port> children.
209    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                        // Check if this is the target service
223                        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                            // Direct child of the target service
237                            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    // Pass 2: stream the WSDL, rewriting address elements only when inside a matched port.
282    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                    // Check if this port is in our target set
296                    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
385/// Rewrite the `location` attribute value on `soap:address` / `soap12:address` elements
386/// in WSDL bytes, replacing it with `new_url`. All other content is preserved unchanged.
387///
388/// Uses quick-xml streaming to avoid full parse overhead.
389pub 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                    // Rewrite the location= attribute if this is a soap:address element
407                    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    // ---- Test WSDL fixtures ----
462
463    // Root WSDL that imports a second WSDL
464    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    // Imported WSDL with its own message and portType operation
505    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    // Simple standalone WSDL with inline schema
522    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    // WSDL forming part A of a diamond: imports B and C
564    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    // WSDL with soap:address (SOAP 1.1 style)
609    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    // ---- Mock loaders ----
621
622    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                    // B imports A again — creates A→B→A cycle
665                    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    // ---- Tests ----
678
679    #[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        // Root operations present
687        assert!(
688            resolved.definition.messages.contains_key("RootMsg"),
689            "RootMsg should be in merged definition"
690        );
691
692        // Imported operations merged in
693        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        // TypeRegistry should contain StandaloneType from inline schema
711        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        // Should have loaded b.wsdl, c.wsdl, d.wsdl — d only once
731        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        // DType should appear exactly once (from d.wsdl loaded once)
748        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        // Mark a.wsdl as already visited before we start (simulating it being the root)
767        visited.insert("a.wsdl".to_string());
768
769        let result = resolve_wsdl(a_wsdl.as_bytes(), &CycleLoader, &mut visited);
770        // With visited containing "a.wsdl", when B tries to import "a.wsdl" it's skipped
771        // So this should succeed (no cycle error needed for this pattern)
772        // The cycle guard is: b.wsdl is loaded, then it tries to load a.wsdl but a.wsdl is already in visited
773        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        // Service name should still be present
818        assert!(
819            output.contains("RootService"),
820            "Service name should be preserved"
821        );
822        // Port name should still be present
823        assert!(output.contains("RootPort"), "Port name should be preserved");
824        // binding reference should still be there
825        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    // ── Finding #6 regression tests: per-service WSDL address rewriting ──────
856
857    /// Multi-service WSDL with ServiceA at /soap/a and ServiceB at /soap/b.
858    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    /// Serving /soap/a?wsdl for ServiceA must:
876    /// - Rewrite ServiceA's address to the current request host/path
877    /// - Leave ServiceB's address pointing at /soap/b (its original path)
878    #[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        // ServiceA's address should be rewritten to the current request URL
888        assert!(
889            output.contains("http://new-server/soap/a"),
890            "ServiceA address should be rewritten to new URL, got: {output}"
891        );
892
893        // ServiceB's address must still point to /soap/b (original path preserved)
894        assert!(
895            output.contains("http://old-server/soap/b"),
896            "ServiceB address must NOT be rewritten when serving ServiceA, got: {output}"
897        );
898
899        // The old ServiceA URL must no longer appear
900        assert!(
901            !output.contains("http://old-server/soap/a"),
902            "Old ServiceA address must be replaced, got: {output}"
903        );
904    }
905
906    /// Serving /soap/b?wsdl for ServiceB must rewrite B but not A.
907    #[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        // ServiceB's address should be updated
917        assert!(
918            output.contains("http://new-server/soap/b"),
919            "ServiceB address should be rewritten, got: {output}"
920        );
921
922        // ServiceA's address should remain unchanged
923        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    /// Empty service name falls back to rewriting all addresses (backward-compat).
930    #[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        // Both addresses should be rewritten
940        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}