use std::collections::HashMap;
use std::fmt;
use crate::clients::errors::InvalidHttpRequestError;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
}
impl fmt::Display for HttpMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Get => write!(f, "get"),
Self::Post => write!(f, "post"),
Self::Put => write!(f, "put"),
Self::Delete => write!(f, "delete"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DataType {
Json,
GraphQL,
}
impl DataType {
#[must_use]
pub const fn as_content_type(&self) -> &'static str {
match self {
Self::Json => "application/json",
Self::GraphQL => "application/graphql",
}
}
}
#[derive(Clone, Debug)]
pub struct HttpRequest {
pub http_method: HttpMethod,
pub path: String,
pub body: Option<serde_json::Value>,
pub body_type: Option<DataType>,
pub query: Option<HashMap<String, String>>,
pub extra_headers: Option<HashMap<String, String>>,
pub tries: u32,
}
impl HttpRequest {
#[must_use]
pub fn builder(method: HttpMethod, path: impl Into<String>) -> HttpRequestBuilder {
HttpRequestBuilder::new(method, path)
}
pub fn verify(&self) -> Result<(), InvalidHttpRequestError> {
if self.body.is_some() && self.body_type.is_none() {
return Err(InvalidHttpRequestError::MissingBodyType);
}
if matches!(self.http_method, HttpMethod::Post | HttpMethod::Put) && self.body.is_none() {
return Err(InvalidHttpRequestError::MissingBody {
method: self.http_method.to_string(),
});
}
Ok(())
}
}
#[derive(Debug)]
pub struct HttpRequestBuilder {
http_method: HttpMethod,
path: String,
body: Option<serde_json::Value>,
body_type: Option<DataType>,
query: Option<HashMap<String, String>>,
extra_headers: Option<HashMap<String, String>>,
tries: u32,
}
impl HttpRequestBuilder {
fn new(method: HttpMethod, path: impl Into<String>) -> Self {
Self {
http_method: method,
path: path.into(),
body: None,
body_type: None,
query: None,
extra_headers: None,
tries: 1,
}
}
#[must_use]
pub fn body(mut self, body: impl Into<serde_json::Value>) -> Self {
self.body = Some(body.into());
self
}
#[must_use]
pub const fn body_type(mut self, body_type: DataType) -> Self {
self.body_type = Some(body_type);
self
}
#[must_use]
pub fn query(mut self, query: HashMap<String, String>) -> Self {
self.query = Some(query);
self
}
#[must_use]
pub fn query_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.query
.get_or_insert_with(HashMap::new)
.insert(key.into(), value.into());
self
}
#[must_use]
pub fn extra_headers(mut self, headers: HashMap<String, String>) -> Self {
self.extra_headers = Some(headers);
self
}
#[must_use]
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra_headers
.get_or_insert_with(HashMap::new)
.insert(key.into(), value.into());
self
}
#[must_use]
pub const fn tries(mut self, tries: u32) -> Self {
self.tries = tries;
self
}
pub fn build(self) -> Result<HttpRequest, InvalidHttpRequestError> {
let request = HttpRequest {
http_method: self.http_method,
path: self.path,
body: self.body,
body_type: self.body_type,
query: self.query,
extra_headers: self.extra_headers,
tries: self.tries,
};
request.verify()?;
Ok(request)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_http_method_display() {
assert_eq!(HttpMethod::Get.to_string(), "get");
assert_eq!(HttpMethod::Post.to_string(), "post");
assert_eq!(HttpMethod::Put.to_string(), "put");
assert_eq!(HttpMethod::Delete.to_string(), "delete");
}
#[test]
fn test_data_type_content_type() {
assert_eq!(DataType::Json.as_content_type(), "application/json");
assert_eq!(DataType::GraphQL.as_content_type(), "application/graphql");
}
#[test]
fn test_builder_creates_valid_get_request() {
let request = HttpRequest::builder(HttpMethod::Get, "products.json")
.build()
.unwrap();
assert_eq!(request.http_method, HttpMethod::Get);
assert_eq!(request.path, "products.json");
assert!(request.body.is_none());
assert!(request.body_type.is_none());
assert_eq!(request.tries, 1);
}
#[test]
fn test_builder_creates_valid_post_request() {
let request = HttpRequest::builder(HttpMethod::Post, "products.json")
.body(json!({"product": {"title": "Test"}}))
.body_type(DataType::Json)
.build()
.unwrap();
assert_eq!(request.http_method, HttpMethod::Post);
assert!(request.body.is_some());
assert_eq!(request.body_type, Some(DataType::Json));
}
#[test]
fn test_verify_requires_body_for_post() {
let result = HttpRequest::builder(HttpMethod::Post, "products.json").build();
assert!(matches!(
result,
Err(InvalidHttpRequestError::MissingBody { method }) if method == "post"
));
}
#[test]
fn test_verify_requires_body_for_put() {
let result = HttpRequest::builder(HttpMethod::Put, "products/123.json").build();
assert!(matches!(
result,
Err(InvalidHttpRequestError::MissingBody { method }) if method == "put"
));
}
#[test]
fn test_verify_requires_body_type_when_body_present() {
let request = HttpRequest {
http_method: HttpMethod::Get,
path: "test".to_string(),
body: Some(json!({"key": "value"})),
body_type: None,
query: None,
extra_headers: None,
tries: 1,
};
assert!(matches!(
request.verify(),
Err(InvalidHttpRequestError::MissingBodyType)
));
}
#[test]
fn test_builder_with_query_params() {
let request = HttpRequest::builder(HttpMethod::Get, "products.json")
.query_param("limit", "50")
.query_param("page_info", "abc123")
.build()
.unwrap();
let query = request.query.unwrap();
assert_eq!(query.get("limit"), Some(&"50".to_string()));
assert_eq!(query.get("page_info"), Some(&"abc123".to_string()));
}
#[test]
fn test_builder_with_extra_headers() {
let request = HttpRequest::builder(HttpMethod::Get, "products.json")
.header("X-Custom-Header", "custom-value")
.build()
.unwrap();
let headers = request.extra_headers.unwrap();
assert_eq!(
headers.get("X-Custom-Header"),
Some(&"custom-value".to_string())
);
}
#[test]
fn test_default_tries_is_one() {
let request = HttpRequest::builder(HttpMethod::Get, "test")
.build()
.unwrap();
assert_eq!(request.tries, 1);
}
}