# cirrus-metadata
Salesforce Metadata API (SOAP) client for the Cirrus SDK.
This project is in no way affiliated with Salesforce.
`cirrus-metadata` is the SOAP-Metadata-API sibling of
[`cirrus`](../cirrus/) (REST) and [`cirrus-auth`](../cirrus-auth/) (OAuth).
It covers the surfaces that don't exist in the Metadata REST API: file-based
`retrieve`, the CRUD-Based Calls (`createMetadata` / `readMetadata` /
`updateMetadata` / `upsertMetadata` / `deleteMetadata` / `renameMetadata`),
and the utility surface (`listMetadata`, `describeMetadata`,
`describeValueType`).
## Why a SOAP crate in 2026?
Salesforce's Metadata *REST* API only covers four `deployRequest`
endpoints (initiate, status, cancel, quick-deploy). Everything else —
including `retrieve` itself — is SOAP-only and is not deprecated. If you
already have `cirrus` and only need to ship a `.zip` to an org,
`Cirrus::metadata()` (REST) is enough. Reach for `cirrus-metadata` when
you need anything beyond `deployRequest`.
## Relationship to `cirrus` and `cirrus-auth`
- Depends only on `cirrus-auth` — no `cirrus` dependency, so callers that
use the SOAP API but not the REST client don't pull in the REST surface.
- Re-exports the auth crate as `cirrus_metadata::auth::*`, so a stand-alone
`cirrus-metadata` user can write `use cirrus_metadata::auth::JwtAuth;`
without an explicit `cirrus-auth` line in `Cargo.toml`.
- Shares the `AuthSession` trait with `cirrus`: one `Arc<dyn AuthSession>`
drives both clients side-by-side.
## What's covered
- **File-based deploy/retrieve** — `deploy`, `check_deploy_status`,
`cancel_deploy`, `deploy_recent_validation`, `retrieve`,
`check_retrieve_status`, plus `wait_for_deploy` / `wait_for_retrieve`
polling helpers with configurable timeout and backoff.
- **CRUD-based calls** — `create_metadata`, `read_metadata`,
`update_metadata`, `upsert_metadata`, `delete_metadata`,
`rename_metadata`. Up to 10 components per call, per the Metadata API
contract.
- **Utility** — `list_metadata`, `describe_metadata`, `describe_value_type`.
- **Typed `package.xml`** — [`PackageManifest`] builder with the full
`MetadataType` taxonomy and round-trippable XML serialization.
- **Open-ended escape hatch** — `MetadataClient::request_builder()` for
hand-rolling envelopes against operations the typed surface hasn't
modeled, with `SoapOperation` carrying the typed call path.
- **Cross-cutting** — retry/backoff via [`RetryPolicy`], automatic
`INVALID_SESSION_ID` refresh against the configured `AuthSession`, SOAP
fault parsing into typed [`MetadataError::SoapFault`].
## Design principles
- **No user-facing types.** The 200+ concrete metadata types
(`CustomObject`, `ApexClass`, `Flow`, …) are caller-supplied XML or
generic via `serde`. Only platform-contract envelopes are typed.
- **No legacy surface.** Operations Salesforce labels deprecated
(pre-API-31 `create` / `update` / `delete`) are intentionally not
exposed.
- **Doc-driven wire shapes.** Every handler ships with wiremock coverage
whose request/response bodies match Salesforce's documented examples.
## Quick start
```toml
[dependencies]
cirrus-metadata = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
```rust,ignore
use cirrus_metadata::auth::StaticTokenAuth;
use cirrus_metadata::{
ListMetadataQuery, MetadataClient, PackageManifest, RetrieveRequest,
};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let auth = Arc::new(StaticTokenAuth::new(
std::env::var("SF_ACCESS_TOKEN")?,
std::env::var("SF_INSTANCE_URL")?,
));
let md = MetadataClient::builder().auth(auth).build()?;
// List every Apex class in the org.
let classes = md
.list_metadata(vec![ListMetadataQuery {
type_name: "ApexClass".into(),
folder: None,
}])
.await?;
for f in &classes {
println!("{}\t{}", f.full_name, f.id.as_deref().unwrap_or(""));
}
// Build a retrieve manifest and pull the matching components.
let manifest = PackageManifest::new(md.api_version())
.add("ApexClass", ["MyService"])
.add("CustomObject", ["Account"]);
let async_result = md
.retrieve(RetrieveRequest {
api_version: md.api_version().to_string(),
package_names: vec![],
single_package: true,
specific_files: vec![],
unpackaged: Some(manifest),
})
.await?;
let result = md.wait_for_retrieve(&async_result.id).await?;
if let Some(zip) = result.zip_file {
fs_err::write("retrieved.zip", zip)?;
}
Ok(())
}
```
## Errors
`MetadataError` covers transport failures, SOAP faults
(`MetadataError::SoapFault { fault_code, fault_string, .. }`), per-component
API errors (`MetadataError::Api`), envelope parsing failures, and auth
errors (`#[from] AuthError`). Use `?` to propagate from any
`AuthSession::access_token` call alongside SOAP traffic.
`MetadataError` is `#[non_exhaustive]` so future variants don't break
downstream `match` arms.
## Integration tests
Live tests against a real sandbox / Developer Edition / scratch org live
under `tests/integration/` and are `#[ignore]`-gated. They share the
workspace's `.env` and the same URL safety guard as `cirrus` and
`cirrus-auth` — see the workspace-root `.env.example`.
```bash
cargo nextest run -p cirrus-metadata --test integration \
--run-ignored only -- --test-threads=1
```
## License
Licensed under the MIT license.