feat: add soap adapter foundation
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use crank_core::{SoapTarget, SoapVersion};
|
||||
use reqwest::{
|
||||
Client,
|
||||
header::{HeaderMap, HeaderName, HeaderValue},
|
||||
};
|
||||
|
||||
use crate::{SoapAdapterError, SoapRequest, SoapResponse, xml};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SoapAdapter {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl Default for SoapAdapter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SoapAdapter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
target: &SoapTarget,
|
||||
request: &SoapRequest,
|
||||
) -> Result<SoapResponse, SoapAdapterError> {
|
||||
let endpoint = target
|
||||
.endpoint_override
|
||||
.as_deref()
|
||||
.ok_or(SoapAdapterError::MissingEndpoint)?;
|
||||
let headers = build_headers(target, request)?;
|
||||
let envelope = xml::build_envelope(
|
||||
&target.operation_name,
|
||||
target.metadata.namespaces.first().map(String::as_str),
|
||||
&request.body,
|
||||
envelope_namespace(target.soap_version),
|
||||
);
|
||||
let response = self
|
||||
.client
|
||||
.post(endpoint)
|
||||
.headers(headers)
|
||||
.body(envelope)
|
||||
.timeout(Duration::from_millis(request.timeout_ms))
|
||||
.send()
|
||||
.await?;
|
||||
let status = response.status();
|
||||
let headers = normalize_headers(response.headers());
|
||||
let body_text = response.text().await?;
|
||||
let body = xml::decode_envelope(&body_text)?;
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(SoapAdapterError::UnexpectedStatus {
|
||||
status: status.as_u16(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(SoapResponse {
|
||||
status_code: status.as_u16(),
|
||||
headers,
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_headers(
|
||||
target: &SoapTarget,
|
||||
request: &SoapRequest,
|
||||
) -> Result<HeaderMap, SoapAdapterError> {
|
||||
let mut headers = HeaderMap::new();
|
||||
let content_type = match target.soap_version {
|
||||
SoapVersion::Soap11 => "text/xml; charset=utf-8".to_owned(),
|
||||
SoapVersion::Soap12 => match target.soap_action.as_deref() {
|
||||
Some(action) => format!(r#"application/soap+xml; charset=utf-8; action="{action}""#),
|
||||
None => "application/soap+xml; charset=utf-8".to_owned(),
|
||||
},
|
||||
};
|
||||
headers.insert(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(&content_type).map_err(|_| SoapAdapterError::MissingEndpoint)?,
|
||||
);
|
||||
headers.insert(
|
||||
reqwest::header::ACCEPT,
|
||||
HeaderValue::from_static("application/soap+xml, text/xml, application/xml"),
|
||||
);
|
||||
|
||||
if matches!(target.soap_version, SoapVersion::Soap11) {
|
||||
if let Some(action) = target.soap_action.as_deref() {
|
||||
headers.insert(
|
||||
HeaderName::from_static("soapaction"),
|
||||
HeaderValue::from_str(action).map_err(|_| SoapAdapterError::MissingEndpoint)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (name, value) in &request.headers {
|
||||
let header_name =
|
||||
HeaderName::try_from(name.as_str()).map_err(|_| SoapAdapterError::MissingEndpoint)?;
|
||||
let header_value =
|
||||
HeaderValue::try_from(value).map_err(|_| SoapAdapterError::MissingEndpoint)?;
|
||||
headers.insert(header_name, header_value);
|
||||
}
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
fn envelope_namespace(version: SoapVersion) -> &'static str {
|
||||
match version {
|
||||
SoapVersion::Soap11 => "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
SoapVersion::Soap12 => "http://www.w3.org/2003/05/soap-envelope",
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_headers(headers: &HeaderMap) -> BTreeMap<String, String> {
|
||||
headers
|
||||
.iter()
|
||||
.filter_map(|(name, value)| {
|
||||
value
|
||||
.to_str()
|
||||
.ok()
|
||||
.map(|value| (name.as_str().to_owned(), value.to_owned()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::{Router, body::Bytes, routing::post};
|
||||
use crank_core::{SoapBindingStyle, SoapOperationMetadata, SoapTarget, SoapVersion, Target};
|
||||
use serde_json::json;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::{SoapAdapter, SoapRequest};
|
||||
|
||||
async fn spawn_server() -> String {
|
||||
async fn handler(body: Bytes) -> String {
|
||||
let text = String::from_utf8_lossy(&body);
|
||||
assert!(text.contains("<email>user@example.com</email>"));
|
||||
r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body>
|
||||
<CreateLeadResponse>
|
||||
<id>lead_123</id>
|
||||
<status>created</status>
|
||||
</CreateLeadResponse>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"#
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
let app = Router::new().route("/", post(handler));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
fn test_target(endpoint: String) -> SoapTarget {
|
||||
match Target::Soap(SoapTarget {
|
||||
wsdl_ref: "sample_wsdl".into(),
|
||||
service_name: "LeadService".to_owned(),
|
||||
port_name: "LeadPort".to_owned(),
|
||||
operation_name: "CreateLead".to_owned(),
|
||||
endpoint_override: Some(endpoint),
|
||||
soap_version: SoapVersion::Soap11,
|
||||
soap_action: Some("urn:createLead".to_owned()),
|
||||
binding_style: SoapBindingStyle::DocumentLiteral,
|
||||
headers: Vec::new(),
|
||||
fault_contract: None,
|
||||
metadata: SoapOperationMetadata {
|
||||
input_part_names: vec!["CreateLeadRequest".to_owned()],
|
||||
output_part_names: vec!["CreateLeadResponse".to_owned()],
|
||||
namespaces: vec!["urn:crm".to_owned()],
|
||||
},
|
||||
}) {
|
||||
Target::Soap(target) => target,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_soap_request_response() {
|
||||
let endpoint = spawn_server().await;
|
||||
let adapter = SoapAdapter::new();
|
||||
let response = adapter
|
||||
.execute(
|
||||
&test_target(endpoint),
|
||||
&SoapRequest {
|
||||
headers: BTreeMap::new(),
|
||||
body: json!({ "email": "user@example.com" }),
|
||||
timeout_ms: 1_000,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
assert_eq!(response.body["id"], "lead_123");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user