use std::time::{Duration, Instant};
use super::request::HttpRequest;
use super::response::HttpResponse;
use super::types::{HttpColors, HttpMethod, RequestState, ResponseView};
use crate::utils::format_size_compact;
use crate::widget::traits::WidgetProps;
pub struct HttpClient {
pub(super) request: HttpRequest,
pub(super) response: Option<HttpResponse>,
pub(super) state: RequestState,
pub(super) error: Option<String>,
pub(super) view: ResponseView,
pub(super) colors: HttpColors,
pub(super) url_cursor: usize,
pub(super) body_scroll: usize,
pub(super) history: Vec<HttpRequest>,
pub(super) history_index: usize,
pub(super) show_headers: bool,
pub props: WidgetProps,
}
impl HttpClient {
pub fn new() -> Self {
Self {
request: HttpRequest::default(),
response: None,
state: RequestState::Idle,
error: None,
view: ResponseView::Body,
colors: HttpColors::default(),
url_cursor: 0,
body_scroll: 0,
history: Vec::new(),
history_index: 0,
show_headers: false,
props: WidgetProps::new(),
}
}
pub fn url(mut self, url: impl Into<String>) -> Self {
let url_str = url.into();
if let Some(valid_req) = HttpRequest::new(&url_str) {
self.request.url = valid_req.url;
} else {
self.error = Some("Invalid URL: only http:// and https:// schemes are allowed, and URL must not exceed 8192 characters".to_string());
self.state = RequestState::Error;
}
self.url_cursor = self.request.url.len();
self
}
pub fn method(mut self, method: HttpMethod) -> Self {
self.request.method = method;
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.request.headers.insert(key.into(), value.into());
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.request.body = body.into();
self
}
pub fn colors(mut self, colors: HttpColors) -> Self {
self.colors = colors;
self
}
pub fn request(&self) -> &HttpRequest {
&self.request
}
pub fn request_mut(&mut self) -> &mut HttpRequest {
&mut self.request
}
pub fn response(&self) -> Option<&HttpResponse> {
self.response.as_ref()
}
pub fn state(&self) -> RequestState {
self.state
}
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
pub fn set_view(&mut self, view: ResponseView) {
self.view = view;
}
pub fn toggle_headers(&mut self) {
self.show_headers = !self.show_headers;
}
pub fn set_url(&mut self, url: impl Into<String>) {
let url_str = url.into();
if let Some(valid_req) = HttpRequest::new(&url_str) {
self.request.url = valid_req.url;
self.url_cursor = self.request.url.len();
} else {
self.error = Some("Invalid URL: only http:// and https:// schemes are allowed, and URL must not exceed 8192 characters".to_string());
self.state = RequestState::Error;
}
}
pub fn cycle_method(&mut self) {
self.request.method = match self.request.method {
HttpMethod::GET => HttpMethod::POST,
HttpMethod::POST => HttpMethod::PUT,
HttpMethod::PUT => HttpMethod::DELETE,
HttpMethod::DELETE => HttpMethod::PATCH,
HttpMethod::PATCH => HttpMethod::HEAD,
HttpMethod::HEAD => HttpMethod::OPTIONS,
HttpMethod::OPTIONS => HttpMethod::GET,
};
}
pub fn send(&mut self) {
self.state = RequestState::Sending;
self.error = None;
self.history.push(self.request.clone());
self.history_index = self.history.len();
let start = Instant::now();
let mock_body = r#"{
"status": "success",
"message": "Request received",
"timestamp": "2024-01-01T00:00:00Z"
}"#;
self.response = Some(HttpResponse {
status: 200,
status_text: "OK".to_string(),
headers: [
("Content-Type".to_string(), "application/json".to_string()),
("Content-Length".to_string(), mock_body.len().to_string()),
]
.into_iter()
.collect(),
body: mock_body.to_string(),
time: start.elapsed(),
size: mock_body.len(),
});
self.state = RequestState::Success;
}
pub fn set_response(&mut self, response: HttpResponse) {
let is_success = response.is_success();
self.response = Some(response);
self.state = if is_success {
RequestState::Success
} else {
RequestState::Error
};
}
pub fn set_error(&mut self, error: impl Into<String>) {
self.error = Some(error.into());
self.state = RequestState::Error;
}
pub fn clear(&mut self) {
self.response = None;
self.error = None;
self.state = RequestState::Idle;
self.body_scroll = 0;
}
pub fn scroll_down(&mut self, amount: usize) {
self.body_scroll = self.body_scroll.saturating_add(amount);
}
pub fn scroll_up(&mut self, amount: usize) {
self.body_scroll = self.body_scroll.saturating_sub(amount);
}
pub fn history_back(&mut self) {
if self.history_index > 0 {
self.history_index -= 1;
if let Some(req) = self.history.get(self.history_index) {
self.request = req.clone();
}
}
}
pub fn history_forward(&mut self) {
if self.history_index < self.history.len() {
self.history_index += 1;
if let Some(req) = self.history.get(self.history_index) {
self.request = req.clone();
}
}
}
#[doc(hidden)]
pub fn colors_for_testing(&self) -> &HttpColors {
&self.colors
}
#[doc(hidden)]
pub fn view_for_testing(&self) -> ResponseView {
self.view
}
#[doc(hidden)]
pub fn show_headers_for_testing(&self) -> bool {
self.show_headers
}
#[doc(hidden)]
pub fn url_cursor_for_testing(&self) -> usize {
self.url_cursor
}
#[doc(hidden)]
pub fn body_scroll_for_testing(&self) -> usize {
self.body_scroll
}
#[doc(hidden)]
pub fn set_body_scroll_for_testing(&mut self, value: usize) {
self.body_scroll = value;
}
#[doc(hidden)]
pub fn history_for_testing(&self) -> &[HttpRequest] {
&self.history
}
#[doc(hidden)]
pub fn history_index_for_testing(&self) -> usize {
self.history_index
}
#[doc(hidden)]
pub fn add_to_history_for_testing(&mut self) {
self.history.push(self.request.clone());
self.history_index = self.history.len();
}
#[doc(hidden)]
pub fn add_default_to_history_for_testing(&mut self) {
self.history.push(HttpRequest::default());
self.history_index = 1;
}
pub(super) fn format_duration(d: Duration) -> String {
let ms = d.as_millis();
if ms < 1000 {
format!("{}ms", ms)
} else {
format!("{:.2}s", d.as_secs_f64())
}
}
pub(super) fn format_size(bytes: usize) -> String {
format_size_compact(bytes as u64)
}
}
impl Default for HttpClient {
fn default() -> Self {
Self::new()
}
}