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}
84
85#[derive(Debug, Clone, Default)]
98pub struct DeployOptions {
99 pub allow_missing_files: Option<bool>,
103 pub auto_update_package: Option<bool>,
105 pub check_only: Option<bool>,
110 pub ignore_warnings: Option<bool>,
112 pub perform_retrieve: Option<bool>,
114 pub purge_on_delete: Option<bool>,
117 pub rollback_on_error: Option<bool>,
120 pub run_tests: Vec<String>,
123 pub single_package: Option<bool>,
125 pub test_level: Option<TestLevel>,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum TestLevel {
132 NoTestRun,
134 RunSpecifiedTests,
136 RunRelevantTests,
138 RunLocalTests,
141 RunAllTestsInOrg,
143}
144
145impl TestLevel {
146 pub(crate) fn as_wire(&self) -> &'static str {
147 match self {
148 Self::NoTestRun => "NoTestRun",
149 Self::RunSpecifiedTests => "RunSpecifiedTests",
150 Self::RunRelevantTests => "RunRelevantTests",
151 Self::RunLocalTests => "RunLocalTests",
152 Self::RunAllTestsInOrg => "RunAllTestsInOrg",
153 }
154 }
155}
156
157#[derive(Debug, Clone, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct DeployResult {
162 pub id: String,
163 #[serde(default)]
166 pub done: bool,
167 #[serde(default)]
169 pub success: bool,
170 #[serde(default)]
171 pub status: Option<DeployStatus>,
172 #[serde(default)]
173 pub check_only: bool,
174 #[serde(default)]
175 pub ignore_warnings: bool,
176 #[serde(default)]
177 pub rollback_on_error: bool,
178 #[serde(default)]
180 pub run_tests_enabled: bool,
181
182 #[serde(default)]
183 pub number_components_deployed: i32,
184 #[serde(default)]
185 pub number_components_total: i32,
186 #[serde(default)]
187 pub number_component_errors: i32,
188 #[serde(default)]
189 pub number_tests_completed: i32,
190 #[serde(default)]
191 pub number_tests_total: i32,
192 #[serde(default)]
193 pub number_test_errors: i32,
194
195 #[serde(default, deserialize_with = "deserialize_nil_string")]
198 pub state_detail: Option<String>,
199
200 #[serde(default, deserialize_with = "deserialize_nil_string")]
201 pub error_status_code: Option<String>,
202 #[serde(default, deserialize_with = "deserialize_nil_string")]
203 pub error_message: Option<String>,
204
205 #[serde(default, deserialize_with = "deserialize_nil_string")]
206 pub created_by: Option<String>,
207 #[serde(default, deserialize_with = "deserialize_nil_string")]
208 pub created_by_name: Option<String>,
209 #[serde(default, deserialize_with = "deserialize_nil_string")]
210 pub created_date: Option<String>,
211 #[serde(default, deserialize_with = "deserialize_nil_string")]
212 pub start_date: Option<String>,
213 #[serde(default, deserialize_with = "deserialize_nil_string")]
214 pub last_modified_date: Option<String>,
215 #[serde(default, deserialize_with = "deserialize_nil_string")]
216 pub completed_date: Option<String>,
217 #[serde(default, deserialize_with = "deserialize_nil_string")]
218 pub canceled_by: Option<String>,
219 #[serde(default, deserialize_with = "deserialize_nil_string")]
220 pub canceled_by_name: Option<String>,
221
222 #[serde(default)]
225 pub details: Option<DeployDetails>,
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
230pub enum DeployStatus {
231 Pending,
232 InProgress,
233 Succeeded,
234 SucceededPartial,
235 Failed,
236 Canceling,
237 Canceled,
238 FinalizingDeploy,
241 FinalizingDeployFailed,
242}
243
244impl DeployStatus {
245 pub fn is_terminal(self) -> bool {
247 matches!(
248 self,
249 Self::Succeeded
250 | Self::SucceededPartial
251 | Self::Failed
252 | Self::Canceled
253 | Self::FinalizingDeployFailed
254 )
255 }
256}
257
258#[derive(Debug, Clone, Default, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub struct DeployDetails {
262 #[serde(default, rename = "componentFailures")]
263 pub component_failures: Vec<DeployMessage>,
264 #[serde(default, rename = "componentSuccesses")]
265 pub component_successes: Vec<DeployMessage>,
266 #[serde(default)]
268 pub run_test_result: Option<RunTestsResult>,
269}
270
271#[derive(Debug, Clone, Deserialize)]
273#[serde(rename_all = "camelCase")]
274pub struct DeployMessage {
275 #[serde(default, deserialize_with = "deserialize_nil_string")]
276 pub id: Option<String>,
277 #[serde(default, deserialize_with = "deserialize_nil_string")]
279 pub component_type: Option<String>,
280 #[serde(default, deserialize_with = "deserialize_nil_string")]
282 pub full_name: Option<String>,
283 #[serde(default, deserialize_with = "deserialize_nil_string")]
285 pub file_name: Option<String>,
286 #[serde(default)]
287 pub success: bool,
288 #[serde(default)]
289 pub changed: bool,
290 #[serde(default)]
291 pub created: bool,
292 #[serde(default)]
293 pub deleted: bool,
294 #[serde(default, deserialize_with = "deserialize_nil_string")]
295 pub created_date: Option<String>,
296 #[serde(default, deserialize_with = "deserialize_nil_string")]
299 pub problem: Option<String>,
300 #[serde(default)]
302 pub problem_type: Option<DeployProblemType>,
303 #[serde(default)]
306 pub line_number: Option<i32>,
307 #[serde(default)]
309 pub column_number: Option<i32>,
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
313pub enum DeployProblemType {
314 Warning,
315 Error,
316}
317
318#[derive(Debug, Clone, Default, Deserialize)]
320#[serde(rename_all = "camelCase")]
321pub struct RunTestsResult {
322 #[serde(default)]
323 pub num_tests_run: i32,
324 #[serde(default)]
325 pub num_failures: i32,
326 #[serde(default)]
327 pub total_time: f64,
328 #[serde(default, deserialize_with = "deserialize_nil_string")]
329 pub apex_log_id: Option<String>,
330 #[serde(default)]
331 pub successes: Vec<RunTestSuccess>,
332 #[serde(default)]
333 pub failures: Vec<RunTestFailure>,
334 #[serde(default)]
335 pub code_coverage: Vec<CodeCoverageResult>,
336 #[serde(default)]
337 pub code_coverage_warnings: Vec<CodeCoverageWarning>,
338}
339
340#[derive(Debug, Clone, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct RunTestSuccess {
343 #[serde(default, deserialize_with = "deserialize_nil_string")]
344 pub id: Option<String>,
345 #[serde(default, deserialize_with = "deserialize_nil_string")]
346 pub name: Option<String>,
347 #[serde(default, deserialize_with = "deserialize_nil_string")]
348 pub method_name: Option<String>,
349 #[serde(default, deserialize_with = "deserialize_nil_string")]
350 pub namespace: Option<String>,
351 #[serde(default)]
352 pub time: f64,
353 #[serde(default)]
354 pub see_all_data: bool,
355}
356
357#[derive(Debug, Clone, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct RunTestFailure {
360 #[serde(default, deserialize_with = "deserialize_nil_string")]
361 pub id: Option<String>,
362 #[serde(default, deserialize_with = "deserialize_nil_string")]
363 pub name: Option<String>,
364 #[serde(default, deserialize_with = "deserialize_nil_string")]
365 pub method_name: Option<String>,
366 #[serde(default, deserialize_with = "deserialize_nil_string")]
367 pub namespace: Option<String>,
368 #[serde(default, deserialize_with = "deserialize_nil_string")]
369 pub message: Option<String>,
370 #[serde(default, deserialize_with = "deserialize_nil_string")]
371 pub stack_trace: Option<String>,
372 #[serde(default)]
373 pub time: f64,
374 #[serde(default)]
375 pub see_all_data: bool,
376}
377
378#[derive(Debug, Clone, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct CodeCoverageResult {
381 #[serde(default, deserialize_with = "deserialize_nil_string")]
382 pub id: Option<String>,
383 #[serde(default, deserialize_with = "deserialize_nil_string")]
384 pub name: Option<String>,
385 #[serde(default, deserialize_with = "deserialize_nil_string")]
386 pub namespace: Option<String>,
387 #[serde(default)]
388 pub num_locations: i32,
389 #[serde(default)]
390 pub num_locations_not_covered: i32,
391}
392
393#[derive(Debug, Clone, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct CodeCoverageWarning {
396 #[serde(default, deserialize_with = "deserialize_nil_string")]
397 pub id: Option<String>,
398 #[serde(default, deserialize_with = "deserialize_nil_string")]
399 pub name: Option<String>,
400 #[serde(default, deserialize_with = "deserialize_nil_string")]
401 pub namespace: Option<String>,
402 #[serde(default, deserialize_with = "deserialize_nil_string")]
403 pub message: Option<String>,
404}
405
406#[derive(Debug, Clone, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct CancelDeployResult {
414 pub id: String,
415 #[serde(default)]
416 pub done: bool,
417}
418
419#[derive(Debug, Clone, Default)]
428pub struct RetrieveRequest {
429 pub api_version: String,
432 pub package_names: Vec<String>,
434 pub single_package: bool,
437 pub specific_files: Vec<String>,
441 pub unpackaged: Option<crate::PackageManifest>,
448}
449
450#[derive(Debug, Clone, Deserialize)]
453#[serde(rename_all = "camelCase")]
454pub struct RetrieveResult {
455 pub id: String,
456 #[serde(default)]
457 pub done: bool,
458 #[serde(default)]
459 pub success: bool,
460 #[serde(default)]
461 pub status: Option<RetrieveStatus>,
462 #[serde(default, deserialize_with = "deserialize_nil_string")]
463 pub error_status_code: Option<String>,
464 #[serde(default, deserialize_with = "deserialize_nil_string")]
465 pub error_message: Option<String>,
466 #[serde(default)]
469 pub file_properties: Vec<FileProperties>,
470 #[serde(default)]
472 pub messages: Vec<RetrieveMessage>,
473 #[serde(default, deserialize_with = "deserialize_nil_string")]
478 pub zip_file: Option<String>,
479}
480
481impl RetrieveResult {
482 pub fn zip_bytes(&self) -> Result<Option<bytes::Bytes>, base64::DecodeError> {
485 use base64::Engine;
486 match &self.zip_file {
487 None => Ok(None),
488 Some(b64) => base64::engine::general_purpose::STANDARD
489 .decode(b64)
490 .map(|v| Some(bytes::Bytes::from(v))),
491 }
492 }
493}
494
495#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
496pub enum RetrieveStatus {
497 Pending,
498 InProgress,
499 Succeeded,
500 Failed,
501}
502
503impl RetrieveStatus {
504 pub fn is_terminal(self) -> bool {
506 matches!(self, Self::Succeeded | Self::Failed)
507 }
508}
509
510#[derive(Debug, Clone, Deserialize)]
512#[serde(rename_all = "camelCase")]
513pub struct FileProperties {
514 pub file_name: String,
515 pub full_name: String,
516 #[serde(default, rename = "type", deserialize_with = "deserialize_nil_string")]
518 pub type_name: Option<String>,
519 #[serde(default, deserialize_with = "deserialize_nil_string")]
520 pub id: Option<String>,
521 #[serde(default, deserialize_with = "deserialize_nil_string")]
522 pub created_by_id: Option<String>,
523 #[serde(default, deserialize_with = "deserialize_nil_string")]
524 pub created_by_name: Option<String>,
525 #[serde(default, deserialize_with = "deserialize_nil_string")]
526 pub created_date: Option<String>,
527 #[serde(default, deserialize_with = "deserialize_nil_string")]
528 pub last_modified_by_id: Option<String>,
529 #[serde(default, deserialize_with = "deserialize_nil_string")]
530 pub last_modified_by_name: Option<String>,
531 #[serde(default, deserialize_with = "deserialize_nil_string")]
532 pub last_modified_date: Option<String>,
533 #[serde(default, deserialize_with = "deserialize_nil_string")]
534 pub namespace_prefix: Option<String>,
535 #[serde(default)]
536 pub manageable_state: Option<ManageableState>,
537}
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
541#[serde(rename_all = "camelCase")]
542pub enum ManageableState {
543 Beta,
544 Deleted,
545 Deprecated,
546 DeprecatedEditable,
547 Installed,
548 InstalledEditable,
549 Released,
550 Unmanaged,
551}
552
553#[derive(Debug, Clone, Deserialize)]
555#[serde(rename_all = "camelCase")]
556pub struct RetrieveMessage {
557 #[serde(default, deserialize_with = "deserialize_nil_string")]
558 pub file_name: Option<String>,
559 pub problem: String,
560}
561
562#[derive(Debug, Clone)]
571pub struct ListMetadataQuery {
572 pub type_name: String,
574 pub folder: Option<String>,
577}
578
579#[derive(Debug, Clone, Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct DescribeMetadataResult {
585 #[serde(default)]
588 pub metadata_objects: Vec<DescribeMetadataObject>,
589 #[serde(default, deserialize_with = "deserialize_nil_string")]
592 pub organization_namespace: Option<String>,
593 #[serde(default)]
599 pub partial_save_allowed: bool,
600 #[serde(default)]
603 pub test_required: bool,
604}
605
606#[derive(Debug, Clone, Deserialize)]
613#[serde(rename_all = "camelCase")]
614pub struct DescribeMetadataObject {
615 pub xml_name: String,
618 #[serde(default, deserialize_with = "deserialize_nil_string")]
621 pub directory_name: Option<String>,
622 #[serde(default, deserialize_with = "deserialize_nil_string")]
626 pub suffix: Option<String>,
627 #[serde(default)]
630 pub in_folder: bool,
631 #[serde(default)]
635 pub meta_file: bool,
636 #[serde(default)]
639 pub child_xml_names: Vec<String>,
640}
641
642#[derive(Debug, Clone, Deserialize)]
646#[serde(rename_all = "camelCase")]
647pub struct DescribeValueTypeResult {
648 #[serde(default)]
651 pub api_creatable: bool,
652 #[serde(default)]
655 pub api_deletable: bool,
656 #[serde(default)]
659 pub api_readable: bool,
660 #[serde(default)]
663 pub api_updatable: bool,
664 #[serde(default)]
668 pub parent_field: Option<ValueTypeField>,
669 #[serde(default)]
671 pub value_type_fields: Vec<ValueTypeField>,
672}
673
674#[derive(Debug, Clone, Default, Deserialize)]
682#[serde(rename_all = "camelCase")]
683pub struct ValueTypeField {
684 #[serde(default, deserialize_with = "deserialize_nil_string")]
686 pub name: Option<String>,
687 #[serde(default, deserialize_with = "deserialize_nil_string")]
690 pub soap_type: Option<String>,
691 #[serde(default)]
694 pub min_occurs: i32,
695 #[serde(default)]
697 pub value_required: bool,
698 #[serde(default)]
700 pub is_name_field: bool,
701 #[serde(default)]
703 pub is_foreign_key: bool,
704 #[serde(default, deserialize_with = "deserialize_nil_string")]
707 pub foreign_key_domain: Option<String>,
708 #[serde(default)]
711 pub picklist_values: Vec<PicklistEntry>,
712 #[serde(default)]
716 pub fields: Vec<ValueTypeField>,
717}
718
719#[derive(Debug, Clone, Deserialize)]
721#[serde(rename_all = "camelCase")]
722pub struct PicklistEntry {
723 #[serde(default, deserialize_with = "deserialize_nil_string")]
725 pub value: Option<String>,
726 #[serde(default, deserialize_with = "deserialize_nil_string")]
728 pub label: Option<String>,
729 #[serde(default)]
731 pub default_value: bool,
732 #[serde(default)]
734 pub active: bool,
735 #[serde(default, deserialize_with = "deserialize_nil_string")]
738 pub valid_for: Option<String>,
739}
740
741#[derive(Debug, Clone, Deserialize)]
751#[serde(rename_all = "camelCase")]
752pub struct SaveResult {
753 #[serde(default)]
755 pub full_name: String,
756 #[serde(default)]
757 pub success: bool,
758 #[serde(default)]
760 pub errors: Vec<MetadataApiError>,
761}
762
763#[derive(Debug, Clone, Deserialize)]
767#[serde(rename_all = "camelCase")]
768pub struct UpsertResult {
769 #[serde(default)]
770 pub full_name: String,
771 #[serde(default)]
772 pub success: bool,
773 #[serde(default)]
777 pub created: bool,
778 #[serde(default)]
779 pub errors: Vec<MetadataApiError>,
780}
781
782#[derive(Debug, Clone, Deserialize)]
785#[serde(rename_all = "camelCase")]
786pub struct DeleteResult {
787 #[serde(default)]
788 pub full_name: String,
789 #[serde(default)]
790 pub success: bool,
791 #[serde(default)]
792 pub errors: Vec<MetadataApiError>,
793}
794
795#[derive(Debug, Clone, Deserialize)]
806#[serde(rename_all = "camelCase")]
807pub struct MetadataApiError {
808 #[serde(default)]
812 pub status_code: String,
813 #[serde(default)]
815 pub message: String,
816 #[serde(default)]
818 pub fields: Vec<String>,
819}
820
821#[cfg(test)]
822#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
823mod tests {
824 use super::*;
825
826 #[test]
827 fn deploy_status_is_terminal_matches_completed_states() {
828 assert!(DeployStatus::Succeeded.is_terminal());
829 assert!(DeployStatus::SucceededPartial.is_terminal());
830 assert!(DeployStatus::Failed.is_terminal());
831 assert!(DeployStatus::Canceled.is_terminal());
832 assert!(!DeployStatus::Pending.is_terminal());
833 assert!(!DeployStatus::InProgress.is_terminal());
834 assert!(!DeployStatus::Canceling.is_terminal());
835 assert!(!DeployStatus::FinalizingDeploy.is_terminal());
836 }
837
838 #[test]
839 fn retrieve_status_is_terminal_matches_succeeded_or_failed() {
840 assert!(RetrieveStatus::Succeeded.is_terminal());
841 assert!(RetrieveStatus::Failed.is_terminal());
842 assert!(!RetrieveStatus::Pending.is_terminal());
843 assert!(!RetrieveStatus::InProgress.is_terminal());
844 }
845
846 #[test]
847 fn test_level_as_wire_matches_doc_strings() {
848 assert_eq!(TestLevel::NoTestRun.as_wire(), "NoTestRun");
849 assert_eq!(TestLevel::RunSpecifiedTests.as_wire(), "RunSpecifiedTests");
850 assert_eq!(TestLevel::RunLocalTests.as_wire(), "RunLocalTests");
851 assert_eq!(TestLevel::RunAllTestsInOrg.as_wire(), "RunAllTestsInOrg");
852 assert_eq!(TestLevel::RunRelevantTests.as_wire(), "RunRelevantTests");
853 }
854
855 #[test]
856 fn retrieve_result_decodes_zip_bytes_from_base64() {
857 let r = RetrieveResult {
858 id: "x".into(),
859 done: true,
860 success: true,
861 status: Some(RetrieveStatus::Succeeded),
862 error_status_code: None,
863 error_message: None,
864 file_properties: vec![],
865 messages: vec![],
866 zip_file: Some("aGVsbG8=".into()), };
868 let bytes = r.zip_bytes().unwrap().unwrap();
869 assert_eq!(&bytes[..], b"hello");
870 }
871
872 #[test]
873 fn retrieve_result_zip_bytes_returns_none_when_absent() {
874 let r = RetrieveResult {
875 id: "x".into(),
876 done: false,
877 success: false,
878 status: None,
879 error_status_code: None,
880 error_message: None,
881 file_properties: vec![],
882 messages: vec![],
883 zip_file: None,
884 };
885 assert!(r.zip_bytes().unwrap().is_none());
886 }
887}