use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error;
use tokio::time::timeout;
use tracing::{debug, error, warn};
use uuid::Uuid;
use crate::http_tcp::{HttpTcpRequest, HttpTcpResponse};
use crate::message::Message;
use crate::communication::{MessageChannel, TcpChannel};
use crate::tcp_types::{ConnectionConfig, ConnectionState};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Error)]
pub enum HttpClientError {
#[error("Connection error: {0}")]
ConnectionError(String),
#[error("Request timed out after {0:?}")]
Timeout(Duration),
#[error("Response error ({status}): {message}")]
ResponseError {
status: u16,
message: String,
},
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("SDK error: {0}")]
SdkError(String),
#[error("Error: {0}")]
Other(String),
}
pub type HttpClientResult<T> = Result<T, HttpClientError>;
pub struct RequestBuilder {
method: String,
uri: String,
channel: Arc<TcpChannel>,
headers: HashMap<String, String>,
query_params: HashMap<String, String>,
body: Option<Vec<u8>>,
timeout: Duration,
retries: u32,
}
impl RequestBuilder {
fn new(method: &str, uri: String, channel: Arc<TcpChannel>) -> Self {
Self {
method: method.to_string(),
uri,
channel,
headers: HashMap::new(),
query_params: HashMap::new(),
body: None,
timeout: DEFAULT_TIMEOUT,
retries: 1,
}
}
pub fn header<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers.extend(headers);
self
}
pub fn query<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
self.query_params.insert(key.into(), value.into());
self
}
pub fn queries(mut self, params: HashMap<String, String>) -> Self {
self.query_params.extend(params);
self
}
pub fn body<B: Into<Vec<u8>>>(mut self, body: B) -> Self {
self.body = Some(body.into());
self
}
pub fn json<T: Serialize>(mut self, data: &T) -> HttpClientResult<Self> {
let json_body = serde_json::to_vec(data)?;
self.headers.insert("Content-Type".to_string(), "application/json".to_string());
self.body = Some(json_body);
Ok(self)
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn retries(mut self, retries: u32) -> Self {
self.retries = retries;
self
}
fn build_uri(&self) -> String {
let mut uri = self.uri.clone();
if !self.query_params.is_empty() {
if uri.contains('?') {
if !uri.ends_with('?') {
uri.push('&');
}
} else {
uri.push('?');
}
let query_string = self.query_params.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
uri.push_str(&query_string);
}
uri
}
fn build_request(&self) -> HttpTcpRequest {
let uri = self.build_uri();
let mut request = HttpTcpRequest::new(self.method.clone(), uri)
.with_headers(self.headers.clone())
.with_request_id(Uuid::new_v4().to_string());
if let Some(body) = &self.body {
request = request.with_body(body.clone());
}
request
}
pub async fn send(self) -> HttpClientResult<HttpTcpResponse> {
if MessageChannel::state(&*self.channel).await != ConnectionState::Connected {
match MessageChannel::connect(&*self.channel).await {
Ok(_) => debug!("Connected to TCP server"),
Err(e) => return Err(HttpClientError::ConnectionError(format!("Failed to connect: {}", e))),
}
}
let request = self.build_request();
let request_id = request.request_id.clone();
debug!("Sending HTTP-over-TCP request: {} {}", request.method, request.uri);
let message = Message::new(request);
let encoded = message.encode()
.map_err(|e| HttpClientError::SdkError(e.to_string()))?;
MessageChannel::send(&*self.channel, encoded).await
.map_err(|e| HttpClientError::SdkError(e.to_string()))?;
let receive_future = MessageChannel::receive(&*self.channel);
let response_encoded = match timeout(self.timeout, receive_future).await {
Ok(Ok(response)) => response,
Ok(Err(e)) => return Err(HttpClientError::SdkError(e.to_string())),
Err(_) => return Err(HttpClientError::Timeout(self.timeout)),
};
let response_json = match response_encoded.format() {
crate::message::EncodingFormat::Json => {
std::str::from_utf8(response_encoded.data())
.map_err(|e| HttpClientError::SdkError(format!("Invalid UTF-8: {}", e)))?
.to_string()
},
_ => {
let json_encoded = response_encoded.to_format(crate::message::EncodingFormat::Json)
.map_err(|e| HttpClientError::SdkError(e.to_string()))?;
std::str::from_utf8(json_encoded.data())
.map_err(|e| HttpClientError::SdkError(format!("Invalid UTF-8: {}", e)))?
.to_string()
}
};
let response: Message<HttpTcpResponse> = serde_json::from_str(&response_json)
.map_err(|e| HttpClientError::SdkError(format!("Failed to deserialize response: {}", e)))?;
let response_data = response.content().clone();
if response_data.request_id != request_id {
warn!(
"Response ID mismatch: expected {}, got {}",
request_id, response_data.request_id
);
}
Ok(response_data)
}
pub async fn send_text(self) -> HttpClientResult<String> {
let response = self.send().await?;
if !response.is_success() {
return Err(HttpClientError::ResponseError {
status: response.status_code,
message: response.body_as_string()
.unwrap_or_else(|| Ok("No response body".to_string()))
.unwrap_or_else(|_| "Failed to decode response body".to_string()),
});
}
if let Some(body) = &response.body {
String::from_utf8(body.clone())
.map_err(|_| HttpClientError::Other("Failed to decode response body as UTF-8".to_string()))
} else {
Ok(String::new())
}
}
pub async fn send_json<T: DeserializeOwned>(self) -> HttpClientResult<T> {
let response = self.send().await?;
if !response.is_success() {
return Err(HttpClientError::ResponseError {
status: response.status_code,
message: response.body_as_string()
.unwrap_or_else(|| Ok("No response body".to_string()))
.unwrap_or_else(|_| "Failed to decode response body".to_string()),
});
}
if let Some(body) = &response.body {
serde_json::from_slice(body)
.map_err(HttpClientError::SerializationError)
} else {
Err(HttpClientError::Other("No response body".to_string()))
}
}
}
pub type ResponseCallback = Box<dyn FnOnce(HttpClientResult<HttpTcpResponse>) + Send + 'static>;
pub struct HttpTcpClient {
config: ConnectionConfig,
channel: Arc<TcpChannel>,
default_timeout: Duration,
default_retries: u32,
base_url: Option<String>,
}
impl HttpTcpClient {
pub async fn new(config: ConnectionConfig) -> HttpClientResult<Self> {
let channel = TcpChannel::connect(config.clone()).await
.map_err(|e| HttpClientError::ConnectionError(e.to_string()))?;
Ok(Self {
config,
channel: Arc::new(channel),
default_timeout: DEFAULT_TIMEOUT,
default_retries: 1,
base_url: None,
})
}
pub fn with_channel(channel: Arc<TcpChannel>, config: ConnectionConfig) -> Self {
Self {
config,
channel,
default_timeout: DEFAULT_TIMEOUT,
default_retries: 1,
base_url: None,
}
}
pub fn with_base_url<S: Into<String>>(mut self, base_url: S) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn with_default_timeout(mut self, timeout: Duration) -> Self {
self.default_timeout = timeout;
self
}
pub fn with_default_retries(mut self, retries: u32) -> Self {
self.default_retries = retries;
self
}
fn resolve_url(&self, url: &str) -> String {
if url.starts_with("http://") || url.starts_with("https://") {
url.to_string()
} else if let Some(base) = &self.base_url {
let mut resolved = base.clone();
if !resolved.ends_with('/') && !url.starts_with('/') {
resolved.push('/');
} else if resolved.ends_with('/') && url.starts_with('/') {
resolved.pop();
}
resolved.push_str(url);
resolved
} else {
url.to_string()
}
}
pub fn get<S: Into<String>>(&self, url: S) -> RequestBuilder {
let url_str = self.resolve_url(&url.into());
RequestBuilder::new("GET", url_str, self.channel.clone())
.timeout(self.default_timeout)
.retries(self.default_retries)
}
pub fn post<S: Into<String>>(&self, url: S) -> RequestBuilder {
let url_str = self.resolve_url(&url.into());
RequestBuilder::new("POST", url_str, self.channel.clone())
.timeout(self.default_timeout)
.retries(self.default_retries)
}
pub fn put<S: Into<String>>(&self, url: S) -> RequestBuilder {
let url_str = self.resolve_url(&url.into());
RequestBuilder::new("PUT", url_str, self.channel.clone())
.timeout(self.default_timeout)
.retries(self.default_retries)
}
pub fn delete<S: Into<String>>(&self, url: S) -> RequestBuilder {
let url_str = self.resolve_url(&url.into());
RequestBuilder::new("DELETE", url_str, self.channel.clone())
.timeout(self.default_timeout)
.retries(self.default_retries)
}
pub fn patch<S: Into<String>>(&self, url: S) -> RequestBuilder {
let url_str = self.resolve_url(&url.into());
RequestBuilder::new("PATCH", url_str, self.channel.clone())
.timeout(self.default_timeout)
.retries(self.default_retries)
}
pub fn head<S: Into<String>>(&self, url: S) -> RequestBuilder {
let url_str = self.resolve_url(&url.into());
RequestBuilder::new("HEAD", url_str, self.channel.clone())
.timeout(self.default_timeout)
.retries(self.default_retries)
}
pub fn options<S: Into<String>>(&self, url: S) -> RequestBuilder {
let url_str = self.resolve_url(&url.into());
RequestBuilder::new("OPTIONS", url_str, self.channel.clone())
.timeout(self.default_timeout)
.retries(self.default_retries)
}
pub async fn get_json<T: DeserializeOwned, S: Into<String>>(&self, url: S) -> HttpClientResult<T> {
self.get(url).send_json().await
}
pub async fn get_text<S: Into<String>>(&self, url: S) -> HttpClientResult<String> {
self.get(url).send_text().await
}
pub async fn post_json<T: Serialize, R: DeserializeOwned, S: Into<String>>(
&self,
url: S,
data: &T,
) -> HttpClientResult<R> {
self.post(url).json(data)?.send_json().await
}
pub async fn connection_state(&self) -> ConnectionState {
MessageChannel::state(&*self.channel).await
}
pub async fn reconnect(&self) -> HttpClientResult<()> {
MessageChannel::connect(&*self.channel).await
.map_err(|e| HttpClientError::ConnectionError(e.to_string()))
}
}
impl fmt::Debug for HttpTcpClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpTcpClient")
.field("config", &self.config)
.field("default_timeout", &self.default_timeout)
.field("default_retries", &self.default_retries)
.field("base_url", &self.base_url)
.finish()
}
}
pub struct HttpClientPool {
clients: HashMap<String, Arc<HttpTcpClient>>,
}
impl HttpClientPool {
pub fn new() -> Self {
Self {
clients: HashMap::new(),
}
}
pub fn add_client<S: Into<String>>(&mut self, name: S, client: HttpTcpClient) {
self.clients.insert(name.into(), Arc::new(client));
}
pub fn get_client<S: AsRef<str>>(&self, name: S) -> Option<Arc<HttpTcpClient>> {
self.clients.get(name.as_ref()).cloned()
}
pub fn remove_client<S: AsRef<str>>(&mut self, name: S) -> Option<Arc<HttpTcpClient>> {
self.clients.remove(name.as_ref())
}
pub fn clear(&mut self) {
self.clients.clear();
}
pub fn len(&self) -> usize {
self.clients.len()
}
pub fn is_empty(&self) -> bool {
self.clients.is_empty()
}
pub fn client_names(&self) -> Vec<String> {
self.clients.keys().cloned().collect()
}
}
impl Default for HttpClientPool {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for HttpClientPool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpClientPool")
.field("clients", &self.client_names())
.finish()
}
}