rsoap 0.3.0

A SOAP client library for Rust with compile-time code generation from WSDL files
Documentation

rsoap

Rust Version Docs.rs License Downloads

A SOAP client library for Rust with compile-time code generation from WSDL files. The rsoap workspace provides a runtime client (rsoap) and a procedural macro (rsoap-macros) that parses a WSDL at compile time and generates typed request/response structs, so SOAP services feel like ordinary Rust APIs.


Workspace

Crate Role
rsoap/ Runtime library — SoapClient, SoapOperation trait, envelope/XML parsing, SoapError
rsoap-macros/ Proc-macro crate — WSDL parser → typed struct generation
examples/ Demo — weather service (hand-written SoapOperation impl)

Quick Start

Add the runtime crate and the derive macro to your Cargo.toml:

[dependencies]
rsoap = { path = "../rsoap", features = ["wss"] }  # omit "wss" if you don't need mTLS
rsoap-macros = { path = "../rsoap-macros" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
anyhow = "1"

Derive a typed operation from a WSDL at compile time:

use rsoap::{SoapClient, SoapOperation, SoapOperation as SoapOperationMacro};

#[derive(SoapOperationMacro)]
#[soap(wsdl = "wsdl/weather.wsdl", operation_name = "GetTemperature")]
pub struct GetTemperature;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = SoapClient::new(GetTemperature::ENDPOINT)?
        .with_header("X-Api-Key", "test-key");

    let request = gettemperature::Request { zipcode: "90210".into() };
    let response = client.call(&GetTemperature, &request).await?;
    println!("{} {}", response.temperature, response.unit);
    Ok(())
}

The macro reads the WSDL, resolves the GetTemperature operation, generates gettemperature::Request and gettemperature::Response with #[serde(rename = "...")] on every field, and implements SoapOperation for the marker struct.

Two-way SSL / mutual TLS (WS-Security)

For services that require client certificates ("Services will be available only over two-way SSL over HTTP"), enable the wss feature and pass a PEM-encoded cert + key to the client:

use rsoap::SoapClient;

let client = SoapClient::new("https://example.com/soap")?
    .with_client_cert("/path/to/client.pem")?;

The certificate is presented on every request, satisfying the transport-binding requirement of OASIS WSS 1.1. For PKCS#12 (.p12 / .pfx) bundles, build the reqwest::Identity yourself (requires reqwest's default-tls feature) and use SoapClient::with_identity.


Features

  • Compile-time WSDL parsing — no runtime XML schema download, no manual struct definitions.
  • Typed request/response — generated structs with serde rename attributes match the XSD element names.
  • maxOccurs="unbounded" — automatically wrapped in Vec<T>.
  • XSD → Rust type mappingxs:stringString, xs:inti32, xs:longi64, xs:float/xs:double/xs:decimalf64, xs:booleanbool, xs:date/xs:dateTimeString.
  • SOAP 1.1 & 1.2 support — version is per-operation (const VERSION: SoapVersion) and auto-detected from the WSDL binding namespace. 1.1 uses text/xml + SOAPAction; 1.2 uses application/soap+xml with the action in the Content-Type parameter, and parses the 1.2 fault structure (<Code><Value> / <Reason><Text>).
  • Fault detection on any HTTP status — non-2xx responses are still read and checked for a SOAP fault body before reporting HttpStatus.
  • WS-Security transport binding (mTLS) — opt-in via the wss Cargo feature. Loads a PEM-encoded client certificate + key and presents it on every request, matching the "two-way SSL over HTTP" requirement from OASIS WSS 1.1.
  • Custom headers.with_header(name, value) for auth, tracing, etc.
  • Namespace-prefix tolerant — handles xs:, xsd:, wsdl:, soap:, wsdlsoap:, env:, and bare tags in WSDLs.

SoapError

pub enum SoapError {
    Http(String),
    SerializeRequest,
    DeserializeResponse(Box<dyn std::error::Error + Send + Sync>),
    SoapFault { code: String, message: String },
    OperationNotFound,
    NoEndpoint,
}

Implements From<reqwest::Error> for ergonomic ? propagation.


SoapOperation trait

pub trait SoapOperation: Send + Sync {
    type Request: Serialize;
    type Response: for<'de> Deserialize<'de>;

    const ACTION: &'static str;
    const ENDPOINT: &'static str;
    const BODY_ELEMENT: &'static str;
    /// SOAP protocol version. Defaults to V11; the derive macro auto-detects
    /// from the WSDL binding namespace (V12 if the WSDL uses
    /// `http://schemas.xmlsoap.org/wsdl/soap12/`).
    const VERSION: SoapVersion = SoapVersion::V11;

    fn build_request_body(&self, req: &Self::Request) -> Result<(String, String), quick_xml::se::SeError> {
        let action = Self::ACTION.to_string();
        let xml = quick_xml::se::to_string_with_root(Self::BODY_ELEMENT, req)?;
        Ok((action, xml))
    }

    fn parse_response(&self, xml: &str) -> Result<Self::Response, SoapError> { /* default */ }
}

A hand-written operation can declare a SOAP 1.2 version explicitly:

impl SoapOperation for MyOp {
    // ...
    const VERSION: SoapVersion = SoapVersion::V12;
}

Build & Test

# Build everything
cargo build --workspace

# Lint (correctness is deny-level — hard failures)
cargo clippy --workspace

# Run all tests (unit + integration + doc tests)
cargo test --workspace

# Run specific test suites
cargo test -p rsoap-macros --lib              # macro unit tests
cargo test -p rsoap --test integration_test   # wiremock e2e tests
cargo run -p rsoap-examples                    # run the demo

Configuration

The workspace enforces strict lints at the root Cargo.toml:

[workspace.lints.clippy]
correctness = { level = "deny" }
suspicious  = { level = "warn" }
style       = { level = "warn" }
complexity  = { level = "warn" }
perf        = { level = "warn" }

[workspace.lints.rust]
missing_docs = "warn"
unsafe_code  = "deny"

unsafe is never allowed. missing_docs is warn-level — doc comments on public items are encouraged.


Limitations

  • The macro uses a lightweight XML walker tuned for WSDL — it handles namespace prefixes (xs:, xsd:, wsdlsoap:, wsdlsoap12:, env:), self-closing tags (<xs:element name="x"/>), and attributes on opening tags. It is not a general XML schema validator: CDATA sections, comments containing tag-like text, or pathological nesting (same-name elements nested inside themselves) may confuse it. Standard WSDLs from SoapUI, WSDL2Java, .NET, and similar tools work correctly.
  • No MTOM / attachments.
  • rsoap-macros reads the WSDL at compile time, so the file path must be valid relative to the crate root where #[derive] is invoked.

License

MIT — see LICENSE.