1use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12
13use zerodds_xml_wire::emitter::{EmitError, XmlEmitter};
14use zerodds_xml_wire::parser::{Event, ParseError, XmlParser};
15
16pub const SOAP_12_NS: &str = "http://www.w3.org/2003/05/soap-envelope";
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
21pub struct Envelope {
22 pub header_xml: Option<String>,
24 pub body_xml: String,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum EnvelopeError {
31 Parse(ParseError),
33 Emit(EmitError),
35 NoEnvelope,
37 NoBody,
39}
40
41impl core::fmt::Display for EnvelopeError {
42 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
43 match self {
44 Self::Parse(e) => write!(f, "parse: {e}"),
45 Self::Emit(e) => write!(f, "emit: {e}"),
46 Self::NoEnvelope => f.write_str("no <soap:Envelope> root"),
47 Self::NoBody => f.write_str("no <soap:Body>"),
48 }
49 }
50}
51
52#[cfg(feature = "std")]
53impl std::error::Error for EnvelopeError {}
54
55impl From<ParseError> for EnvelopeError {
56 fn from(e: ParseError) -> Self {
57 Self::Parse(e)
58 }
59}
60impl From<EmitError> for EnvelopeError {
61 fn from(e: EmitError) -> Self {
62 Self::Emit(e)
63 }
64}
65
66pub fn build_envelope(env: &Envelope) -> Result<String, EnvelopeError> {
71 let mut e = XmlEmitter::new();
72 e.declaration();
73 e.start_element("soap:Envelope", &[("xmlns:soap", SOAP_12_NS)])?;
74 if let Some(h) = &env.header_xml {
75 e.start_element("soap:Header", &[])?;
78 let mut prefix = e.finish();
81 prefix.push_str(h);
82 prefix.push_str("</soap:Header>");
83 let mut e2 = XmlEmitter::new();
84 e2.start_element("soap:Body", &[])?;
85 prefix.push_str(&e2.finish());
86 prefix.push_str(&env.body_xml);
87 prefix.push_str("</soap:Body></soap:Envelope>");
88 return Ok(prefix);
89 }
90 e.start_element("soap:Body", &[])?;
92 let mut out = e.finish();
93 out.push_str(&env.body_xml);
94 out.push_str("</soap:Body></soap:Envelope>");
95 Ok(out)
96}
97
98pub fn parse_envelope(xml: &str) -> Result<Envelope, EnvelopeError> {
103 let env_open = xml
105 .find("<soap:Envelope")
106 .ok_or(EnvelopeError::NoEnvelope)?;
107 let env_inner_start =
108 xml[env_open..].find('>').ok_or(EnvelopeError::NoEnvelope)? + env_open + 1;
109 let env_close = xml
110 .rfind("</soap:Envelope>")
111 .ok_or(EnvelopeError::NoEnvelope)?;
112 let inner = &xml[env_inner_start..env_close];
113
114 let header_xml = inner_block(inner, "soap:Header");
116 let body_xml = inner_block(inner, "soap:Body").ok_or(EnvelopeError::NoBody)?;
117
118 let _ = XmlParser::new(&body_xml).collect::<Result<Vec<Event>, _>>();
122
123 Ok(Envelope {
124 header_xml,
125 body_xml,
126 })
127}
128
129fn inner_block(input: &str, tag: &str) -> Option<String> {
130 let open_marker = format!("<{tag}");
131 let close_marker = format!("</{tag}>");
132 let open = input.find(&open_marker)?;
133 let inner_start = input[open..].find('>')? + open + 1;
134 let close = input.find(&close_marker)?;
135 if close <= inner_start {
136 return None;
137 }
138 Some(input[inner_start..close].trim().to_string())
139}
140
141#[cfg(test)]
142#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn build_simple_body_only() {
148 let env = Envelope {
149 header_xml: None,
150 body_xml: "<m:Get xmlns:m=\"x\"/>".into(),
151 };
152 let out = build_envelope(&env).unwrap();
153 assert!(out.contains("<soap:Envelope"));
154 assert!(out.contains("<soap:Body>"));
155 assert!(out.contains("<m:Get"));
156 assert!(out.ends_with("</soap:Envelope>"));
157 }
158
159 #[test]
160 fn build_with_header() {
161 let env = Envelope {
162 header_xml: Some("<h:Token xmlns:h=\"y\"/>".into()),
163 body_xml: "<m:Op/>".into(),
164 };
165 let out = build_envelope(&env).unwrap();
166 assert!(out.contains("<soap:Header>"));
167 assert!(out.contains("<h:Token"));
168 assert!(out.contains("<soap:Body>"));
169 }
170
171 #[test]
172 fn parse_round_trip() {
173 let env = Envelope {
174 header_xml: Some("<h:Token xmlns:h=\"y\"/>".into()),
175 body_xml: "<m:Op xmlns:m=\"x\"/>".into(),
176 };
177 let xml = build_envelope(&env).unwrap();
178 let parsed = parse_envelope(&xml).unwrap();
179 assert!(parsed.body_xml.contains("m:Op"));
180 assert!(parsed.header_xml.unwrap().contains("h:Token"));
181 }
182
183 #[test]
184 fn parse_missing_envelope_rejected() {
185 let xml = "<foo/>";
186 assert!(matches!(
187 parse_envelope(xml),
188 Err(EnvelopeError::NoEnvelope)
189 ));
190 }
191
192 #[test]
193 fn parse_missing_body_rejected() {
194 let xml = format!("<soap:Envelope xmlns:soap=\"{SOAP_12_NS}\"></soap:Envelope>");
195 assert!(matches!(parse_envelope(&xml), Err(EnvelopeError::NoBody)));
196 }
197
198 #[test]
199 fn build_includes_namespace_declaration() {
200 let env = Envelope {
201 header_xml: None,
202 body_xml: String::new(),
203 };
204 let out = build_envelope(&env).unwrap();
205 assert!(out.contains(SOAP_12_NS));
206 }
207}