Skip to main content

cirrus_metadata/
result.rs

1//! Typed wire envelopes for the file-based Metadata API operations.
2//!
3//! Every struct here is a platform contract — its shape is defined by
4//! the Salesforce Metadata API and shipped per the field tables in the
5//! [Metadata API Developer Guide]. Caller-controlled metadata
6//! components (CustomObject XML, ApexClass source, etc.) are *not*
7//! modeled — they belong in opaque zip payloads on deploy and arrive
8//! as base64-encoded zip bytes on retrieve.
9//!
10//! ## Forward compatibility
11//!
12//! Salesforce adds fields to these envelopes every release. We
13//! deliberately:
14//!
15//! - use `#[serde(default)]` on every optional / list field, and
16//! - omit `#[serde(deny_unknown_fields)]`,
17//!
18//! so a response carrying new fields deserializes cleanly into the old
19//! struct rather than failing the call. The cost is that genuinely
20//! malformed responses degrade silently; the trade-off is worth it for
21//! a long-running SDK.
22//!
23//! [Metadata API Developer Guide]: https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/
24
25use serde::Deserialize;
26
27/// Adapter that maps empty strings to `None`.
28///
29/// Salesforce's SOAP responses encode null `Option<String>` fields as
30/// `<field xsi:nil="true"/>` self-closing elements. quick-xml's serde
31/// adapter surfaces these as `Some("")` rather than `None` — the
32/// `xsi:nil` attribute carries no semantics at the serde layer. Without
33/// this adapter, downstream code that branches on `.is_none()` would
34/// instead see `Some("")` for unnamespaced components, types with no
35/// file suffix, etc. — which are the common cases.
36///
37/// Apply via `#[serde(default, deserialize_with = "deserialize_nil_string")]`
38/// on every `Option<String>` field that Salesforce can render as
39/// `xsi:nil="true"`.
40fn 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// -- Async kickoff envelopes -------------------------------------------------
49
50/// Returned by `deploy()` and `retrieve()` to identify the async job.
51///
52/// Most fields beyond `id` are deprecated as of API v31; we keep them
53/// optional for future-proofing but in practice only `id` is reliably
54/// populated.
55#[derive(Debug, Clone, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct AsyncResult {
58    /// ID of the deployment or retrieval job. Pass this to
59    /// `check_deploy_status` / `check_retrieve_status`.
60    pub id: String,
61    /// Whether the job has completed. Deprecated in newer API versions
62    /// (use `check_*_status` instead), kept for compatibility.
63    #[serde(default)]
64    pub done: bool,
65    /// Job state. Deprecated in newer API versions.
66    #[serde(default)]
67    pub state: Option<AsyncRequestState>,
68    /// Status code on error. Deprecated.
69    #[serde(default, deserialize_with = "deserialize_nil_string")]
70    pub status_code: Option<String>,
71    /// Error message corresponding to `status_code`. Deprecated.
72    #[serde(default, deserialize_with = "deserialize_nil_string")]
73    pub message: Option<String>,
74}
75
76/// Lifecycle state of an async metadata call.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
78pub enum AsyncRequestState {
79    Queued,
80    InProgress,
81    Completed,
82    Error,
83    /// A state literal this SDK version doesn't know — kept from
84    /// turning the whole response into a deserialization error.
85    #[serde(other)]
86    Unknown,
87}
88
89// -- Deploy ------------------------------------------------------------------
90
91/// Options for a `deploy()` call.
92///
93/// All fields are optional — omitted fields are not sent and Salesforce
94/// applies its defaults. Pass `Default::default()` to use Salesforce's
95/// defaults for everything.
96///
97/// See the [DeployOptions docs] for field semantics and production-deploy
98/// requirements (e.g. `rollback_on_error` must be `true` for prod).
99///
100/// [DeployOptions docs]: https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy.htm
101#[derive(Debug, Clone, Default)]
102pub struct DeployOptions {
103    /// If `true`, the deployment proceeds even if files listed in
104    /// `package.xml` are missing from the zip. **Don't set on
105    /// production deploys.**
106    pub allow_missing_files: Option<bool>,
107    /// Reserved for future use.
108    pub auto_update_package: Option<bool>,
109    /// If `true`, performs a test deployment (validation) without
110    /// actually committing the components. Pair with
111    /// `test_level: RunLocalTests` to qualify the result for
112    /// `deploy_recent_validation`.
113    pub check_only: Option<bool>,
114    /// Continue on warnings.
115    pub ignore_warnings: Option<bool>,
116    /// Reserved for future use.
117    pub perform_retrieve: Option<bool>,
118    /// In dev/sandbox orgs only: skip the Recycle Bin when deleting
119    /// components listed in `destructiveChanges.xml`.
120    pub purge_on_delete: Option<bool>,
121    /// Required `true` for production deployments — roll back the
122    /// whole job on any failure.
123    pub rollback_on_error: Option<bool>,
124    /// Specific Apex test class names to run. Only meaningful when
125    /// `test_level` is `RunSpecifiedTests`.
126    pub run_tests: Vec<String>,
127    /// `true` if the zip is a single package; `false` for a set.
128    pub single_package: Option<bool>,
129    /// How aggressively to run tests during deployment.
130    pub test_level: Option<TestLevel>,
131}
132
133/// How much of the org's Apex test suite to run during a deployment.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum TestLevel {
136    /// No tests. Sandbox/dev only.
137    NoTestRun,
138    /// Only the classes listed in [`DeployOptions::run_tests`].
139    RunSpecifiedTests,
140    /// (Beta) Salesforce-selected relevant tests.
141    RunRelevantTests,
142    /// All non-managed-package tests in the org. Default for prod
143    /// deploys that contain Apex.
144    RunLocalTests,
145    /// Every test in the org including managed-package ones.
146    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/// Returned by `check_deploy_status`. The headline summary of a
162/// deployment.
163#[derive(Debug, Clone, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct DeployResult {
166    pub id: String,
167    /// Whether the server is done processing the job. Poll until this
168    /// is `true`.
169    #[serde(default)]
170    pub done: bool,
171    /// Overall success/failure. Only meaningful once `done == true`.
172    #[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    /// Whether Apex tests were exercised.
183    #[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    /// Free-form description of the in-progress component or test
200    /// class.
201    #[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    /// Per-component success/failure entries. Only populated when
227    /// `check_deploy_status` was called with `include_details: true`.
228    #[serde(default)]
229    pub details: Option<DeployDetails>,
230}
231
232/// State of a deployment job. See [`DeployResult::status`].
233#[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    /// Newer status, post-commit phase that can't be canceled in
243    /// API 65.0+.
244    FinalizingDeploy,
245    FinalizingDeployFailed,
246    /// A status literal this SDK version doesn't know. Salesforce has
247    /// extended the set before (`FinalizingDeploy` arrived in API
248    /// 65.0), and a new literal must not turn every status poll into
249    /// a deserialization error. Terminal-state detection is unaffected:
250    /// [`wait_for_deploy`] keys off the `done` flag, not the status.
251    ///
252    /// [`wait_for_deploy`]: crate::MetadataClient::wait_for_deploy
253    #[serde(other)]
254    Unknown,
255}
256
257impl DeployStatus {
258    /// True when the deploy job is finished, regardless of success.
259    ///
260    /// [`Unknown`](Self::Unknown) reports `false` — an unrecognized
261    /// status can't be assumed finished.
262    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/// Per-component results bundled into a [`DeployResult`].
275#[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    /// Apex test results.
283    #[serde(default)]
284    pub run_test_result: Option<RunTestsResult>,
285}
286
287/// Per-component status entry inside [`DeployDetails`].
288#[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    /// Metadata type, e.g. `ApexClass`.
294    #[serde(default, deserialize_with = "deserialize_nil_string")]
295    pub component_type: Option<String>,
296    /// Component identifier (e.g. `MyClass`).
297    #[serde(default, deserialize_with = "deserialize_nil_string")]
298    pub full_name: Option<String>,
299    /// File path inside the deployed zip.
300    #[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    /// Error or warning message text when `success == false` or
313    /// `problem_type == Warning`.
314    #[serde(default, deserialize_with = "deserialize_nil_string")]
315    pub problem: Option<String>,
316    /// Distinguishes errors from warnings.
317    #[serde(default)]
318    pub problem_type: Option<DeployProblemType>,
319    /// Line number in a source file where the problem occurred, when
320    /// applicable (Apex class compile errors, etc.).
321    #[serde(default)]
322    pub line_number: Option<i32>,
323    /// Column number, paired with `line_number`.
324    #[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/// Apex test results inside [`DeployDetails`].
335#[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// -- Cancel ------------------------------------------------------------------
423
424/// Returned by `cancel_deploy()`. `done == false` means the cancellation
425/// is in progress; `done == true` means it landed (the deployment was
426/// either still queued or cancelled successfully).
427#[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// -- Retrieve ----------------------------------------------------------------
436
437/// Input for a `retrieve()` call.
438///
439/// At least one of [`package_names`](Self::package_names),
440/// [`specific_files`](Self::specific_files), or
441/// [`unpackaged`](Self::unpackaged) should be set — otherwise there's
442/// nothing to retrieve.
443#[derive(Debug, Clone, Default)]
444pub struct RetrieveRequest {
445    /// API version for the retrieve. The version inside `package.xml`
446    /// takes precedence in API v31+.
447    pub api_version: String,
448    /// Packaged components to retrieve by managed-package name.
449    pub package_names: Vec<String>,
450    /// `true` if the result is one package (vs. a set). Required
451    /// `true` when `specific_files` is non-empty.
452    pub single_package: bool,
453    /// Specific file paths to retrieve, e.g.
454    /// `["unpackaged/classes/MyClass.cls"]`. When set, `package_names`
455    /// must be empty and `single_package` must be `true`.
456    pub specific_files: Vec<String>,
457    /// Unpackaged components to retrieve, expressed as a
458    /// [`PackageManifest`]. Built with the same fluent API used for
459    /// generating `package.xml` files — see the manifest module
460    /// docs.
461    ///
462    /// [`PackageManifest`]: crate::PackageManifest
463    pub unpackaged: Option<crate::PackageManifest>,
464}
465
466/// Returned by `check_retrieve_status`. Once `done == true` and
467/// `success == true`, `zip_file` contains the retrieved zip bytes.
468#[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    /// Per-file properties for everything in the retrieved zip,
483    /// including the manifest.
484    #[serde(default)]
485    pub file_properties: Vec<FileProperties>,
486    /// Errors and warnings encountered during the retrieve.
487    #[serde(default)]
488    pub messages: Vec<RetrieveMessage>,
489    /// Base64-encoded zip bytes. Use [`Self::zip_bytes`] to decode.
490    /// Only populated when `done == true` and `success == true`,
491    /// and only when `check_retrieve_status` was called with
492    /// `include_zip == true`.
493    #[serde(default, deserialize_with = "deserialize_nil_string")]
494    pub zip_file: Option<String>,
495}
496
497impl RetrieveResult {
498    /// Decode the `zip_file` field from base64 into raw zip bytes.
499    /// Returns `Ok(None)` if no zip is present in the result.
500    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    /// A status literal this SDK version doesn't know — kept from
518    /// turning the whole response into a deserialization error.
519    /// Terminal-state detection is unaffected: [`wait_for_retrieve`]
520    /// keys off the `done` flag, not the status.
521    ///
522    /// [`wait_for_retrieve`]: crate::MetadataClient::wait_for_retrieve
523    #[serde(other)]
524    Unknown,
525}
526
527impl RetrieveStatus {
528    /// True when the retrieve job is finished, regardless of success.
529    ///
530    /// [`Unknown`](Self::Unknown) reports `false` — an unrecognized
531    /// status can't be assumed finished.
532    pub fn is_terminal(self) -> bool {
533        matches!(self, Self::Succeeded | Self::Failed)
534    }
535}
536
537/// Properties of one file inside a retrieve result.
538#[derive(Debug, Clone, Deserialize)]
539#[serde(rename_all = "camelCase")]
540pub struct FileProperties {
541    pub file_name: String,
542    pub full_name: String,
543    /// Metadata type name, e.g. `"ApexClass"`.
544    #[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/// Distribution / lifecycle state of a packaged component.
567#[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    /// A state literal this SDK version doesn't know. This enum rides
579    /// inside every [`FileProperties`], so without a fallback a single
580    /// new literal would fail deserialization of an entire
581    /// `listMetadata` / retrieve response.
582    #[serde(other)]
583    Unknown,
584}
585
586/// Error / warning surfaced in a [`RetrieveResult`].
587#[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// -- Utility ops -------------------------------------------------------------
596
597/// One query inside a `list_metadata` call.
598///
599/// At most three queries may be batched per call (Salesforce server
600/// limit). `type_name` is required; `folder` is needed for components
601/// that live under a folder (Dashboard, Document, EmailTemplate,
602/// Report).
603#[derive(Debug, Clone)]
604pub struct ListMetadataQuery {
605    /// Metadata type, e.g. `"ApexClass"`, `"CustomObject"`.
606    pub type_name: String,
607    /// Folder name when querying a folder-based type. Set to `None`
608    /// for top-level types.
609    pub folder: Option<String>,
610}
611
612/// Returned by `describe_metadata`. Catalogs the metadata types
613/// available in the target org plus a few org-wide flags useful for
614/// deciding deploy behavior.
615#[derive(Debug, Clone, Deserialize)]
616#[serde(rename_all = "camelCase")]
617pub struct DescribeMetadataResult {
618    /// Per-type descriptors — directory name, file suffix, child types,
619    /// etc. One entry per metadata type the org supports.
620    #[serde(default)]
621    pub metadata_objects: Vec<DescribeMetadataObject>,
622    /// Namespace prefix for managed packages in this org. Empty
623    /// (`""`) for orgs with no namespace.
624    #[serde(default, deserialize_with = "deserialize_nil_string")]
625    pub organization_namespace: Option<String>,
626    /// Whether the org allows partial deployments (`rollbackOnError`
627    /// can be `false`). In practice this is the inverse of
628    /// [`Self::test_required`] — production-like orgs require tests
629    /// and disallow partial saves — but both fields come from the
630    /// server, so trust the wire over the invariant.
631    #[serde(default)]
632    pub partial_save_allowed: bool,
633    /// Whether Apex tests are required on deploy. See
634    /// [`Self::partial_save_allowed`] for the usual relationship.
635    #[serde(default)]
636    pub test_required: bool,
637}
638
639/// Descriptor for one metadata type, returned inside
640/// [`DescribeMetadataResult::metadata_objects`].
641///
642/// This is the source of truth for `package.xml` `<types><name>` values
643/// and for zip directory layout — `xml_name` is what goes in the
644/// manifest, `directory_name` is what the zip folder is called.
645#[derive(Debug, Clone, Deserialize)]
646#[serde(rename_all = "camelCase")]
647pub struct DescribeMetadataObject {
648    /// Component name as it appears in `package.xml` (and in
649    /// `<types><name>`).
650    pub xml_name: String,
651    /// Top-level directory inside the deploy zip for components of
652    /// this type.
653    #[serde(default, deserialize_with = "deserialize_nil_string")]
654    pub directory_name: Option<String>,
655    /// File extension (without the leading dot) for component files.
656    /// `None` for types whose components live entirely inside a
657    /// `-meta.xml` file with no companion data file.
658    #[serde(default, deserialize_with = "deserialize_nil_string")]
659    pub suffix: Option<String>,
660    /// Whether components of this type live in a folder
661    /// (Dashboard / Document / EmailTemplate / Report).
662    #[serde(default)]
663    pub in_folder: bool,
664    /// Whether components of this type require a companion
665    /// `-meta.xml` file alongside the source file (ApexClass,
666    /// Document, etc.).
667    #[serde(default)]
668    pub meta_file: bool,
669    /// Names of child sub-component types (e.g. `CustomField` is a
670    /// child of `CustomObject`). Useful for crawling a metadata graph.
671    #[serde(default)]
672    pub child_xml_names: Vec<String>,
673}
674
675/// Returned by `describe_value_type`. Schema-level information about
676/// one specific metadata type — what fields it has, whether it supports
677/// CRUD operations, etc.
678#[derive(Debug, Clone, Deserialize)]
679#[serde(rename_all = "camelCase")]
680pub struct DescribeValueTypeResult {
681    /// `true` if components of this type can be created via
682    /// `create_metadata`.
683    #[serde(default)]
684    pub api_creatable: bool,
685    /// `true` if components of this type can be deleted via
686    /// `delete_metadata`.
687    #[serde(default)]
688    pub api_deletable: bool,
689    /// `true` if components of this type can be read via
690    /// `read_metadata`.
691    #[serde(default)]
692    pub api_readable: bool,
693    /// `true` if components of this type can be updated via
694    /// `update_metadata`.
695    #[serde(default)]
696    pub api_updatable: bool,
697    /// Information about the parent field for types whose `fullName`
698    /// embeds a parent identifier (e.g. `Account.MyField__c` for
699    /// `CustomField`). `None` for types with no parent.
700    #[serde(default)]
701    pub parent_field: Option<ValueTypeField>,
702    /// Fields of this metadata type.
703    #[serde(default)]
704    pub value_type_fields: Vec<ValueTypeField>,
705}
706
707/// Describes one field of a metadata type, returned inside
708/// [`DescribeValueTypeResult::value_type_fields`].
709///
710/// Self-referential — complex fields can carry nested
711/// [`fields`](Self::fields) describing their own structure (e.g. a
712/// `CustomField` value type field on `CustomObject` itself has a
713/// nested schema). Use [`Self::fields`] to walk the tree.
714#[derive(Debug, Clone, Default, Deserialize)]
715#[serde(rename_all = "camelCase")]
716pub struct ValueTypeField {
717    /// Field name. `None` for the placeholder root in `parent_field`.
718    #[serde(default, deserialize_with = "deserialize_nil_string")]
719    pub name: Option<String>,
720    /// XML Schema simple type name (e.g. `"boolean"`, `"double"`,
721    /// `"string"`).
722    #[serde(default, deserialize_with = "deserialize_nil_string")]
723    pub soap_type: Option<String>,
724    /// `1` if the field is required, `0` otherwise. (The wire uses an
725    /// XSD-style cardinality bound.)
726    #[serde(default)]
727    pub min_occurs: i32,
728    /// Whether the field must have a non-null value.
729    #[serde(default)]
730    pub value_required: bool,
731    /// Whether this field is the type's `fullName`.
732    #[serde(default)]
733    pub is_name_field: bool,
734    /// Whether this field is a foreign key to another component.
735    #[serde(default)]
736    pub is_foreign_key: bool,
737    /// Target object type when [`is_foreign_key`](Self::is_foreign_key)
738    /// is true (e.g. `"Account"`, `"Opportunity"`).
739    #[serde(default, deserialize_with = "deserialize_nil_string")]
740    pub foreign_key_domain: Option<String>,
741    /// Picklist options when this field is a picklist. Empty for
742    /// non-picklist fields.
743    #[serde(default)]
744    pub picklist_values: Vec<PicklistEntry>,
745    /// Nested fields for complex / structured value types. The wire
746    /// emits multiple `<fields>` siblings, each carrying its own
747    /// `ValueTypeField`.
748    #[serde(default)]
749    pub fields: Vec<ValueTypeField>,
750}
751
752/// One picklist option inside a [`ValueTypeField`].
753#[derive(Debug, Clone, Deserialize)]
754#[serde(rename_all = "camelCase")]
755pub struct PicklistEntry {
756    /// Wire value of the option.
757    #[serde(default, deserialize_with = "deserialize_nil_string")]
758    pub value: Option<String>,
759    /// Display label.
760    #[serde(default, deserialize_with = "deserialize_nil_string")]
761    pub label: Option<String>,
762    /// Whether this option is the default selection.
763    #[serde(default)]
764    pub default_value: bool,
765    /// Whether the option is currently active.
766    #[serde(default)]
767    pub active: bool,
768    /// Encoded `validFor` bitmap for dependent picklists. Salesforce
769    /// emits this as base64; we surface the raw string.
770    #[serde(default, deserialize_with = "deserialize_nil_string")]
771    pub valid_for: Option<String>,
772}
773
774// -- CRUD ops ----------------------------------------------------------------
775
776/// Per-component result for `createMetadata`, `updateMetadata`, and
777/// `renameMetadata`.
778///
779/// `success == true` means the component was applied; `errors` is the
780/// failure detail otherwise. A single call can have a mix of
781/// per-component successes and failures — Salesforce's default in
782/// API v34+ allows partial success.
783#[derive(Debug, Clone, Deserialize)]
784#[serde(rename_all = "camelCase")]
785pub struct SaveResult {
786    /// `fullName` of the component that was processed.
787    #[serde(default)]
788    pub full_name: String,
789    #[serde(default)]
790    pub success: bool,
791    /// Per-component errors when `success == false`.
792    #[serde(default)]
793    pub errors: Vec<MetadataApiError>,
794}
795
796/// Per-component result for `upsertMetadata`. Same shape as
797/// [`SaveResult`] plus a `created` flag that distinguishes
798/// newly-inserted components from those that were updated.
799#[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    /// `true` if the upsert resulted in a newly-created component;
807    /// `false` if an existing component was updated. Only meaningful
808    /// when `success == true`.
809    #[serde(default)]
810    pub created: bool,
811    #[serde(default)]
812    pub errors: Vec<MetadataApiError>,
813}
814
815/// Per-component result for `deleteMetadata`. Same shape as
816/// [`SaveResult`] in API v30+.
817#[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/// One error entry inside a CRUD result.
829///
830/// Distinct from [`MetadataError`](crate::MetadataError) — that's the
831/// transport-level enum; this is the per-component validation /
832/// permission failure Salesforce attaches to a SaveResult /
833/// UpsertResult / DeleteResult.
834///
835/// `status_code` is left as a `String` rather than an enum because
836/// Salesforce ships hundreds of status codes across the platform and
837/// adds new ones each release.
838#[derive(Debug, Clone, Deserialize)]
839#[serde(rename_all = "camelCase")]
840pub struct MetadataApiError {
841    /// Salesforce status code identifier (e.g. `"DUPLICATE_VALUE"`,
842    /// `"INVALID_FIELD"`). String-typed because the closed enum
843    /// would lag behind Salesforce releases.
844    #[serde(default)]
845    pub status_code: String,
846    /// Human-readable error message.
847    #[serde(default)]
848    pub message: String,
849    /// Field names involved in the error, when applicable.
850    #[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        // Salesforce extends these enums across API versions
882        // (FinalizingDeploy arrived in 65.0); an unrecognized literal
883        // must degrade to Unknown, not fail the whole response.
884        #[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()), // base64 of "hello"
929        };
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}