use roxmltree::{Document, Node}; use serde_json::{Map, Value}; use crate::SoapAdapterError; pub fn build_envelope( operation_name: &str, namespace: Option<&str>, body: &Value, envelope_namespace: &str, ) -> String { let mut xml = String::new(); xml.push_str(&format!( r#""# )); xml.push_str(""); match namespace { Some(namespace) => xml.push_str(&format!(r#""#)), None => xml.push_str(&format!("<{operation_name}>")), } append_value_children(&mut xml, body); match namespace { Some(_) => xml.push_str(&format!("")), None => xml.push_str(&format!("")), } xml.push_str(""); xml } pub fn decode_envelope(payload: &str) -> Result { let document = Document::parse(payload)?; let body = document .descendants() .find(|node| node.is_element() && node.tag_name().name() == "Body") .ok_or(SoapAdapterError::MissingBody)?; let body_child = body .children() .find(|node| node.is_element()) .ok_or(SoapAdapterError::EmptyBody)?; if body_child.tag_name().name() == "Fault" { return Err(parse_fault(body_child)); } Ok(node_to_json(body_child, true)) } fn parse_fault(fault: Node<'_, '_>) -> SoapAdapterError { let code = find_nested_text(fault, &["Code", "Value"]) .or_else(|| find_nested_text(fault, &["faultcode"])); let message = find_nested_text(fault, &["Reason", "Text"]) .or_else(|| find_nested_text(fault, &["faultstring"])) .unwrap_or_else(|| "SOAP fault".to_owned()); let detail = fault .children() .find(|node| node.is_element() && node.tag_name().name() == "Detail") .map(|node| node_to_json(node, false)); SoapAdapterError::SoapFault { code, message, detail, } } fn find_nested_text(node: Node<'_, '_>, path: &[&str]) -> Option { let mut current = node; for segment in path { current = current .children() .find(|child| child.is_element() && child.tag_name().name() == *segment)?; } current .text() .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn append_value_children(xml: &mut String, value: &Value) { match value { Value::Object(object) => { for (key, value) in object { append_named_value(xml, key, value); } } other => append_named_value(xml, "value", other), } } fn append_named_value(xml: &mut String, name: &str, value: &Value) { match value { Value::Array(items) => { for item in items { append_named_value(xml, name, item); } } Value::Object(object) => { xml.push('<'); xml.push_str(name); xml.push('>'); for (child_name, child_value) in object { append_named_value(xml, child_name, child_value); } xml.push_str("'); } Value::Null => { xml.push('<'); xml.push_str(name); xml.push_str("/>"); } Value::Bool(value) => { xml.push('<'); xml.push_str(name); xml.push('>'); xml.push_str(if *value { "true" } else { "false" }); xml.push_str("'); } Value::Number(value) => { xml.push('<'); xml.push_str(name); xml.push('>'); xml.push_str(&value.to_string()); xml.push_str("'); } Value::String(value) => { xml.push('<'); xml.push_str(name); xml.push('>'); xml.push_str(&escape_xml(value)); xml.push_str("'); } } } fn escape_xml(value: &str) -> String { value .replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } fn node_to_json(node: Node<'_, '_>, _unwrap_root: bool) -> Value { let children = node .children() .filter(|child| child.is_element()) .collect::>(); if children.is_empty() { return node .text() .map(str::trim) .filter(|value| !value.is_empty()) .map(|value| Value::String(value.to_owned())) .unwrap_or(Value::Null); } let mut object = Map::new(); for child in children { let key = child.tag_name().name().to_owned(); let value = node_to_json(child, false); match object.get_mut(&key) { Some(existing) => match existing { Value::Array(array) => array.push(value), other => { let previous = other.take(); *other = Value::Array(vec![previous, value]); } }, None => { object.insert(key, value); } } } Value::Object(object) } #[cfg(test)] mod tests { use serde_json::json; use super::{build_envelope, decode_envelope}; #[test] fn builds_document_literal_envelope() { let xml = build_envelope( "CreateLead", Some("urn:crm"), &json!({"email":"user@example.com","source":"mcp"}), "http://schemas.xmlsoap.org/soap/envelope/", ); assert!(xml.contains("user@example.com")); } #[test] fn decodes_wrapped_response_body() { let value = decode_envelope( r#" lead_123 created "#, ) .unwrap(); assert_eq!(value["id"], "lead_123"); assert_eq!(value["status"], "created"); } }