Skip to main content

zerodds_soap/
wsdl.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! WSDL 1.1 + 2.0 Generation aus IDL-Service-Defs.
5//!
6//! WSDL 1.1: `http://schemas.xmlsoap.org/wsdl/`
7//! WSDL 2.0: `http://www.w3.org/ns/wsdl`
8//!
9//! Wir generieren Spec-konforme `<types>`/`<message>`/`<portType>`/
10//! `<binding>`/`<service>` (1.1) bzw. `<types>`/`<interface>`/
11//! `<binding>`/`<service>` (2.0).
12
13use alloc::format;
14use alloc::string::String;
15use alloc::vec::Vec;
16
17/// WSDL-Version-Selector.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum WsdlVersion {
20    /// WSDL 1.1 (`http://schemas.xmlsoap.org/wsdl/`).
21    V11,
22    /// WSDL 2.0 (`http://www.w3.org/ns/wsdl`).
23    V20,
24}
25
26/// Operation einer WSDL-Schnittstelle.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Operation {
29    /// Operation-Name.
30    pub name: String,
31    /// Input-Message-Element (XSD-Type-Name).
32    pub input_type: String,
33    /// Output-Message-Element (XSD-Type-Name).
34    pub output_type: String,
35}
36
37/// WSDL-Generator-Builder.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct WsdlGenerator {
40    service_name: String,
41    target_namespace: String,
42    endpoint_url: String,
43    operations: Vec<Operation>,
44    version: WsdlVersion,
45}
46
47impl WsdlGenerator {
48    /// Konstruktor.
49    #[must_use]
50    pub fn new(service_name: &str, namespace: &str, endpoint: &str, version: WsdlVersion) -> Self {
51        Self {
52            service_name: service_name.into(),
53            target_namespace: namespace.into(),
54            endpoint_url: endpoint.into(),
55            operations: Vec::new(),
56            version,
57        }
58    }
59
60    /// Operation hinzufuegen.
61    #[must_use]
62    pub fn operation(mut self, op: Operation) -> Self {
63        self.operations.push(op);
64        self
65    }
66
67    /// Render-Method (laut Version-Schalter).
68    #[must_use]
69    pub fn render(&self) -> String {
70        match self.version {
71            WsdlVersion::V11 => self.render_v11(),
72            WsdlVersion::V20 => self.render_v20(),
73        }
74    }
75
76    fn render_v11(&self) -> String {
77        let mut out = String::new();
78        out.push_str("<?xml version=\"1.0\"?>\n");
79        out.push_str(&format!(
80            "<definitions name=\"{}\" targetNamespace=\"{}\" \
81xmlns=\"http://schemas.xmlsoap.org/wsdl/\" \
82xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" \
83xmlns:tns=\"{}\" \
84xmlns:soap=\"http://schemas.xmlsoap.org/wsdl/soap/\">\n",
85            self.service_name, self.target_namespace, self.target_namespace
86        ));
87        // Messages.
88        for op in &self.operations {
89            out.push_str(&format!(
90                "  <message name=\"{}Request\"><part name=\"body\" element=\"tns:{}\"/></message>\n",
91                op.name, op.input_type
92            ));
93            out.push_str(&format!(
94                "  <message name=\"{}Response\"><part name=\"body\" element=\"tns:{}\"/></message>\n",
95                op.name, op.output_type
96            ));
97        }
98        // PortType.
99        out.push_str(&format!(
100            "  <portType name=\"{}PortType\">\n",
101            self.service_name
102        ));
103        for op in &self.operations {
104            out.push_str(&format!(
105                "    <operation name=\"{0}\">\n      <input message=\"tns:{0}Request\"/>\n      <output message=\"tns:{0}Response\"/>\n    </operation>\n",
106                op.name
107            ));
108        }
109        out.push_str("  </portType>\n");
110        // Binding.
111        out.push_str(&format!(
112            "  <binding name=\"{0}SoapBinding\" type=\"tns:{0}PortType\">\n    <soap:binding style=\"document\" transport=\"http://schemas.xmlsoap.org/soap/http\"/>\n",
113            self.service_name
114        ));
115        for op in &self.operations {
116            out.push_str(&format!(
117                "    <operation name=\"{}\"><soap:operation soapAction=\"{}/{}\"/><input><soap:body use=\"literal\"/></input><output><soap:body use=\"literal\"/></output></operation>\n",
118                op.name, self.target_namespace, op.name
119            ));
120        }
121        out.push_str("  </binding>\n");
122        // Service.
123        out.push_str(&format!(
124            "  <service name=\"{0}\">\n    <port name=\"{0}Port\" binding=\"tns:{0}SoapBinding\">\n      <soap:address location=\"{1}\"/>\n    </port>\n  </service>\n",
125            self.service_name, self.endpoint_url
126        ));
127        out.push_str("</definitions>\n");
128        out
129    }
130
131    fn render_v20(&self) -> String {
132        let mut out = String::new();
133        out.push_str("<?xml version=\"1.0\"?>\n");
134        out.push_str(&format!(
135            "<description xmlns=\"http://www.w3.org/ns/wsdl\" \
136targetNamespace=\"{}\" \
137xmlns:tns=\"{}\" \
138xmlns:wsoap=\"http://www.w3.org/ns/wsdl/soap\">\n",
139            self.target_namespace, self.target_namespace
140        ));
141        // Interface.
142        out.push_str(&format!(
143            "  <interface name=\"{}Interface\">\n",
144            self.service_name
145        ));
146        for op in &self.operations {
147            out.push_str(&format!(
148                "    <operation name=\"{0}\" pattern=\"http://www.w3.org/ns/wsdl/in-out\">\n      <input element=\"tns:{1}\"/>\n      <output element=\"tns:{2}\"/>\n    </operation>\n",
149                op.name, op.input_type, op.output_type
150            ));
151        }
152        out.push_str("  </interface>\n");
153        // Binding.
154        out.push_str(&format!(
155            "  <binding name=\"{0}SoapBinding\" interface=\"tns:{0}Interface\" type=\"http://www.w3.org/ns/wsdl/soap\" wsoap:protocol=\"http://www.w3.org/2003/05/soap/bindings/HTTP/\"/>\n",
156            self.service_name
157        ));
158        // Service.
159        out.push_str(&format!(
160            "  <service name=\"{0}\" interface=\"tns:{0}Interface\">\n    <endpoint name=\"{0}Endpoint\" binding=\"tns:{0}SoapBinding\" address=\"{1}\"/>\n  </service>\n",
161            self.service_name, self.endpoint_url
162        ));
163        out.push_str("</description>\n");
164        out
165    }
166
167    /// Anzahl konfigurierter Operations.
168    #[must_use]
169    pub fn operation_count(&self) -> usize {
170        self.operations.len()
171    }
172}
173
174#[cfg(test)]
175#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
176mod tests {
177    use super::*;
178
179    fn echo_op() -> Operation {
180        Operation {
181            name: "Echo".into(),
182            input_type: "EchoRequest".into(),
183            output_type: "EchoResponse".into(),
184        }
185    }
186
187    #[test]
188    fn wsdl_11_emits_definitions_root() {
189        let g = WsdlGenerator::new(
190            "TraderService",
191            "http://demo/trader",
192            "http://localhost/svc",
193            WsdlVersion::V11,
194        )
195        .operation(echo_op());
196        let xml = g.render();
197        assert!(xml.contains("<definitions name=\"TraderService\""));
198        assert!(xml.contains("<portType name=\"TraderServicePortType\""));
199        assert!(xml.contains("<message name=\"EchoRequest\""));
200        assert!(xml.contains("<service name=\"TraderService\""));
201        assert!(xml.contains("schemas.xmlsoap.org/wsdl/"));
202    }
203
204    #[test]
205    fn wsdl_20_emits_description_root() {
206        let g = WsdlGenerator::new(
207            "TraderService",
208            "http://demo/trader",
209            "http://localhost/svc",
210            WsdlVersion::V20,
211        )
212        .operation(echo_op());
213        let xml = g.render();
214        assert!(xml.contains("<description xmlns=\"http://www.w3.org/ns/wsdl\""));
215        assert!(xml.contains("<interface name=\"TraderServiceInterface\">"));
216        assert!(xml.contains("<endpoint name=\"TraderServiceEndpoint\""));
217    }
218
219    #[test]
220    fn wsdl_11_includes_all_operation_messages() {
221        let g = WsdlGenerator::new("S", "http://demo/", "http://demo/svc", WsdlVersion::V11)
222            .operation(echo_op())
223            .operation(Operation {
224                name: "Ping".into(),
225                input_type: "PingRequest".into(),
226                output_type: "PingResponse".into(),
227            });
228        let xml = g.render();
229        assert!(xml.contains("EchoRequest"));
230        assert!(xml.contains("EchoResponse"));
231        assert!(xml.contains("PingRequest"));
232        assert!(xml.contains("PingResponse"));
233    }
234
235    #[test]
236    fn wsdl_11_binding_uses_soap_http_transport() {
237        let g = WsdlGenerator::new("S", "http://demo/", "http://demo/svc", WsdlVersion::V11)
238            .operation(echo_op());
239        let xml = g.render();
240        assert!(xml.contains("transport=\"http://schemas.xmlsoap.org/soap/http\""));
241    }
242
243    #[test]
244    fn wsdl_20_binding_targets_soap_protocol() {
245        let g = WsdlGenerator::new("S", "http://demo/", "http://demo/svc", WsdlVersion::V20)
246            .operation(echo_op());
247        let xml = g.render();
248        assert!(xml.contains("wsoap:protocol="));
249        assert!(xml.contains("soap/bindings/HTTP"));
250    }
251
252    #[test]
253    fn endpoint_address_is_included() {
254        let g = WsdlGenerator::new(
255            "S",
256            "http://demo/",
257            "http://example.org:8080/api",
258            WsdlVersion::V11,
259        )
260        .operation(echo_op());
261        let xml = g.render();
262        assert!(xml.contains("location=\"http://example.org:8080/api\""));
263    }
264}