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}
84
85// -- Deploy ------------------------------------------------------------------
86
87/// Options for a `deploy()` call.
88///
89/// All fields are optional — omitted fields are not sent and Salesforce
90/// applies its defaults. Pass `Default::default()` to use Salesforce's
91/// defaults for everything.
92///
93/// See the [DeployOptions docs] for field semantics and production-deploy
94/// requirements (e.g. `rollback_on_error` must be `true` for prod).
95///
96/// [DeployOptions docs]: https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy.htm
97#[derive(Debug, Clone, Default)]
98pub struct DeployOptions {
99    /// If `true`, the deployment proceeds even if files listed in
100    /// `package.xml` are missing from the zip. **Don't set on
101    /// production deploys.**
102    pub allow_missing_files: Option<bool>,
103    /// Reserved for future use.
104    pub auto_update_package: Option<bool>,
105    /// If `true`, performs a test deployment (validation) without
106    /// actually committing the components. Pair with
107    /// `test_level: RunLocalTests` to qualify the result for
108    /// `deploy_recent_validation`.
109    pub check_only: Option<bool>,
110    /// Continue on warnings.
111    pub ignore_warnings: Option<bool>,
112    /// Reserved for future use.
113    pub perform_retrieve: Option<bool>,
114    /// In dev/sandbox orgs only: skip the Recycle Bin when deleting
115    /// components listed in `destructiveChanges.xml`.
116    pub purge_on_delete: Option<bool>,
117    /// Required `true` for production deployments — roll back the
118    /// whole job on any failure.
119    pub rollback_on_error: Option<bool>,
120    /// Specific Apex test class names to run. Only meaningful when
121    /// `test_level` is `RunSpecifiedTests`.
122    pub run_tests: Vec<String>,
123    /// `true` if the zip is a single package; `false` for a set.
124    pub single_package: Option<bool>,
125    /// How aggressively to run tests during deployment.
126    pub test_level: Option<TestLevel>,
127}
128
129/// How much of the org's Apex test suite to run during a deployment.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum TestLevel {
132    /// No tests. Sandbox/dev only.
133    NoTestRun,
134    /// Only the classes listed in [`DeployOptions::run_tests`].
135    RunSpecifiedTests,
136    /// (Beta) Salesforce-selected relevant tests.
137    RunRelevantTests,
138    /// All non-managed-package tests in the org. Default for prod
139    /// deploys that contain Apex.
140    RunLocalTests,
141    /// Every test in the org including managed-package ones.
142    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/// Returned by `check_deploy_status`. The headline summary of a
158/// deployment.
159#[derive(Debug, Clone, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct DeployResult {
162    pub id: String,
163    /// Whether the server is done processing the job. Poll until this
164    /// is `true`.
165    #[serde(default)]
166    pub done: bool,
167    /// Overall success/failure. Only meaningful once `done == true`.
168    #[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    /// Whether Apex tests were exercised.
179    #[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    /// Free-form description of the in-progress component or test
196    /// class.
197    #[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    /// Per-component success/failure entries. Only populated when
223    /// `check_deploy_status` was called with `include_details: true`.
224    #[serde(default)]
225    pub details: Option<DeployDetails>,
226}
227
228/// State of a deployment job. See [`DeployResult::status`].
229#[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    /// Newer status, post-commit phase that can't be canceled in
239    /// API 65.0+.
240    FinalizingDeploy,
241    FinalizingDeployFailed,
242}
243
244impl DeployStatus {
245    /// True when the deploy job is finished, regardless of success.
246    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/// Per-component results bundled into a [`DeployResult`].
259#[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    /// Apex test results.
267    #[serde(default)]
268    pub run_test_result: Option<RunTestsResult>,
269}
270
271/// Per-component status entry inside [`DeployDetails`].
272#[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    /// Metadata type, e.g. `ApexClass`.
278    #[serde(default, deserialize_with = "deserialize_nil_string")]
279    pub component_type: Option<String>,
280    /// Component identifier (e.g. `MyClass`).
281    #[serde(default, deserialize_with = "deserialize_nil_string")]
282    pub full_name: Option<String>,
283    /// File path inside the deployed zip.
284    #[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    /// Error or warning message text when `success == false` or
297    /// `problem_type == Warning`.
298    #[serde(default, deserialize_with = "deserialize_nil_string")]
299    pub problem: Option<String>,
300    /// Distinguishes errors from warnings.
301    #[serde(default)]
302    pub problem_type: Option<DeployProblemType>,
303    /// Line number in a source file where the problem occurred, when
304    /// applicable (Apex class compile errors, etc.).
305    #[serde(default)]
306    pub line_number: Option<i32>,
307    /// Column number, paired with `line_number`.
308    #[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/// Apex test results inside [`DeployDetails`].
319#[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// -- Cancel ------------------------------------------------------------------
407
408/// Returned by `cancel_deploy()`. `done == false` means the cancellation
409/// is in progress; `done == true` means it landed (the deployment was
410/// either still queued or cancelled successfully).
411#[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// -- Retrieve ----------------------------------------------------------------
420
421/// Input for a `retrieve()` call.
422///
423/// At least one of [`package_names`](Self::package_names),
424/// [`specific_files`](Self::specific_files), or
425/// [`unpackaged`](Self::unpackaged) should be set — otherwise there's
426/// nothing to retrieve.
427#[derive(Debug, Clone, Default)]
428pub struct RetrieveRequest {
429    /// API version for the retrieve. The version inside `package.xml`
430    /// takes precedence in API v31+.
431    pub api_version: String,
432    /// Packaged components to retrieve by managed-package name.
433    pub package_names: Vec<String>,
434    /// `true` if the result is one package (vs. a set). Required
435    /// `true` when `specific_files` is non-empty.
436    pub single_package: bool,
437    /// Specific file paths to retrieve, e.g.
438    /// `["unpackaged/classes/MyClass.cls"]`. When set, `package_names`
439    /// must be empty and `single_package` must be `true`.
440    pub specific_files: Vec<String>,
441    /// Unpackaged components to retrieve, expressed as a
442    /// [`PackageManifest`]. Built with the same fluent API used for
443    /// generating `package.xml` files — see the manifest module
444    /// docs.
445    ///
446    /// [`PackageManifest`]: crate::PackageManifest
447    pub unpackaged: Option<crate::PackageManifest>,
448}
449
450/// Returned by `check_retrieve_status`. Once `done == true` and
451/// `success == true`, `zip_file` contains the retrieved zip bytes.
452#[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    /// Per-file properties for everything in the retrieved zip,
467    /// including the manifest.
468    #[serde(default)]
469    pub file_properties: Vec<FileProperties>,
470    /// Errors and warnings encountered during the retrieve.
471    #[serde(default)]
472    pub messages: Vec<RetrieveMessage>,
473    /// Base64-encoded zip bytes. Use [`Self::zip_bytes`] to decode.
474    /// Only populated when `done == true` and `success == true`,
475    /// and only when `check_retrieve_status` was called with
476    /// `include_zip == true`.
477    #[serde(default, deserialize_with = "deserialize_nil_string")]
478    pub zip_file: Option<String>,
479}
480
481impl RetrieveResult {
482    /// Decode the `zip_file` field from base64 into raw zip bytes.
483    /// Returns `Ok(None)` if no zip is present in the result.
484    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    /// True when the retrieve job is finished, regardless of success.
505    pub fn is_terminal(self) -> bool {
506        matches!(self, Self::Succeeded | Self::Failed)
507    }
508}
509
510/// Properties of one file inside a retrieve result.
511#[derive(Debug, Clone, Deserialize)]
512#[serde(rename_all = "camelCase")]
513pub struct FileProperties {
514    pub file_name: String,
515    pub full_name: String,
516    /// Metadata type name, e.g. `"ApexClass"`.
517    #[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/// Distribution / lifecycle state of a packaged component.
540#[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/// Error / warning surfaced in a [`RetrieveResult`].
554#[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// -- Utility ops -------------------------------------------------------------
563
564/// One query inside a `list_metadata` call.
565///
566/// At most three queries may be batched per call (Salesforce server
567/// limit). `type_name` is required; `folder` is needed for components
568/// that live under a folder (Dashboard, Document, EmailTemplate,
569/// Report).
570#[derive(Debug, Clone)]
571pub struct ListMetadataQuery {
572    /// Metadata type, e.g. `"ApexClass"`, `"CustomObject"`.
573    pub type_name: String,
574    /// Folder name when querying a folder-based type. Set to `None`
575    /// for top-level types.
576    pub folder: Option<String>,
577}
578
579/// Returned by `describe_metadata`. Catalogs the metadata types
580/// available in the target org plus a few org-wide flags useful for
581/// deciding deploy behavior.
582#[derive(Debug, Clone, Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct DescribeMetadataResult {
585    /// Per-type descriptors — directory name, file suffix, child types,
586    /// etc. One entry per metadata type the org supports.
587    #[serde(default)]
588    pub metadata_objects: Vec<DescribeMetadataObject>,
589    /// Namespace prefix for managed packages in this org. Empty
590    /// (`""`) for orgs with no namespace.
591    #[serde(default, deserialize_with = "deserialize_nil_string")]
592    pub organization_namespace: Option<String>,
593    /// Whether the org allows partial deployments (`rollbackOnError`
594    /// can be `false`). In practice this is the inverse of
595    /// [`Self::test_required`] — production-like orgs require tests
596    /// and disallow partial saves — but both fields come from the
597    /// server, so trust the wire over the invariant.
598    #[serde(default)]
599    pub partial_save_allowed: bool,
600    /// Whether Apex tests are required on deploy. See
601    /// [`Self::partial_save_allowed`] for the usual relationship.
602    #[serde(default)]
603    pub test_required: bool,
604}
605
606/// Descriptor for one metadata type, returned inside
607/// [`DescribeMetadataResult::metadata_objects`].
608///
609/// This is the source of truth for `package.xml` `<types><name>` values
610/// and for zip directory layout — `xml_name` is what goes in the
611/// manifest, `directory_name` is what the zip folder is called.
612#[derive(Debug, Clone, Deserialize)]
613#[serde(rename_all = "camelCase")]
614pub struct DescribeMetadataObject {
615    /// Component name as it appears in `package.xml` (and in
616    /// `<types><name>`).
617    pub xml_name: String,
618    /// Top-level directory inside the deploy zip for components of
619    /// this type.
620    #[serde(default, deserialize_with = "deserialize_nil_string")]
621    pub directory_name: Option<String>,
622    /// File extension (without the leading dot) for component files.
623    /// `None` for types whose components live entirely inside a
624    /// `-meta.xml` file with no companion data file.
625    #[serde(default, deserialize_with = "deserialize_nil_string")]
626    pub suffix: Option<String>,
627    /// Whether components of this type live in a folder
628    /// (Dashboard / Document / EmailTemplate / Report).
629    #[serde(default)]
630    pub in_folder: bool,
631    /// Whether components of this type require a companion
632    /// `-meta.xml` file alongside the source file (ApexClass,
633    /// Document, etc.).
634    #[serde(default)]
635    pub meta_file: bool,
636    /// Names of child sub-component types (e.g. `CustomField` is a
637    /// child of `CustomObject`). Useful for crawling a metadata graph.
638    #[serde(default)]
639    pub child_xml_names: Vec<String>,
640}
641
642/// Returned by `describe_value_type`. Schema-level information about
643/// one specific metadata type — what fields it has, whether it supports
644/// CRUD operations, etc.
645#[derive(Debug, Clone, Deserialize)]
646#[serde(rename_all = "camelCase")]
647pub struct DescribeValueTypeResult {
648    /// `true` if components of this type can be created via
649    /// `create_metadata`.
650    #[serde(default)]
651    pub api_creatable: bool,
652    /// `true` if components of this type can be deleted via
653    /// `delete_metadata`.
654    #[serde(default)]
655    pub api_deletable: bool,
656    /// `true` if components of this type can be read via
657    /// `read_metadata`.
658    #[serde(default)]
659    pub api_readable: bool,
660    /// `true` if components of this type can be updated via
661    /// `update_metadata`.
662    #[serde(default)]
663    pub api_updatable: bool,
664    /// Information about the parent field for types whose `fullName`
665    /// embeds a parent identifier (e.g. `Account.MyField__c` for
666    /// `CustomField`). `None` for types with no parent.
667    #[serde(default)]
668    pub parent_field: Option<ValueTypeField>,
669    /// Fields of this metadata type.
670    #[serde(default)]
671    pub value_type_fields: Vec<ValueTypeField>,
672}
673
674/// Describes one field of a metadata type, returned inside
675/// [`DescribeValueTypeResult::value_type_fields`].
676///
677/// Self-referential — complex fields can carry nested
678/// [`fields`](Self::fields) describing their own structure (e.g. a
679/// `CustomField` value type field on `CustomObject` itself has a
680/// nested schema). Use [`Self::fields`] to walk the tree.
681#[derive(Debug, Clone, Default, Deserialize)]
682#[serde(rename_all = "camelCase")]
683pub struct ValueTypeField {
684    /// Field name. `None` for the placeholder root in `parent_field`.
685    #[serde(default, deserialize_with = "deserialize_nil_string")]
686    pub name: Option<String>,
687    /// XML Schema simple type name (e.g. `"boolean"`, `"double"`,
688    /// `"string"`).
689    #[serde(default, deserialize_with = "deserialize_nil_string")]
690    pub soap_type: Option<String>,
691    /// `1` if the field is required, `0` otherwise. (The wire uses an
692    /// XSD-style cardinality bound.)
693    #[serde(default)]
694    pub min_occurs: i32,
695    /// Whether the field must have a non-null value.
696    #[serde(default)]
697    pub value_required: bool,
698    /// Whether this field is the type's `fullName`.
699    #[serde(default)]
700    pub is_name_field: bool,
701    /// Whether this field is a foreign key to another component.
702    #[serde(default)]
703    pub is_foreign_key: bool,
704    /// Target object type when [`is_foreign_key`](Self::is_foreign_key)
705    /// is true (e.g. `"Account"`, `"Opportunity"`).
706    #[serde(default, deserialize_with = "deserialize_nil_string")]
707    pub foreign_key_domain: Option<String>,
708    /// Picklist options when this field is a picklist. Empty for
709    /// non-picklist fields.
710    #[serde(default)]
711    pub picklist_values: Vec<PicklistEntry>,
712    /// Nested fields for complex / structured value types. The wire
713    /// emits multiple `<fields>` siblings, each carrying its own
714    /// `ValueTypeField`.
715    #[serde(default)]
716    pub fields: Vec<ValueTypeField>,
717}
718
719/// One picklist option inside a [`ValueTypeField`].
720#[derive(Debug, Clone, Deserialize)]
721#[serde(rename_all = "camelCase")]
722pub struct PicklistEntry {
723    /// Wire value of the option.
724    #[serde(default, deserialize_with = "deserialize_nil_string")]
725    pub value: Option<String>,
726    /// Display label.
727    #[serde(default, deserialize_with = "deserialize_nil_string")]
728    pub label: Option<String>,
729    /// Whether this option is the default selection.
730    #[serde(default)]
731    pub default_value: bool,
732    /// Whether the option is currently active.
733    #[serde(default)]
734    pub active: bool,
735    /// Encoded `validFor` bitmap for dependent picklists. Salesforce
736    /// emits this as base64; we surface the raw string.
737    #[serde(default, deserialize_with = "deserialize_nil_string")]
738    pub valid_for: Option<String>,
739}
740
741// -- CRUD ops ----------------------------------------------------------------
742
743/// Per-component result for `createMetadata`, `updateMetadata`, and
744/// `renameMetadata`.
745///
746/// `success == true` means the component was applied; `errors` is the
747/// failure detail otherwise. A single call can have a mix of
748/// per-component successes and failures — Salesforce's default in
749/// API v34+ allows partial success.
750#[derive(Debug, Clone, Deserialize)]
751#[serde(rename_all = "camelCase")]
752pub struct SaveResult {
753    /// `fullName` of the component that was processed.
754    #[serde(default)]
755    pub full_name: String,
756    #[serde(default)]
757    pub success: bool,
758    /// Per-component errors when `success == false`.
759    #[serde(default)]
760    pub errors: Vec<MetadataApiError>,
761}
762
763/// Per-component result for `upsertMetadata`. Same shape as
764/// [`SaveResult`] plus a `created` flag that distinguishes
765/// newly-inserted components from those that were updated.
766#[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    /// `true` if the upsert resulted in a newly-created component;
774    /// `false` if an existing component was updated. Only meaningful
775    /// when `success == true`.
776    #[serde(default)]
777    pub created: bool,
778    #[serde(default)]
779    pub errors: Vec<MetadataApiError>,
780}
781
782/// Per-component result for `deleteMetadata`. Same shape as
783/// [`SaveResult`] in API v30+.
784#[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/// One error entry inside a CRUD result.
796///
797/// Distinct from [`MetadataError`](crate::MetadataError) — that's the
798/// transport-level enum; this is the per-component validation /
799/// permission failure Salesforce attaches to a SaveResult /
800/// UpsertResult / DeleteResult.
801///
802/// `status_code` is left as a `String` rather than an enum because
803/// Salesforce ships hundreds of status codes across the platform and
804/// adds new ones each release.
805#[derive(Debug, Clone, Deserialize)]
806#[serde(rename_all = "camelCase")]
807pub struct MetadataApiError {
808    /// Salesforce status code identifier (e.g. `"DUPLICATE_VALUE"`,
809    /// `"INVALID_FIELD"`). String-typed because the closed enum
810    /// would lag behind Salesforce releases.
811    #[serde(default)]
812    pub status_code: String,
813    /// Human-readable error message.
814    #[serde(default)]
815    pub message: String,
816    /// Field names involved in the error, when applicable.
817    #[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()), // base64 of "hello"
867        };
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}