use serde::Deserialize;
fn deserialize_nil_string<'de, D>(d: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(d)?;
Ok(opt.filter(|s| !s.is_empty()))
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AsyncResult {
pub id: String,
#[serde(default)]
pub done: bool,
#[serde(default)]
pub state: Option<AsyncRequestState>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub status_code: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum AsyncRequestState {
Queued,
InProgress,
Completed,
Error,
}
#[derive(Debug, Clone, Default)]
pub struct DeployOptions {
pub allow_missing_files: Option<bool>,
pub auto_update_package: Option<bool>,
pub check_only: Option<bool>,
pub ignore_warnings: Option<bool>,
pub perform_retrieve: Option<bool>,
pub purge_on_delete: Option<bool>,
pub rollback_on_error: Option<bool>,
pub run_tests: Vec<String>,
pub single_package: Option<bool>,
pub test_level: Option<TestLevel>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TestLevel {
NoTestRun,
RunSpecifiedTests,
RunRelevantTests,
RunLocalTests,
RunAllTestsInOrg,
}
impl TestLevel {
pub(crate) fn as_wire(&self) -> &'static str {
match self {
Self::NoTestRun => "NoTestRun",
Self::RunSpecifiedTests => "RunSpecifiedTests",
Self::RunRelevantTests => "RunRelevantTests",
Self::RunLocalTests => "RunLocalTests",
Self::RunAllTestsInOrg => "RunAllTestsInOrg",
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeployResult {
pub id: String,
#[serde(default)]
pub done: bool,
#[serde(default)]
pub success: bool,
#[serde(default)]
pub status: Option<DeployStatus>,
#[serde(default)]
pub check_only: bool,
#[serde(default)]
pub ignore_warnings: bool,
#[serde(default)]
pub rollback_on_error: bool,
#[serde(default)]
pub run_tests_enabled: bool,
#[serde(default)]
pub number_components_deployed: i32,
#[serde(default)]
pub number_components_total: i32,
#[serde(default)]
pub number_component_errors: i32,
#[serde(default)]
pub number_tests_completed: i32,
#[serde(default)]
pub number_tests_total: i32,
#[serde(default)]
pub number_test_errors: i32,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub state_detail: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub error_status_code: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub error_message: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub created_by: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub created_by_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub created_date: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub start_date: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub last_modified_date: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub completed_date: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub canceled_by: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub canceled_by_name: Option<String>,
#[serde(default)]
pub details: Option<DeployDetails>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum DeployStatus {
Pending,
InProgress,
Succeeded,
SucceededPartial,
Failed,
Canceling,
Canceled,
FinalizingDeploy,
FinalizingDeployFailed,
}
impl DeployStatus {
pub fn is_terminal(self) -> bool {
matches!(
self,
Self::Succeeded
| Self::SucceededPartial
| Self::Failed
| Self::Canceled
| Self::FinalizingDeployFailed
)
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeployDetails {
#[serde(default, rename = "componentFailures")]
pub component_failures: Vec<DeployMessage>,
#[serde(default, rename = "componentSuccesses")]
pub component_successes: Vec<DeployMessage>,
#[serde(default)]
pub run_test_result: Option<RunTestsResult>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeployMessage {
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub id: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub component_type: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub full_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub file_name: Option<String>,
#[serde(default)]
pub success: bool,
#[serde(default)]
pub changed: bool,
#[serde(default)]
pub created: bool,
#[serde(default)]
pub deleted: bool,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub created_date: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub problem: Option<String>,
#[serde(default)]
pub problem_type: Option<DeployProblemType>,
#[serde(default)]
pub line_number: Option<i32>,
#[serde(default)]
pub column_number: Option<i32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum DeployProblemType {
Warning,
Error,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunTestsResult {
#[serde(default)]
pub num_tests_run: i32,
#[serde(default)]
pub num_failures: i32,
#[serde(default)]
pub total_time: f64,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub apex_log_id: Option<String>,
#[serde(default)]
pub successes: Vec<RunTestSuccess>,
#[serde(default)]
pub failures: Vec<RunTestFailure>,
#[serde(default)]
pub code_coverage: Vec<CodeCoverageResult>,
#[serde(default)]
pub code_coverage_warnings: Vec<CodeCoverageWarning>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunTestSuccess {
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub id: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub method_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub namespace: Option<String>,
#[serde(default)]
pub time: f64,
#[serde(default)]
pub see_all_data: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunTestFailure {
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub id: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub method_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub namespace: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub message: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub stack_trace: Option<String>,
#[serde(default)]
pub time: f64,
#[serde(default)]
pub see_all_data: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeCoverageResult {
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub id: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub namespace: Option<String>,
#[serde(default)]
pub num_locations: i32,
#[serde(default)]
pub num_locations_not_covered: i32,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeCoverageWarning {
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub id: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub namespace: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelDeployResult {
pub id: String,
#[serde(default)]
pub done: bool,
}
#[derive(Debug, Clone, Default)]
pub struct RetrieveRequest {
pub api_version: String,
pub package_names: Vec<String>,
pub single_package: bool,
pub specific_files: Vec<String>,
pub unpackaged: Option<crate::PackageManifest>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetrieveResult {
pub id: String,
#[serde(default)]
pub done: bool,
#[serde(default)]
pub success: bool,
#[serde(default)]
pub status: Option<RetrieveStatus>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub error_status_code: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub error_message: Option<String>,
#[serde(default)]
pub file_properties: Vec<FileProperties>,
#[serde(default)]
pub messages: Vec<RetrieveMessage>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub zip_file: Option<String>,
}
impl RetrieveResult {
pub fn zip_bytes(&self) -> Result<Option<bytes::Bytes>, base64::DecodeError> {
use base64::Engine;
match &self.zip_file {
None => Ok(None),
Some(b64) => base64::engine::general_purpose::STANDARD
.decode(b64)
.map(|v| Some(bytes::Bytes::from(v))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum RetrieveStatus {
Pending,
InProgress,
Succeeded,
Failed,
}
impl RetrieveStatus {
pub fn is_terminal(self) -> bool {
matches!(self, Self::Succeeded | Self::Failed)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileProperties {
pub file_name: String,
pub full_name: String,
#[serde(default, rename = "type", deserialize_with = "deserialize_nil_string")]
pub type_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub id: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub created_by_id: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub created_by_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub created_date: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub last_modified_by_id: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub last_modified_by_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub last_modified_date: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub namespace_prefix: Option<String>,
#[serde(default)]
pub manageable_state: Option<ManageableState>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ManageableState {
Beta,
Deleted,
Deprecated,
DeprecatedEditable,
Installed,
InstalledEditable,
Released,
Unmanaged,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetrieveMessage {
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub file_name: Option<String>,
pub problem: String,
}
#[derive(Debug, Clone)]
pub struct ListMetadataQuery {
pub type_name: String,
pub folder: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DescribeMetadataResult {
#[serde(default)]
pub metadata_objects: Vec<DescribeMetadataObject>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub organization_namespace: Option<String>,
#[serde(default)]
pub partial_save_allowed: bool,
#[serde(default)]
pub test_required: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DescribeMetadataObject {
pub xml_name: String,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub directory_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub suffix: Option<String>,
#[serde(default)]
pub in_folder: bool,
#[serde(default)]
pub meta_file: bool,
#[serde(default)]
pub child_xml_names: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DescribeValueTypeResult {
#[serde(default)]
pub api_creatable: bool,
#[serde(default)]
pub api_deletable: bool,
#[serde(default)]
pub api_readable: bool,
#[serde(default)]
pub api_updatable: bool,
#[serde(default)]
pub parent_field: Option<ValueTypeField>,
#[serde(default)]
pub value_type_fields: Vec<ValueTypeField>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValueTypeField {
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub soap_type: Option<String>,
#[serde(default)]
pub min_occurs: i32,
#[serde(default)]
pub value_required: bool,
#[serde(default)]
pub is_name_field: bool,
#[serde(default)]
pub is_foreign_key: bool,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub foreign_key_domain: Option<String>,
#[serde(default)]
pub picklist_values: Vec<PicklistEntry>,
#[serde(default)]
pub fields: Vec<ValueTypeField>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PicklistEntry {
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub value: Option<String>,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub label: Option<String>,
#[serde(default)]
pub default_value: bool,
#[serde(default)]
pub active: bool,
#[serde(default, deserialize_with = "deserialize_nil_string")]
pub valid_for: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveResult {
#[serde(default)]
pub full_name: String,
#[serde(default)]
pub success: bool,
#[serde(default)]
pub errors: Vec<MetadataApiError>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpsertResult {
#[serde(default)]
pub full_name: String,
#[serde(default)]
pub success: bool,
#[serde(default)]
pub created: bool,
#[serde(default)]
pub errors: Vec<MetadataApiError>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteResult {
#[serde(default)]
pub full_name: String,
#[serde(default)]
pub success: bool,
#[serde(default)]
pub errors: Vec<MetadataApiError>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataApiError {
#[serde(default)]
pub status_code: String,
#[serde(default)]
pub message: String,
#[serde(default)]
pub fields: Vec<String>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn deploy_status_is_terminal_matches_completed_states() {
assert!(DeployStatus::Succeeded.is_terminal());
assert!(DeployStatus::SucceededPartial.is_terminal());
assert!(DeployStatus::Failed.is_terminal());
assert!(DeployStatus::Canceled.is_terminal());
assert!(!DeployStatus::Pending.is_terminal());
assert!(!DeployStatus::InProgress.is_terminal());
assert!(!DeployStatus::Canceling.is_terminal());
assert!(!DeployStatus::FinalizingDeploy.is_terminal());
}
#[test]
fn retrieve_status_is_terminal_matches_succeeded_or_failed() {
assert!(RetrieveStatus::Succeeded.is_terminal());
assert!(RetrieveStatus::Failed.is_terminal());
assert!(!RetrieveStatus::Pending.is_terminal());
assert!(!RetrieveStatus::InProgress.is_terminal());
}
#[test]
fn test_level_as_wire_matches_doc_strings() {
assert_eq!(TestLevel::NoTestRun.as_wire(), "NoTestRun");
assert_eq!(TestLevel::RunSpecifiedTests.as_wire(), "RunSpecifiedTests");
assert_eq!(TestLevel::RunLocalTests.as_wire(), "RunLocalTests");
assert_eq!(TestLevel::RunAllTestsInOrg.as_wire(), "RunAllTestsInOrg");
assert_eq!(TestLevel::RunRelevantTests.as_wire(), "RunRelevantTests");
}
#[test]
fn retrieve_result_decodes_zip_bytes_from_base64() {
let r = RetrieveResult {
id: "x".into(),
done: true,
success: true,
status: Some(RetrieveStatus::Succeeded),
error_status_code: None,
error_message: None,
file_properties: vec![],
messages: vec![],
zip_file: Some("aGVsbG8=".into()), };
let bytes = r.zip_bytes().unwrap().unwrap();
assert_eq!(&bytes[..], b"hello");
}
#[test]
fn retrieve_result_zip_bytes_returns_none_when_absent() {
let r = RetrieveResult {
id: "x".into(),
done: false,
success: false,
status: None,
error_status_code: None,
error_message: None,
file_properties: vec![],
messages: vec![],
zip_file: None,
};
assert!(r.zip_bytes().unwrap().is_none());
}
}