Skip to main content

zerodds_soap/
fault.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! SOAP-Fault — W3C SOAP 1.2 Part 1 §5.4.
5//!
6//! Spec §5.4: SOAP-Fault hat fuenf Standard-Codes:
7//! `VersionMismatch`, `MustUnderstand`, `DataEncodingUnknown`,
8//! `Sender` (Client-Side-Error), `Receiver` (Server-Side-Error).
9
10use alloc::format;
11use alloc::string::{String, ToString};
12
13use crate::envelope::SOAP_12_NS;
14
15/// SOAP 1.2 Fault-Code (W3C SOAP 1.2 §5.4.1.1).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum FaultCode {
18    /// `env:VersionMismatch`.
19    VersionMismatch,
20    /// `env:MustUnderstand`.
21    MustUnderstand,
22    /// `env:DataEncodingUnknown`.
23    DataEncodingUnknown,
24    /// `env:Sender` (Client-Side-Error, prev. SOAP 1.1 `Client`).
25    Sender,
26    /// `env:Receiver` (Server-Side-Error, prev. SOAP 1.1 `Server`).
27    Receiver,
28}
29
30impl FaultCode {
31    /// Spec-Name als XML-QName (`env:<Code>`).
32    #[must_use]
33    pub fn qname(self) -> &'static str {
34        match self {
35            Self::VersionMismatch => "env:VersionMismatch",
36            Self::MustUnderstand => "env:MustUnderstand",
37            Self::DataEncodingUnknown => "env:DataEncodingUnknown",
38            Self::Sender => "env:Sender",
39            Self::Receiver => "env:Receiver",
40        }
41    }
42}
43
44/// SOAP 1.2 Fault.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Fault {
47    /// Fault-Code.
48    pub code: FaultCode,
49    /// Reason — kurze human-readable Beschreibung.
50    pub reason: String,
51    /// Optional Detail-XML (Subcode-/App-spezifisch).
52    pub detail: Option<String>,
53    /// Optional `xml:lang` fuer den Reason-Text (default `en`).
54    pub lang: String,
55}
56
57impl Fault {
58    /// Konstruktor mit Default-Lang `en`.
59    #[must_use]
60    pub fn new(code: FaultCode, reason: &str) -> Self {
61        Self {
62            code,
63            reason: reason.into(),
64            detail: None,
65            lang: "en".to_string(),
66        }
67    }
68
69    /// Render zu XML — wird typisch in `Envelope.body_xml` verbaut.
70    /// Spec §5.4 Skeleton.
71    #[must_use]
72    pub fn to_xml(&self) -> String {
73        let detail = match &self.detail {
74            Some(d) => format!("<env:Detail>{d}</env:Detail>"),
75            None => String::new(),
76        };
77        format!(
78            "<env:Fault xmlns:env=\"{SOAP_12_NS}\">\
79<env:Code><env:Value>{}</env:Value></env:Code>\
80<env:Reason><env:Text xml:lang=\"{}\">{}</env:Text></env:Reason>\
81{detail}\
82</env:Fault>",
83            self.code.qname(),
84            self.lang,
85            xml_escape(&self.reason)
86        )
87    }
88}
89
90fn xml_escape(s: &str) -> String {
91    s.replace('&', "&amp;")
92        .replace('<', "&lt;")
93        .replace('>', "&gt;")
94}
95
96#[cfg(test)]
97#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn fault_codes_have_correct_qnames() {
103        assert_eq!(FaultCode::Sender.qname(), "env:Sender");
104        assert_eq!(FaultCode::Receiver.qname(), "env:Receiver");
105        assert_eq!(FaultCode::VersionMismatch.qname(), "env:VersionMismatch");
106        assert_eq!(FaultCode::MustUnderstand.qname(), "env:MustUnderstand");
107        assert_eq!(
108            FaultCode::DataEncodingUnknown.qname(),
109            "env:DataEncodingUnknown"
110        );
111    }
112
113    #[test]
114    fn fault_xml_contains_required_elements() {
115        let f = Fault::new(FaultCode::Sender, "Bad input");
116        let xml = f.to_xml();
117        assert!(xml.contains("<env:Code>"));
118        assert!(xml.contains("<env:Value>env:Sender</env:Value>"));
119        assert!(xml.contains("<env:Reason>"));
120        assert!(xml.contains("<env:Text xml:lang=\"en\">Bad input</env:Text>"));
121    }
122
123    #[test]
124    fn fault_with_detail_includes_detail_block() {
125        let mut f = Fault::new(FaultCode::Receiver, "Server error");
126        f.detail = Some("<x:Cause>db down</x:Cause>".into());
127        let xml = f.to_xml();
128        assert!(xml.contains("<env:Detail><x:Cause>db down</x:Cause></env:Detail>"));
129    }
130
131    #[test]
132    fn fault_reason_is_xml_escaped() {
133        let f = Fault::new(FaultCode::Sender, "<bad> & worse");
134        let xml = f.to_xml();
135        assert!(xml.contains("&lt;bad&gt; &amp; worse"));
136        assert!(!xml.contains("<bad>"));
137    }
138
139    #[test]
140    fn fault_default_lang_is_en() {
141        let f = Fault::new(FaultCode::Sender, "x");
142        assert_eq!(f.lang, "en");
143        assert!(f.to_xml().contains("xml:lang=\"en\""));
144    }
145
146    #[test]
147    fn custom_lang_is_emitted() {
148        let mut f = Fault::new(FaultCode::Sender, "x");
149        f.lang = "de".into();
150        assert!(f.to_xml().contains("xml:lang=\"de\""));
151    }
152}