1use alloc::format;
11use alloc::string::{String, ToString};
12
13use crate::envelope::SOAP_12_NS;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum FaultCode {
18 VersionMismatch,
20 MustUnderstand,
22 DataEncodingUnknown,
24 Sender,
26 Receiver,
28}
29
30impl FaultCode {
31 #[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#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Fault {
47 pub code: FaultCode,
49 pub reason: String,
51 pub detail: Option<String>,
53 pub lang: String,
55}
56
57impl Fault {
58 #[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 #[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('&', "&")
92 .replace('<', "<")
93 .replace('>', ">")
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("<bad> & 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}