230 lines
6.5 KiB
Rust
230 lines
6.5 KiB
Rust
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#"<soap:Envelope xmlns:soap="{envelope_namespace}">"#
|
|
));
|
|
xml.push_str("<soap:Body>");
|
|
match namespace {
|
|
Some(namespace) => xml.push_str(&format!(r#"<m:{operation_name}" xmlns:m="{namespace}">"#)),
|
|
None => xml.push_str(&format!("<{operation_name}>")),
|
|
}
|
|
append_value_children(&mut xml, body);
|
|
match namespace {
|
|
Some(_) => xml.push_str(&format!("</m:{operation_name}>")),
|
|
None => xml.push_str(&format!("</{operation_name}>")),
|
|
}
|
|
xml.push_str("</soap:Body></soap:Envelope>");
|
|
xml
|
|
}
|
|
|
|
pub fn decode_envelope(payload: &str) -> Result<Value, SoapAdapterError> {
|
|
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<String> {
|
|
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("</");
|
|
xml.push_str(name);
|
|
xml.push('>');
|
|
}
|
|
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("</");
|
|
xml.push_str(name);
|
|
xml.push('>');
|
|
}
|
|
Value::Number(value) => {
|
|
xml.push('<');
|
|
xml.push_str(name);
|
|
xml.push('>');
|
|
xml.push_str(&value.to_string());
|
|
xml.push_str("</");
|
|
xml.push_str(name);
|
|
xml.push('>');
|
|
}
|
|
Value::String(value) => {
|
|
xml.push('<');
|
|
xml.push_str(name);
|
|
xml.push('>');
|
|
xml.push_str(&escape_xml(value));
|
|
xml.push_str("</");
|
|
xml.push_str(name);
|
|
xml.push('>');
|
|
}
|
|
}
|
|
}
|
|
|
|
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::<Vec<_>>();
|
|
|
|
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("<soap:Envelope"));
|
|
assert!(xml.contains("<m:CreateLead"));
|
|
assert!(xml.contains(r#"xmlns:m="urn:crm""#));
|
|
assert!(xml.contains("<email>user@example.com</email>"));
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_wrapped_response_body() {
|
|
let value = decode_envelope(
|
|
r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<soap:Body>
|
|
<CreateLeadResponse>
|
|
<id>lead_123</id>
|
|
<status>created</status>
|
|
</CreateLeadResponse>
|
|
</soap:Body>
|
|
</soap:Envelope>"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(value["id"], "lead_123");
|
|
assert_eq!(value["status"], "created");
|
|
}
|
|
}
|