use reqwest::Method;
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use crate::error::{Result, RurlError};
#[derive(Debug, Clone)]
pub struct HttpRequest {
pub method: Method,
pub url: String,
pub headers: HashMap<String, String>,
pub body: Option<String>,
pub timeout: Duration,
pub follow_redirects: bool,
}
impl HttpRequest {
pub fn new(url: impl Into<String>) -> Self {
Self {
method: Method::GET,
url: url.into(),
headers: HashMap::new(),
body: None,
timeout: Duration::from_secs(30),
follow_redirects: true,
}
}
pub fn method(mut self, method: &str) -> Result<Self> {
self.method = method.to_uppercase().parse().map_err(|_| {
RurlError::InvalidMethod(method.to_string())
})?;
Ok(self)
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn headers_from_strings(mut self, headers: &[String]) -> Result<Self> {
for header in headers {
let parts: Vec<&str> = header.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(RurlError::InvalidHeader(header.clone()));
}
self.headers.insert(
parts[0].trim().to_string(),
parts[1].trim().to_string(),
);
}
Ok(self)
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn body_from_file(mut self, path: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
self.body = Some(content);
Ok(self)
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn follow_redirects(mut self, follow: bool) -> Self {
self.follow_redirects = follow;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_request() {
let request = HttpRequest::new("https://example.com");
assert_eq!(request.url, "https://example.com");
assert_eq!(request.method, Method::GET);
assert!(request.follow_redirects);
}
#[test]
fn test_method_post() {
let request = HttpRequest::new("https://example.com")
.method("POST")
.unwrap();
assert_eq!(request.method, Method::POST);
}
#[test]
fn test_method_case_insensitive() {
let request = HttpRequest::new("https://example.com")
.method("post")
.unwrap();
assert_eq!(request.method, Method::POST);
}
#[test]
fn test_invalid_method() {
let result = HttpRequest::new("https://example.com")
.method("");
assert!(result.is_err());
}
#[test]
fn test_custom_method_allowed() {
let result = HttpRequest::new("https://example.com")
.method("CUSTOM");
assert!(result.is_ok());
}
#[test]
fn test_headers() {
let request = HttpRequest::new("https://example.com")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token");
assert_eq!(request.headers.len(), 2);
assert_eq!(request.headers.get("Content-Type"), Some(&"application/json".to_string()));
}
#[test]
fn test_headers_from_strings() {
let headers = vec![
"Content-Type: application/json".to_string(),
"X-Custom: value".to_string(),
];
let request = HttpRequest::new("https://example.com")
.headers_from_strings(&headers)
.unwrap();
assert_eq!(request.headers.get("Content-Type"), Some(&"application/json".to_string()));
assert_eq!(request.headers.get("X-Custom"), Some(&"value".to_string()));
}
#[test]
fn test_invalid_header_format() {
let headers = vec!["invalid-header-no-colon".to_string()];
let result = HttpRequest::new("https://example.com")
.headers_from_strings(&headers);
assert!(result.is_err());
}
#[test]
fn test_body() {
let request = HttpRequest::new("https://example.com")
.body(r#"{"key": "value"}"#);
assert_eq!(request.body, Some(r#"{"key": "value"}"#.to_string()));
}
#[test]
fn test_timeout() {
let request = HttpRequest::new("https://example.com")
.timeout(Duration::from_secs(60));
assert_eq!(request.timeout, Duration::from_secs(60));
}
}