use reqwest::{header, Client, ClientBuilder, Url};
use serde::de::DeserializeOwned;
use thiserror::Error;
use trust_tasks_rs::{ErrorResponse, Payload, TransportHandler, TrustTask};
use crate::handler::HttpsHandler;
#[derive(Default)]
pub struct HttpsClientBuilder {
server_url: Option<String>,
server_vid: Option<String>,
my_vid: Option<String>,
my_token: Option<String>,
strip_redundant_in_band: bool,
}
impl HttpsClientBuilder {
pub fn server_url(mut self, url: impl Into<String>) -> Self {
self.server_url = Some(url.into());
self
}
pub fn server_vid(mut self, vid: impl Into<String>) -> Self {
self.server_vid = Some(vid.into());
self
}
pub fn my_vid(mut self, vid: impl Into<String>) -> Self {
self.my_vid = Some(vid.into());
self
}
pub fn my_token(mut self, token: impl Into<String>) -> Self {
self.my_token = Some(token.into());
self
}
pub fn strip_redundant_in_band(mut self, strip: bool) -> Self {
self.strip_redundant_in_band = strip;
self
}
pub fn build(self) -> Result<HttpsClient, ClientError> {
let server_url = self
.server_url
.ok_or_else(|| ClientError::Config("server_url is required".into()))?;
let base: Url = format!("{}/trust-tasks", server_url.trim_end_matches('/'))
.parse()
.map_err(|e| ClientError::Config(format!("server_url is not a valid URL: {e}")))?;
let http = ClientBuilder::new()
.build()
.map_err(|e| ClientError::Config(e.to_string()))?;
Ok(HttpsClient {
http,
endpoint: base,
server_vid: self.server_vid,
my_vid: self.my_vid,
my_token: self.my_token,
strip_redundant_in_band: self.strip_redundant_in_band,
})
}
}
pub struct HttpsClient {
http: Client,
endpoint: Url,
server_vid: Option<String>,
my_vid: Option<String>,
my_token: Option<String>,
strip_redundant_in_band: bool,
}
impl HttpsClient {
pub fn builder() -> HttpsClientBuilder {
HttpsClientBuilder::default()
}
pub async fn send<Req, Resp>(
&self,
mut request: TrustTask<Req>,
) -> Result<TrustTask<Resp>, ClientError>
where
Req: Payload + serde::Serialize,
Resp: Payload + DeserializeOwned,
{
if request.issuer.is_none() {
request.issuer = self.my_vid.clone();
}
if request.recipient.is_none() {
request.recipient = self.server_vid.clone();
}
if request.issued_at.is_none() {
request.issued_at = Some(chrono::Utc::now());
}
if self.strip_redundant_in_band {
HttpsHandler::new(self.my_vid.clone(), self.server_vid.clone())
.prepare_outbound(&mut request);
}
let mut req = self
.http
.post(self.endpoint.clone())
.header(header::CONTENT_TYPE, "application/json")
.json(&request);
if let Some(token) = &self.my_token {
req = req.bearer_auth(token);
}
let resp = req.send().await?;
let status = resp.status();
let body = resp.bytes().await?;
if status.is_success() {
let typed: TrustTask<Resp> = serde_json::from_slice(&body)
.map_err(|e| ClientError::ResponseDecode(e.to_string()))?;
Ok(typed)
} else {
match serde_json::from_slice::<ErrorResponse>(&body) {
Ok(error_doc) => Err(ClientError::TrustTaskError {
http_status: status.as_u16(),
error: Box::new(error_doc),
}),
Err(_) => Err(ClientError::HttpStatus {
http_status: status.as_u16(),
body: String::from_utf8_lossy(&body).to_string(),
}),
}
}
}
}
#[derive(Debug, Error)]
pub enum ClientError {
#[error("client configuration error: {0}")]
Config(String),
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("server returned trust-task-error/0.1 (HTTP {http_status}): {error}")]
TrustTaskError {
http_status: u16,
error: Box<ErrorResponse>,
},
#[error("non-2xx HTTP response ({http_status}) with non-Trust-Task body: {body}")]
HttpStatus {
http_status: u16,
body: String,
},
#[error("response body did not match expected type: {0}")]
ResponseDecode(String),
}