1use serde::Deserialize;
26
27fn deserialize_nil_string<'de, D>(d: D) -> Result<Option<String>, D::Error>
41where
42 D: serde::Deserializer<'de>,
43{
44 let opt: Option<String> = Option::deserialize(d)?;
45 Ok(opt.filter(|s| !s.is_empty()))
46}
47
48#[derive(Debug, Clone, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct AsyncResult {
58 pub id: String,
61 #[serde(default)]
64 pub done: bool,
65 #[serde(default)]
67 pub state: Option<AsyncRequestState>,
68 #[serde(default, deserialize_with = "deserialize_nil_string")]
70 pub status_code: Option<String>,
71 #[serde(default, deserialize_with = "deserialize_nil_string")]
73 pub message: Option<String>,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
78pub enum AsyncRequestState {
79 Queued,
80 InProgress,
81 Completed,
82 Error,
83 #[serde(other)]
86 Unknown,
87}
88
89#[derive(Debug, Clone, Default)]
102pub struct DeployOptions {
103 pub allow_missing_files: Option<bool>,
107 pub auto_update_package: Option<bool>,
109 pub check_only: Option<bool>,
114 pub ignore_warnings: Option<bool>,
116 pub perform_retrieve: Option<bool>,
118 pub purge_on_delete: Option<bool>,
121 pub rollback_on_error: Option<bool>,
124 pub run_tests: Vec<String>,
127 pub single_package: Option<bool>,
129 pub test_level: Option<TestLevel>,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum TestLevel {
136 NoTestRun,
138 RunSpecifiedTests,
140 RunRelevantTests,
142 RunLocalTests,
145 RunAllTestsInOrg,
147}
148
149impl TestLevel {
150 pub(crate) fn as_wire(&self) -> &'static str {
151 match self {
152 Self::NoTestRun => "NoTestRun",
153 Self::RunSpecifiedTests => "RunSpecifiedTests",
154 Self::RunRelevantTests => "RunRelevantTests",
155 Self::RunLocalTests => "RunLocalTests",
156 Self::RunAllTestsInOrg => "RunAllTestsInOrg",
157 }
158 }
159}
160
161#[derive(Debug, Clone, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct DeployResult {
166 pub id: String,
167 #[serde(default)]
170 pub done: bool,
171 #[serde(default)]
173 pub success: bool,
174 #[serde(default)]
175 pub status: Option<DeployStatus>,
176 #[serde(default)]
177 pub check_only: bool,
178 #[serde(default)]
179 pub ignore_warnings: bool,
180 #[serde(default)]
181 pub rollback_on_error: bool,
182 #[serde(default)]
184 pub run_tests_enabled: bool,
185
186 #[serde(default)]
187 pub number_components_deployed: i32,
188 #[serde(default)]
189 pub number_components_total: i32,
190 #[serde(default)]
191 pub number_component_errors: i32,
192 #[serde(default)]
193 pub number_tests_completed: i32,
194 #[serde(default)]
195 pub number_tests_total: i32,
196 #[serde(default)]
197 pub number_test_errors: i32,
198
199 #[serde(default, deserialize_with = "deserialize_nil_string")]
202 pub state_detail: Option<String>,
203
204 #[serde(default, deserialize_with = "deserialize_nil_string")]
205 pub error_status_code: Option<String>,
206 #[serde(default, deserialize_with = "deserialize_nil_string")]
207 pub error_message: Option<String>,
208
209 #[serde(default, deserialize_with = "deserialize_nil_string")]
210 pub created_by: Option<String>,
211 #[serde(default, deserialize_with = "deserialize_nil_string")]
212 pub created_by_name: Option<String>,
213 #[serde(default, deserialize_with = "deserialize_nil_string")]
214 pub created_date: Option<String>,
215 #[serde(default, deserialize_with = "deserialize_nil_string")]
216 pub start_date: Option<String>,
217 #[serde(default, deserialize_with = "deserialize_nil_string")]
218 pub last_modified_date: Option<String>,
219 #[serde(default, deserialize_with = "deserialize_nil_string")]
220 pub completed_date: Option<String>,
221 #[serde(default, deserialize_with = "deserialize_nil_string")]
222 pub canceled_by: Option<String>,
223 #[serde(default, deserialize_with = "deserialize_nil_string")]
224 pub canceled_by_name: Option<String>,
225
226 #[serde(default)]
229 pub details: Option<DeployDetails>,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
234pub enum DeployStatus {
235 Pending,
236 InProgress,
237 Succeeded,
238 SucceededPartial,
239 Failed,
240 Canceling,
241 Canceled,
242 FinalizingDeploy,
245 FinalizingDeployFailed,
246 #[serde(other)]
254 Unknown,
255}
256
257impl DeployStatus {
258 pub fn is_terminal(self) -> bool {
263 matches!(
264 self,
265 Self::Succeeded
266 | Self::SucceededPartial
267 | Self::Failed
268 | Self::Canceled
269 | Self::FinalizingDeployFailed
270 )
271 }
272}
273
274#[derive(Debug, Clone, Default, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct DeployDetails {
278 #[serde(default, rename = "componentFailures")]
279 pub component_failures: Vec<DeployMessage>,
280 #[serde(default, rename = "componentSuccesses")]
281 pub component_successes: Vec<DeployMessage>,
282 #[serde(default)]
284 pub run_test_result: Option<RunTestsResult>,
285}
286
287#[derive(Debug, Clone, Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub struct DeployMessage {
291 #[serde(default, deserialize_with = "deserialize_nil_string")]
292 pub id: Option<String>,
293 #[serde(default, deserialize_with = "deserialize_nil_string")]
295 pub component_type: Option<String>,
296 #[serde(default, deserialize_with = "deserialize_nil_string")]
298 pub full_name: Option<String>,
299 #[serde(default, deserialize_with = "deserialize_nil_string")]
301 pub file_name: Option<String>,
302 #[serde(default)]
303 pub success: bool,
304 #[serde(default)]
305 pub changed: bool,
306 #[serde(default)]
307 pub created: bool,
308 #[serde(default)]
309 pub deleted: bool,
310 #[serde(default, deserialize_with = "deserialize_nil_string")]
311 pub created_date: Option<String>,
312 #[serde(default, deserialize_with = "deserialize_nil_string")]
315 pub problem: Option<String>,
316 #[serde(default)]
318 pub problem_type: Option<DeployProblemType>,
319 #[serde(default)]
322 pub line_number: Option<i32>,
323 #[serde(default)]
325 pub column_number: Option<i32>,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
329pub enum DeployProblemType {
330 Warning,
331 Error,
332}
333
334#[derive(Debug, Clone, Default, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct RunTestsResult {
338 #[serde(default)]
339 pub num_tests_run: i32,
340 #[serde(default)]
341 pub num_failures: i32,
342 #[serde(default)]
343 pub total_time: f64,
344 #[serde(default, deserialize_with = "deserialize_nil_string")]
345 pub apex_log_id: Option<String>,
346 #[serde(default)]
347 pub successes: Vec<RunTestSuccess>,
348 #[serde(default)]
349 pub failures: Vec<RunTestFailure>,
350 #[serde(default)]
351 pub code_coverage: Vec<CodeCoverageResult>,
352 #[serde(default)]
353 pub code_coverage_warnings: Vec<CodeCoverageWarning>,
354}
355
356#[derive(Debug, Clone, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct RunTestSuccess {
359 #[serde(default, deserialize_with = "deserialize_nil_string")]
360 pub id: Option<String>,
361 #[serde(default, deserialize_with = "deserialize_nil_string")]
362 pub name: Option<String>,
363 #[serde(default, deserialize_with = "deserialize_nil_string")]
364 pub method_name: Option<String>,
365 #[serde(default, deserialize_with = "deserialize_nil_string")]
366 pub namespace: Option<String>,
367 #[serde(default)]
368 pub time: f64,
369 #[serde(default)]
370 pub see_all_data: bool,
371}
372
373#[derive(Debug, Clone, Deserialize)]
374#[serde(rename_all = "camelCase")]
375pub struct RunTestFailure {
376 #[serde(default, deserialize_with = "deserialize_nil_string")]
377 pub id: Option<String>,
378 #[serde(default, deserialize_with = "deserialize_nil_string")]
379 pub name: Option<String>,
380 #[serde(default, deserialize_with = "deserialize_nil_string")]
381 pub method_name: Option<String>,
382 #[serde(default, deserialize_with = "deserialize_nil_string")]
383 pub namespace: Option<String>,
384 #[serde(default, deserialize_with = "deserialize_nil_string")]
385 pub message: Option<String>,
386 #[serde(default, deserialize_with = "deserialize_nil_string")]
387 pub stack_trace: Option<String>,
388 #[serde(default)]
389 pub time: f64,
390 #[serde(default)]
391 pub see_all_data: bool,
392}
393
394#[derive(Debug, Clone, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct CodeCoverageResult {
397 #[serde(default, deserialize_with = "deserialize_nil_string")]
398 pub id: Option<String>,
399 #[serde(default, deserialize_with = "deserialize_nil_string")]
400 pub name: Option<String>,
401 #[serde(default, deserialize_with = "deserialize_nil_string")]
402 pub namespace: Option<String>,
403 #[serde(default)]
404 pub num_locations: i32,
405 #[serde(default)]
406 pub num_locations_not_covered: i32,
407}
408
409#[derive(Debug, Clone, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct CodeCoverageWarning {
412 #[serde(default, deserialize_with = "deserialize_nil_string")]
413 pub id: Option<String>,
414 #[serde(default, deserialize_with = "deserialize_nil_string")]
415 pub name: Option<String>,
416 #[serde(default, deserialize_with = "deserialize_nil_string")]
417 pub namespace: Option<String>,
418 #[serde(default, deserialize_with = "deserialize_nil_string")]
419 pub message: Option<String>,
420}
421
422#[derive(Debug, Clone, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct CancelDeployResult {
430 pub id: String,
431 #[serde(default)]
432 pub done: bool,
433}
434
435#[derive(Debug, Clone, Default)]
444pub struct RetrieveRequest {
445 pub api_version: String,
448 pub package_names: Vec<String>,
450 pub single_package: bool,
453 pub specific_files: Vec<String>,
457 pub unpackaged: Option<crate::PackageManifest>,
464}
465
466#[derive(Debug, Clone, Deserialize)]
469#[serde(rename_all = "camelCase")]
470pub struct RetrieveResult {
471 pub id: String,
472 #[serde(default)]
473 pub done: bool,
474 #[serde(default)]
475 pub success: bool,
476 #[serde(default)]
477 pub status: Option<RetrieveStatus>,
478 #[serde(default, deserialize_with = "deserialize_nil_string")]
479 pub error_status_code: Option<String>,
480 #[serde(default, deserialize_with = "deserialize_nil_string")]
481 pub error_message: Option<String>,
482 #[serde(default)]
485 pub file_properties: Vec<FileProperties>,
486 #[serde(default)]
488 pub messages: Vec<RetrieveMessage>,
489 #[serde(default, deserialize_with = "deserialize_nil_string")]
494 pub zip_file: Option<String>,
495}
496
497impl RetrieveResult {
498 pub fn zip_bytes(&self) -> Result<Option<bytes::Bytes>, base64::DecodeError> {
501 use base64::Engine;
502 match &self.zip_file {
503 None => Ok(None),
504 Some(b64) => base64::engine::general_purpose::STANDARD
505 .decode(b64)
506 .map(|v| Some(bytes::Bytes::from(v))),
507 }
508 }
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
512pub enum RetrieveStatus {
513 Pending,
514 InProgress,
515 Succeeded,
516 Failed,
517 #[serde(other)]
524 Unknown,
525}
526
527impl RetrieveStatus {
528 pub fn is_terminal(self) -> bool {
533 matches!(self, Self::Succeeded | Self::Failed)
534 }
535}
536
537#[derive(Debug, Clone, Deserialize)]
539#[serde(rename_all = "camelCase")]
540pub struct FileProperties {
541 pub file_name: String,
542 pub full_name: String,
543 #[serde(default, rename = "type", deserialize_with = "deserialize_nil_string")]
545 pub type_name: Option<String>,
546 #[serde(default, deserialize_with = "deserialize_nil_string")]
547 pub id: Option<String>,
548 #[serde(default, deserialize_with = "deserialize_nil_string")]
549 pub created_by_id: Option<String>,
550 #[serde(default, deserialize_with = "deserialize_nil_string")]
551 pub created_by_name: Option<String>,
552 #[serde(default, deserialize_with = "deserialize_nil_string")]
553 pub created_date: Option<String>,
554 #[serde(default, deserialize_with = "deserialize_nil_string")]
555 pub last_modified_by_id: Option<String>,
556 #[serde(default, deserialize_with = "deserialize_nil_string")]
557 pub last_modified_by_name: Option<String>,
558 #[serde(default, deserialize_with = "deserialize_nil_string")]
559 pub last_modified_date: Option<String>,
560 #[serde(default, deserialize_with = "deserialize_nil_string")]
561 pub namespace_prefix: Option<String>,
562 #[serde(default)]
563 pub manageable_state: Option<ManageableState>,
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
568#[serde(rename_all = "camelCase")]
569pub enum ManageableState {
570 Beta,
571 Deleted,
572 Deprecated,
573 DeprecatedEditable,
574 Installed,
575 InstalledEditable,
576 Released,
577 Unmanaged,
578 #[serde(other)]
583 Unknown,
584}
585
586#[derive(Debug, Clone, Deserialize)]
588#[serde(rename_all = "camelCase")]
589pub struct RetrieveMessage {
590 #[serde(default, deserialize_with = "deserialize_nil_string")]
591 pub file_name: Option<String>,
592 pub problem: String,
593}
594
595#[derive(Debug, Clone)]
604pub struct ListMetadataQuery {
605 pub type_name: String,
607 pub folder: Option<String>,
610}
611
612#[derive(Debug, Clone, Deserialize)]
616#[serde(rename_all = "camelCase")]
617pub struct DescribeMetadataResult {
618 #[serde(default)]
621 pub metadata_objects: Vec<DescribeMetadataObject>,
622 #[serde(default, deserialize_with = "deserialize_nil_string")]
625 pub organization_namespace: Option<String>,
626 #[serde(default)]
632 pub partial_save_allowed: bool,
633 #[serde(default)]
636 pub test_required: bool,
637}
638
639#[derive(Debug, Clone, Deserialize)]
646#[serde(rename_all = "camelCase")]
647pub struct DescribeMetadataObject {
648 pub xml_name: String,
651 #[serde(default, deserialize_with = "deserialize_nil_string")]
654 pub directory_name: Option<String>,
655 #[serde(default, deserialize_with = "deserialize_nil_string")]
659 pub suffix: Option<String>,
660 #[serde(default)]
663 pub in_folder: bool,
664 #[serde(default)]
668 pub meta_file: bool,
669 #[serde(default)]
672 pub child_xml_names: Vec<String>,
673}
674
675#[derive(Debug, Clone, Deserialize)]
679#[serde(rename_all = "camelCase")]
680pub struct DescribeValueTypeResult {
681 #[serde(default)]
684 pub api_creatable: bool,
685 #[serde(default)]
688 pub api_deletable: bool,
689 #[serde(default)]
692 pub api_readable: bool,
693 #[serde(default)]
696 pub api_updatable: bool,
697 #[serde(default)]
701 pub parent_field: Option<ValueTypeField>,
702 #[serde(default)]
704 pub value_type_fields: Vec<ValueTypeField>,
705}
706
707#[derive(Debug, Clone, Default, Deserialize)]
715#[serde(rename_all = "camelCase")]
716pub struct ValueTypeField {
717 #[serde(default, deserialize_with = "deserialize_nil_string")]
719 pub name: Option<String>,
720 #[serde(default, deserialize_with = "deserialize_nil_string")]
723 pub soap_type: Option<String>,
724 #[serde(default)]
727 pub min_occurs: i32,
728 #[serde(default)]
730 pub value_required: bool,
731 #[serde(default)]
733 pub is_name_field: bool,
734 #[serde(default)]
736 pub is_foreign_key: bool,
737 #[serde(default, deserialize_with = "deserialize_nil_string")]
740 pub foreign_key_domain: Option<String>,
741 #[serde(default)]
744 pub picklist_values: Vec<PicklistEntry>,
745 #[serde(default)]
749 pub fields: Vec<ValueTypeField>,
750}
751
752#[derive(Debug, Clone, Deserialize)]
754#[serde(rename_all = "camelCase")]
755pub struct PicklistEntry {
756 #[serde(default, deserialize_with = "deserialize_nil_string")]
758 pub value: Option<String>,
759 #[serde(default, deserialize_with = "deserialize_nil_string")]
761 pub label: Option<String>,
762 #[serde(default)]
764 pub default_value: bool,
765 #[serde(default)]
767 pub active: bool,
768 #[serde(default, deserialize_with = "deserialize_nil_string")]
771 pub valid_for: Option<String>,
772}
773
774#[derive(Debug, Clone, Deserialize)]
784#[serde(rename_all = "camelCase")]
785pub struct SaveResult {
786 #[serde(default)]
788 pub full_name: String,
789 #[serde(default)]
790 pub success: bool,
791 #[serde(default)]
793 pub errors: Vec<MetadataApiError>,
794}
795
796#[derive(Debug, Clone, Deserialize)]
800#[serde(rename_all = "camelCase")]
801pub struct UpsertResult {
802 #[serde(default)]
803 pub full_name: String,
804 #[serde(default)]
805 pub success: bool,
806 #[serde(default)]
810 pub created: bool,
811 #[serde(default)]
812 pub errors: Vec<MetadataApiError>,
813}
814
815#[derive(Debug, Clone, Deserialize)]
818#[serde(rename_all = "camelCase")]
819pub struct DeleteResult {
820 #[serde(default)]
821 pub full_name: String,
822 #[serde(default)]
823 pub success: bool,
824 #[serde(default)]
825 pub errors: Vec<MetadataApiError>,
826}
827
828#[derive(Debug, Clone, Deserialize)]
839#[serde(rename_all = "camelCase")]
840pub struct MetadataApiError {
841 #[serde(default)]
845 pub status_code: String,
846 #[serde(default)]
848 pub message: String,
849 #[serde(default)]
851 pub fields: Vec<String>,
852}
853
854#[cfg(test)]
855#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
856mod tests {
857 use super::*;
858
859 #[test]
860 fn deploy_status_is_terminal_matches_completed_states() {
861 assert!(DeployStatus::Succeeded.is_terminal());
862 assert!(DeployStatus::SucceededPartial.is_terminal());
863 assert!(DeployStatus::Failed.is_terminal());
864 assert!(DeployStatus::Canceled.is_terminal());
865 assert!(!DeployStatus::Pending.is_terminal());
866 assert!(!DeployStatus::InProgress.is_terminal());
867 assert!(!DeployStatus::Canceling.is_terminal());
868 assert!(!DeployStatus::FinalizingDeploy.is_terminal());
869 }
870
871 #[test]
872 fn retrieve_status_is_terminal_matches_succeeded_or_failed() {
873 assert!(RetrieveStatus::Succeeded.is_terminal());
874 assert!(RetrieveStatus::Failed.is_terminal());
875 assert!(!RetrieveStatus::Pending.is_terminal());
876 assert!(!RetrieveStatus::InProgress.is_terminal());
877 }
878
879 #[test]
880 fn unknown_status_literals_deserialize_to_unknown_not_error() {
881 #[derive(Deserialize)]
885 struct Wire {
886 deploy: DeployStatus,
887 retrieve: RetrieveStatus,
888 state: AsyncRequestState,
889 manageable: ManageableState,
890 }
891 let parsed: Wire = quick_xml::de::from_str(
892 "<Wire>\
893 <deploy>BrandNewPhase</deploy>\
894 <retrieve>BrandNewPhase</retrieve>\
895 <state>BrandNewPhase</state>\
896 <manageable>brandNewState</manageable>\
897 </Wire>",
898 )
899 .unwrap();
900 assert_eq!(parsed.deploy, DeployStatus::Unknown);
901 assert!(!parsed.deploy.is_terminal());
902 assert_eq!(parsed.retrieve, RetrieveStatus::Unknown);
903 assert!(!parsed.retrieve.is_terminal());
904 assert_eq!(parsed.state, AsyncRequestState::Unknown);
905 assert_eq!(parsed.manageable, ManageableState::Unknown);
906 }
907
908 #[test]
909 fn test_level_as_wire_matches_doc_strings() {
910 assert_eq!(TestLevel::NoTestRun.as_wire(), "NoTestRun");
911 assert_eq!(TestLevel::RunSpecifiedTests.as_wire(), "RunSpecifiedTests");
912 assert_eq!(TestLevel::RunLocalTests.as_wire(), "RunLocalTests");
913 assert_eq!(TestLevel::RunAllTestsInOrg.as_wire(), "RunAllTestsInOrg");
914 assert_eq!(TestLevel::RunRelevantTests.as_wire(), "RunRelevantTests");
915 }
916
917 #[test]
918 fn retrieve_result_decodes_zip_bytes_from_base64() {
919 let r = RetrieveResult {
920 id: "x".into(),
921 done: true,
922 success: true,
923 status: Some(RetrieveStatus::Succeeded),
924 error_status_code: None,
925 error_message: None,
926 file_properties: vec![],
927 messages: vec![],
928 zip_file: Some("aGVsbG8=".into()), };
930 let bytes = r.zip_bytes().unwrap().unwrap();
931 assert_eq!(&bytes[..], b"hello");
932 }
933
934 #[test]
935 fn retrieve_result_zip_bytes_returns_none_when_absent() {
936 let r = RetrieveResult {
937 id: "x".into(),
938 done: false,
939 success: false,
940 status: None,
941 error_status_code: None,
942 error_message: None,
943 file_properties: vec![],
944 messages: vec![],
945 zip_file: None,
946 };
947 assert!(r.zip_bytes().unwrap().is_none());
948 }
949}