use crate::MetadataClient;
use crate::envelope::xml_escape;
use crate::error::{MetadataError, MetadataResult};
use crate::result::{DeleteResult, SaveResult, UpsertResult};
use crate::transport::SoapOperation;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use std::marker::PhantomData;
pub const MAX_CRUD_COMPONENTS_PER_CALL: usize = 10;
fn render_metadata_components<S: AsRef<str>>(type_name: &str, components: &[S], out: &mut String) {
for component in components {
out.push_str(r#"<met:metadata xsi:type="met:"#);
out.push_str(&xml_escape(type_name));
out.push_str(r#"" xmlns="http://soap.sforce.com/2006/04/metadata">"#);
out.push_str(component.as_ref());
out.push_str("</met:metadata>");
}
}
fn render_type_and_full_names<S: AsRef<str>>(type_name: &str, full_names: &[S], out: &mut String) {
out.push_str("<met:type>");
out.push_str(&xml_escape(type_name));
out.push_str("</met:type>");
for name in full_names {
out.push_str("<met:fullNames>");
out.push_str(&xml_escape(name.as_ref()));
out.push_str("</met:fullNames>");
}
}
fn check_component_cap(count: usize, op_label: &str) -> MetadataResult<()> {
if count == 0 {
return Err(MetadataError::InvalidArgument(format!(
"{op_label} requires at least one component; got 0"
)));
}
if count > MAX_CRUD_COMPONENTS_PER_CALL {
return Err(MetadataError::InvalidArgument(format!(
"{op_label} accepts at most {MAX_CRUD_COMPONENTS_PER_CALL} components per call; \
got {count}"
)));
}
Ok(())
}
struct CreateMetadataOp<'a, S: AsRef<str>> {
type_name: &'a str,
components: &'a [S],
}
#[derive(Deserialize)]
struct SaveResultsWire {
#[serde(default, rename = "result")]
results: Vec<SaveResult>,
}
impl<S: AsRef<str>> SoapOperation for CreateMetadataOp<'_, S> {
const NAME: &'static str = "createMetadata";
type Response = SaveResultsWire;
fn render_body(&self) -> MetadataResult<String> {
let mut out = String::with_capacity(self.components.len() * 256);
render_metadata_components(self.type_name, self.components, &mut out);
Ok(out)
}
}
struct UpdateMetadataOp<'a, S: AsRef<str>> {
type_name: &'a str,
components: &'a [S],
}
impl<S: AsRef<str>> SoapOperation for UpdateMetadataOp<'_, S> {
const NAME: &'static str = "updateMetadata";
type Response = SaveResultsWire;
fn render_body(&self) -> MetadataResult<String> {
let mut out = String::with_capacity(self.components.len() * 256);
render_metadata_components(self.type_name, self.components, &mut out);
Ok(out)
}
}
struct UpsertMetadataOp<'a, S: AsRef<str>> {
type_name: &'a str,
components: &'a [S],
}
#[derive(Deserialize)]
struct UpsertResultsWire {
#[serde(default, rename = "result")]
results: Vec<UpsertResult>,
}
impl<S: AsRef<str>> SoapOperation for UpsertMetadataOp<'_, S> {
const NAME: &'static str = "upsertMetadata";
type Response = UpsertResultsWire;
fn render_body(&self) -> MetadataResult<String> {
let mut out = String::with_capacity(self.components.len() * 256);
render_metadata_components(self.type_name, self.components, &mut out);
Ok(out)
}
}
struct DeleteMetadataOp<'a, S: AsRef<str>> {
type_name: &'a str,
full_names: &'a [S],
}
#[derive(Deserialize)]
struct DeleteResultsWire {
#[serde(default, rename = "result")]
results: Vec<DeleteResult>,
}
impl<S: AsRef<str>> SoapOperation for DeleteMetadataOp<'_, S> {
const NAME: &'static str = "deleteMetadata";
type Response = DeleteResultsWire;
fn render_body(&self) -> MetadataResult<String> {
let mut out = String::with_capacity(64 + self.full_names.len() * 64);
render_type_and_full_names(self.type_name, self.full_names, &mut out);
Ok(out)
}
}
struct ReadMetadataOp<'a, T, S: AsRef<str>> {
type_name: &'a str,
full_names: &'a [S],
_marker: PhantomData<fn() -> T>,
}
#[derive(Deserialize)]
#[serde(bound(deserialize = "T: serde::de::DeserializeOwned"))]
struct ReadMetadataResponseWire<T> {
result: ReadResultWire<T>,
}
#[derive(Deserialize)]
#[serde(bound(deserialize = "T: serde::de::DeserializeOwned"))]
struct ReadResultWire<T> {
#[serde(default = "Vec::new")]
records: Vec<T>,
}
impl<T, S> SoapOperation for ReadMetadataOp<'_, T, S>
where
T: DeserializeOwned,
S: AsRef<str>,
{
const NAME: &'static str = "readMetadata";
type Response = ReadMetadataResponseWire<T>;
fn render_body(&self) -> MetadataResult<String> {
let mut out = String::with_capacity(64 + self.full_names.len() * 64);
render_type_and_full_names(self.type_name, self.full_names, &mut out);
Ok(out)
}
}
struct RenameMetadataOp<'a> {
type_name: &'a str,
old_full_name: &'a str,
new_full_name: &'a str,
}
#[derive(Deserialize)]
struct RenameMetadataResponseWire {
result: SaveResult,
}
impl SoapOperation for RenameMetadataOp<'_> {
const NAME: &'static str = "renameMetadata";
type Response = RenameMetadataResponseWire;
fn render_body(&self) -> MetadataResult<String> {
Ok(format!(
"<met:type>{}</met:type>\
<met:oldFullName>{}</met:oldFullName>\
<met:newFullName>{}</met:newFullName>",
xml_escape(self.type_name),
xml_escape(self.old_full_name),
xml_escape(self.new_full_name),
))
}
}
impl MetadataClient {
pub async fn create_metadata<S: AsRef<str>>(
&self,
type_name: &str,
components: &[S],
) -> MetadataResult<Vec<SaveResult>> {
check_component_cap(components.len(), "create_metadata")?;
let op = CreateMetadataOp {
type_name,
components,
};
let resp = self.call(&op).await?;
Ok(resp.results)
}
pub async fn update_metadata<S: AsRef<str>>(
&self,
type_name: &str,
components: &[S],
) -> MetadataResult<Vec<SaveResult>> {
check_component_cap(components.len(), "update_metadata")?;
let op = UpdateMetadataOp {
type_name,
components,
};
let resp = self.call(&op).await?;
Ok(resp.results)
}
pub async fn upsert_metadata<S: AsRef<str>>(
&self,
type_name: &str,
components: &[S],
) -> MetadataResult<Vec<UpsertResult>> {
check_component_cap(components.len(), "upsert_metadata")?;
let op = UpsertMetadataOp {
type_name,
components,
};
let resp = self.call(&op).await?;
Ok(resp.results)
}
pub async fn delete_metadata<S: AsRef<str>>(
&self,
type_name: &str,
full_names: &[S],
) -> MetadataResult<Vec<DeleteResult>> {
check_component_cap(full_names.len(), "delete_metadata")?;
let op = DeleteMetadataOp {
type_name,
full_names,
};
let resp = self.call(&op).await?;
Ok(resp.results)
}
pub async fn read_metadata<T, S>(
&self,
type_name: &str,
full_names: &[S],
) -> MetadataResult<Vec<T>>
where
T: DeserializeOwned,
S: AsRef<str>,
{
check_component_cap(full_names.len(), "read_metadata")?;
let op = ReadMetadataOp::<T, S> {
type_name,
full_names,
_marker: PhantomData,
};
let resp = self.call(&op).await?;
Ok(resp.result.records)
}
pub async fn rename_metadata(
&self,
type_name: &str,
old_full_name: &str,
new_full_name: &str,
) -> MetadataResult<SaveResult> {
let op = RenameMetadataOp {
type_name,
old_full_name,
new_full_name,
};
let resp = self.call(&op).await?;
Ok(resp.result)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn create_op_wraps_components_with_xsi_type_and_default_ns() {
let op = CreateMetadataOp {
type_name: "ApexClass",
components: &["<fullName>Foo</fullName>"],
};
let body = op.render_body().unwrap();
assert!(body.contains(r#"<met:metadata xsi:type="met:ApexClass""#));
assert!(body.contains(r#"xmlns="http://soap.sforce.com/2006/04/metadata""#));
assert!(body.contains("<fullName>Foo</fullName>"));
assert!(body.contains("</met:metadata>"));
}
#[test]
fn create_op_emits_one_wrapper_per_component() {
let op = CreateMetadataOp {
type_name: "ApexClass",
components: &["<fullName>A</fullName>", "<fullName>B</fullName>"],
};
let body = op.render_body().unwrap();
assert_eq!(
body.matches(r#"<met:metadata xsi:type="met:ApexClass""#)
.count(),
2
);
assert_eq!(body.matches("</met:metadata>").count(), 2);
}
#[test]
fn read_op_emits_type_and_full_names() {
#[derive(Deserialize)]
struct Empty {}
let op = ReadMetadataOp::<Empty, _> {
type_name: "ApexClass",
full_names: &["Foo", "Bar"],
_marker: PhantomData,
};
let body = op.render_body().unwrap();
assert_eq!(
body,
"<met:type>ApexClass</met:type>\
<met:fullNames>Foo</met:fullNames>\
<met:fullNames>Bar</met:fullNames>"
);
}
#[test]
fn delete_op_shares_body_shape_with_read() {
let op = DeleteMetadataOp {
type_name: "ApexTrigger",
full_names: &["AccountTrigger"],
};
let body = op.render_body().unwrap();
assert_eq!(
body,
"<met:type>ApexTrigger</met:type>\
<met:fullNames>AccountTrigger</met:fullNames>"
);
}
#[test]
fn rename_op_emits_type_and_both_full_names() {
let op = RenameMetadataOp {
type_name: "ApexClass",
old_full_name: "OldName",
new_full_name: "NewName",
};
let body = op.render_body().unwrap();
assert_eq!(
body,
"<met:type>ApexClass</met:type>\
<met:oldFullName>OldName</met:oldFullName>\
<met:newFullName>NewName</met:newFullName>"
);
}
#[test]
fn render_escapes_special_chars_in_type_and_names() {
let op = DeleteMetadataOp {
type_name: "Weird<>",
full_names: &["a&b"],
};
let body = op.render_body().unwrap();
assert!(body.contains("<met:type>Weird<></met:type>"));
assert!(body.contains("<met:fullNames>a&b</met:fullNames>"));
}
#[test]
fn check_component_cap_rejects_empty_input() {
let err = check_component_cap(0, "create_metadata").unwrap_err();
assert!(err.to_string().contains("at least one"));
}
#[test]
fn check_component_cap_rejects_more_than_ten() {
let err = check_component_cap(11, "delete_metadata").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("10"));
assert!(msg.contains("11"));
}
#[test]
fn check_component_cap_accepts_one_to_ten() {
for n in 1..=10 {
assert!(check_component_cap(n, "x").is_ok(), "should accept {n}");
}
}
}