use super::types::{
DeleteResult, DescribeGlobalResult, DescribeGlobalSObject, DescribeSObjectResult,
FieldDescribe, PicklistEntry, QueryResult, SObject, SaveResult, SearchResult, SoapError,
UpsertResult, UserInfo,
};
use crate::error::{ForceError, Result};
use quick_xml::Reader;
use quick_xml::events::{BytesRef, BytesStart, Event};
#[derive(Debug, Default)]
pub struct Node {
pub name: String,
pub text: String,
pub nil: bool,
pub children: Vec<Self>,
}
impl Node {
pub fn text_owned(&self) -> String {
self.text.trim().to_string()
}
pub fn find_child(&self, name: &str) -> Option<&Self> {
self.children.iter().find(|c| c.name == name)
}
pub fn child_text(&self, name: &str) -> Option<String> {
self.find_child(name).map(Self::text_owned)
}
fn child_bool(&self, name: &str) -> Option<bool> {
self.child_text(name).and_then(|t| match t.as_str() {
"true" => Some(true),
"false" => Some(false),
_ => None,
})
}
fn child_i64(&self, name: &str) -> Option<i64> {
self.child_text(name).and_then(|t| t.parse().ok())
}
fn children_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Self> {
self.children.iter().filter(move |c| c.name == name)
}
pub fn find_descendant(&self, name: &str) -> Option<&Self> {
if self.name == name {
return Some(self);
}
for child in &self.children {
if let Some(found) = child.find_descendant(name) {
return Some(found);
}
}
None
}
}
fn node_from_start(start: &BytesStart<'_>) -> Node {
let name = String::from_utf8_lossy(start.local_name().as_ref()).into_owned();
let mut nil = false;
for attr in start.attributes().flatten() {
if attr.key.local_name().as_ref() == b"nil" && attr.value.as_ref() == b"true" {
nil = true;
}
}
Node {
name,
nil,
..Node::default()
}
}
fn resolve_ref(reference: &BytesRef<'_>) -> String {
if let Ok(Some(ch)) = reference.resolve_char_ref() {
return ch.to_string();
}
if let Ok(name) = reference.decode() {
if let Some(resolved) = quick_xml::escape::resolve_predefined_entity(&name) {
return resolved.to_string();
}
}
String::new()
}
pub fn parse_document(xml: &str) -> Result<Node> {
let mut reader = Reader::from_str(xml);
let mut stack: Vec<Node> = vec![Node::default()];
loop {
match reader.read_event() {
Ok(Event::Start(start)) => stack.push(node_from_start(&start)),
Ok(Event::Empty(start)) => {
let node = node_from_start(&start);
if let Some(parent) = stack.last_mut() {
parent.children.push(node);
}
}
Ok(Event::Text(text)) => {
if let (Some(parent), Ok(decoded)) = (stack.last_mut(), text.decode()) {
parent.text.push_str(&decoded);
}
}
Ok(Event::CData(cdata)) => {
if let (Some(parent), Ok(decoded)) = (stack.last_mut(), cdata.decode()) {
parent.text.push_str(&decoded);
}
}
Ok(Event::GeneralRef(reference)) => {
if let Some(parent) = stack.last_mut() {
parent.text.push_str(&resolve_ref(&reference));
}
}
Ok(Event::End(_)) => {
if stack.len() > 1 {
if let Some(node) = stack.pop() {
if let Some(parent) = stack.last_mut() {
parent.children.push(node);
}
}
}
}
Ok(Event::Eof) => break,
Err(err) => {
return Err(ForceError::InvalidInput(format!(
"failed to parse SOAP response XML: {err}"
)));
}
Ok(_) => {}
}
}
stack
.into_iter()
.next()
.ok_or_else(|| ForceError::InvalidInput("empty SOAP response".to_string()))
}
fn operation_result_nodes(root: &Node) -> Vec<&Node> {
let Some(body) = root.find_descendant("Body") else {
return Vec::new();
};
let Some(response) = body.children.iter().find(|c| !c.name.is_empty()) else {
return Vec::new();
};
response.children_named("result").collect()
}
fn parse_error(node: &Node) -> SoapError {
SoapError {
status_code: node.child_text("statusCode").unwrap_or_default(),
message: node.child_text("message").unwrap_or_default(),
fields: node
.children_named("fields")
.map(Node::text_owned)
.collect(),
}
}
fn parse_record(node: &Node) -> SObject {
let mut obj = SObject::new(String::new());
let mut seen_id = false;
for child in &node.children {
if child.name == "type" {
obj.sobject_type = child.text_owned();
continue;
}
if child.name == "Id" {
if seen_id {
continue;
}
seen_id = true;
}
if child.children.iter().any(|c| !c.name.is_empty()) {
continue;
}
let value = if child.nil {
None
} else {
Some(child.text_owned())
};
obj.fields.push((child.name.clone(), value));
}
obj
}
pub fn parse_save_results(xml: &str) -> Result<Vec<SaveResult>> {
let root = parse_document(xml)?;
Ok(operation_result_nodes(&root)
.into_iter()
.map(|node| SaveResult {
id: node.child_text("id").filter(|s| !s.is_empty()),
success: node.child_bool("success").unwrap_or(false),
errors: node.children_named("errors").map(parse_error).collect(),
})
.collect())
}
pub fn parse_upsert_results(xml: &str) -> Result<Vec<UpsertResult>> {
let root = parse_document(xml)?;
Ok(operation_result_nodes(&root)
.into_iter()
.map(|node| UpsertResult {
created: node.child_bool("created").unwrap_or(false),
id: node.child_text("id").filter(|s| !s.is_empty()),
success: node.child_bool("success").unwrap_or(false),
errors: node.children_named("errors").map(parse_error).collect(),
})
.collect())
}
pub fn parse_delete_results(xml: &str) -> Result<Vec<DeleteResult>> {
let root = parse_document(xml)?;
Ok(operation_result_nodes(&root)
.into_iter()
.map(|node| DeleteResult {
id: node.child_text("id").filter(|s| !s.is_empty()),
success: node.child_bool("success").unwrap_or(false),
errors: node.children_named("errors").map(parse_error).collect(),
})
.collect())
}
pub fn parse_retrieve(xml: &str) -> Result<Vec<SObject>> {
let root = parse_document(xml)?;
Ok(operation_result_nodes(&root)
.into_iter()
.filter(|node| !node.nil)
.map(parse_record)
.collect())
}
pub fn parse_retrieve_optional(xml: &str) -> Result<Vec<Option<SObject>>> {
let root = parse_document(xml)?;
Ok(operation_result_nodes(&root)
.into_iter()
.map(|node| {
if node.nil {
None
} else {
Some(parse_record(node))
}
})
.collect())
}
pub fn parse_query_result(xml: &str) -> Result<QueryResult> {
let root = parse_document(xml)?;
let results = operation_result_nodes(&root);
let Some(result) = results.into_iter().next() else {
return Ok(QueryResult {
done: true,
query_locator: None,
size: 0,
records: Vec::new(),
});
};
Ok(QueryResult {
done: result.child_bool("done").unwrap_or(true),
query_locator: result.child_text("queryLocator").filter(|s| !s.is_empty()),
size: result.child_i64("size").unwrap_or(0),
records: result.children_named("records").map(parse_record).collect(),
})
}
pub fn parse_search_result(xml: &str) -> Result<SearchResult> {
let root = parse_document(xml)?;
let results = operation_result_nodes(&root);
let Some(result) = results.into_iter().next() else {
return Ok(SearchResult::default());
};
let records = result
.children_named("searchRecords")
.map(|sr| {
sr.find_child("record")
.map_or_else(|| parse_record(sr), parse_record)
})
.collect();
Ok(SearchResult { records })
}
fn parse_picklist_entry(node: &Node) -> PicklistEntry {
PicklistEntry {
value: node.child_text("value").unwrap_or_default(),
label: node.child_text("label"),
active: node.child_bool("active"),
default_value: node.child_bool("defaultValue"),
}
}
fn parse_field_describe(node: &Node) -> FieldDescribe {
FieldDescribe {
name: node.child_text("name").unwrap_or_default(),
label: node.child_text("label"),
field_type: node.child_text("type"),
length: node.child_i64("length"),
nillable: node.child_bool("nillable"),
custom: node.child_bool("custom"),
picklist_values: node
.children_named("picklistValues")
.map(parse_picklist_entry)
.collect(),
}
}
pub fn parse_describe_sobject(xml: &str) -> Result<DescribeSObjectResult> {
let root = parse_document(xml)?;
let results = operation_result_nodes(&root);
let result = results
.into_iter()
.next()
.ok_or_else(|| ForceError::InvalidInput("missing describeSObject result".to_string()))?;
Ok(describe_sobject_from_node(result))
}
pub fn parse_describe_sobjects(xml: &str) -> Result<Vec<DescribeSObjectResult>> {
let root = parse_document(xml)?;
Ok(operation_result_nodes(&root)
.into_iter()
.map(describe_sobject_from_node)
.collect())
}
fn describe_sobject_from_node(result: &Node) -> DescribeSObjectResult {
DescribeSObjectResult {
name: result.child_text("name").unwrap_or_default(),
label: result.child_text("label"),
label_plural: result.child_text("labelPlural"),
key_prefix: result.child_text("keyPrefix").filter(|s| !s.is_empty()),
custom: result.child_bool("custom").unwrap_or(false),
createable: result.child_bool("createable").unwrap_or(false),
updateable: result.child_bool("updateable").unwrap_or(false),
deletable: result.child_bool("deletable").unwrap_or(false),
queryable: result.child_bool("queryable").unwrap_or(false),
fields: result
.children_named("fields")
.map(parse_field_describe)
.collect(),
}
}
pub fn parse_describe_global(xml: &str) -> Result<DescribeGlobalResult> {
let root = parse_document(xml)?;
let results = operation_result_nodes(&root);
let Some(result) = results.into_iter().next() else {
return Ok(DescribeGlobalResult::default());
};
Ok(DescribeGlobalResult {
encoding: result.child_text("encoding"),
max_batch_size: result.child_i64("maxBatchSize"),
sobjects: result
.children_named("sobjects")
.map(|node| DescribeGlobalSObject {
name: node.child_text("name").unwrap_or_default(),
label: node.child_text("label"),
key_prefix: node.child_text("keyPrefix").filter(|s| !s.is_empty()),
custom: node.child_bool("custom").unwrap_or(false),
createable: node.child_bool("createable").unwrap_or(false),
queryable: node.child_bool("queryable").unwrap_or(false),
updateable: node.child_bool("updateable").unwrap_or(false),
deletable: node.child_bool("deletable").unwrap_or(false),
})
.collect(),
})
}
pub fn parse_user_info(xml: &str) -> Result<UserInfo> {
let root = parse_document(xml)?;
let results = operation_result_nodes(&root);
let result = results
.into_iter()
.next()
.ok_or_else(|| ForceError::InvalidInput("missing getUserInfo result".to_string()))?;
Ok(UserInfo {
user_id: result.child_text("userId").unwrap_or_default(),
user_full_name: result.child_text("userFullName").unwrap_or_default(),
user_email: result.child_text("userEmail").unwrap_or_default(),
user_name: result.child_text("userName").unwrap_or_default(),
organization_id: result.child_text("organizationId").unwrap_or_default(),
organization_name: result.child_text("organizationName").unwrap_or_default(),
profile_id: result.child_text("profileId").filter(|s| !s.is_empty()),
role_id: result.child_text("roleId").filter(|s| !s.is_empty()),
session_seconds_valid: result.child_i64("sessionSecondsValid"),
user_default_currency_iso_code: result.child_text("userDefaultCurrencyIsoCode"),
user_language: result.child_text("userLanguage"),
user_locale: result.child_text("userLocale"),
user_time_zone: result.child_text("userTimeZone"),
user_type: result.child_text("userType"),
currency_symbol: result.child_text("currencySymbol"),
org_default_currency_iso_code: result.child_text("orgDefaultCurrencyIsoCode"),
org_disallow_html_attachments: result.child_bool("orgDisallowHtmlAttachments"),
org_has_person_accounts: result.child_bool("orgHasPersonAccounts"),
accessibility_mode: result.child_bool("accessibilityMode"),
})
}
pub fn parse_server_timestamp(xml: &str) -> Result<String> {
let root = parse_document(xml)?;
let results = operation_result_nodes(&root);
results
.into_iter()
.next()
.and_then(|result| result.child_text("timestamp"))
.filter(|s| !s.is_empty())
.ok_or_else(|| ForceError::InvalidInput("missing server timestamp".to_string()))
}