use std::collections::HashMap;
use bytes::Bytes;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Method {
GET,
POST,
PUT,
DELETE,
HEAD,
OPTIONS,
PATCH,
}
impl Method {
pub fn as_str(&self) -> &'static str {
match self {
Method::GET => "GET",
Method::POST => "POST",
Method::PUT => "PUT",
Method::DELETE => "DELETE",
Method::HEAD => "HEAD",
Method::OPTIONS => "OPTIONS",
Method::PATCH => "PATCH",
}
}
}
#[derive(Debug, Clone)]
pub enum Body {
None,
Text(String),
Bytes(Bytes),
Json(serde_json::Value),
}
impl Body {
pub fn is_empty(&self) -> bool {
matches!(self, Body::None)
}
pub fn as_bytes(&self) -> Vec<u8> {
match self {
Body::None => Vec::new(),
Body::Text(s) => s.as_bytes().to_vec(),
Body::Bytes(b) => b.to_vec(),
Body::Json(v) => v.to_string().into_bytes(),
}
}
pub fn len(&self) -> usize {
match self {
Body::None => 0,
Body::Text(s) => s.len(),
Body::Bytes(b) => b.len(),
Body::Json(v) => v.to_string().len(),
}
}
}
#[derive(Debug, Clone)]
pub struct Request {
pub method: Method,
pub url: String,
pub headers: HashMap<String, String>,
pub body: Body,
}
impl Request {
pub fn builder() -> RequestBuilder {
RequestBuilder::new()
}
pub fn get(url: impl Into<String>) -> RequestBuilder {
RequestBuilder::new().method(Method::GET).url(url)
}
pub fn post(url: impl Into<String>) -> RequestBuilder {
RequestBuilder::new().method(Method::POST).url(url)
}
pub fn put(url: impl Into<String>) -> RequestBuilder {
RequestBuilder::new().method(Method::PUT).url(url)
}
pub fn delete(url: impl Into<String>) -> RequestBuilder {
RequestBuilder::new().method(Method::DELETE).url(url)
}
}
#[derive(Debug)]
pub struct RequestBuilder {
method: Method,
url: Option<String>,
headers: HashMap<String, String>,
body: Body,
}
impl RequestBuilder {
pub fn new() -> Self {
let mut headers = HashMap::new();
headers.insert("User-Agent".to_string(), "reqres/0.2.0".to_string());
headers.insert("Accept".to_string(), "*/*".to_string());
headers.insert("Accept-Encoding".to_string(), "identity".to_string());
RequestBuilder {
method: Method::GET,
url: None,
headers,
body: Body::None,
}
}
pub fn method(mut self, method: Method) -> Self {
self.method = method;
self
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
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 body(mut self, body: impl Into<String>) -> Self {
let body_str = body.into();
self.body = Body::Text(body_str);
self.update_content_length();
self
}
pub fn body_bytes(mut self, body: impl Into<Bytes>) -> Self {
let bytes = body.into();
self.body = Body::Bytes(bytes);
self.update_content_length();
self
}
pub fn json(mut self, json: impl serde::Serialize) -> crate::Result<Self> {
let value = serde_json::to_value(json)?;
self.body = Body::Json(value);
self.headers.insert("Content-Type".to_string(), "application/json".to_string());
self.update_content_length();
Ok(self)
}
pub fn form(mut self, form: &[(&str, &str)]) -> Self {
let form_str = form
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
self.body = Body::Text(form_str);
self.headers.insert("Content-Type".to_string(), "application/x-www-form-urlencoded".to_string());
self.update_content_length();
self
}
pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
self.headers.insert("Content-Type".to_string(), content_type.into());
self
}
fn update_content_length(&mut self) {
if !self.body.is_empty() {
self.headers.insert("Content-Length".to_string(), self.body.len().to_string());
}
}
pub fn build(self) -> crate::Result<Request> {
let url = self.url.ok_or("URL is required")?;
Ok(Request {
method: self.method,
url,
headers: self.headers,
body: self.body,
})
}
}
impl Default for RequestBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_method_as_str() {
assert_eq!(Method::GET.as_str(), "GET");
assert_eq!(Method::POST.as_str(), "POST");
assert_eq!(Method::PUT.as_str(), "PUT");
assert_eq!(Method::DELETE.as_str(), "DELETE");
assert_eq!(Method::HEAD.as_str(), "HEAD");
assert_eq!(Method::OPTIONS.as_str(), "OPTIONS");
assert_eq!(Method::PATCH.as_str(), "PATCH");
}
#[test]
fn test_method_equality() {
assert_eq!(Method::GET, Method::GET);
assert_ne!(Method::GET, Method::POST);
}
#[test]
fn test_body_is_empty() {
assert!(Body::None.is_empty());
assert!(!Body::Text("test".to_string()).is_empty());
assert!(!Body::Bytes(Bytes::from("test")).is_empty());
assert!(!Body::Json(serde_json::json!({})).is_empty());
}
#[test]
fn test_body_as_bytes() {
assert_eq!(Body::None.as_bytes(), Vec::<u8>::new());
assert_eq!(Body::Text("hello".to_string()).as_bytes(), b"hello");
assert_eq!(Body::Bytes(Bytes::from(&b"world"[..])).as_bytes(), b"world");
assert_eq!(Body::Json(serde_json::json!({"key": "value"})).as_bytes(), br#"{"key":"value"}"#);
}
#[test]
fn test_body_len() {
assert_eq!(Body::None.len(), 0);
assert_eq!(Body::Text("hello".to_string()).len(), 5);
assert_eq!(Body::Bytes(Bytes::from(&b"world"[..])).len(), 5);
let json_body = Body::Json(serde_json::json!({"key": "value"}));
assert!(json_body.len() > 0);
assert!(json_body.len() <= 20);
}
#[test]
fn test_request_builder() {
let builder = Request::builder();
assert_eq!(builder.method, Method::GET);
assert_eq!(builder.url, None);
assert!(!builder.headers.is_empty());
matches!(builder.body, Body::None);
}
#[test]
fn test_request_get() {
let builder = Request::get("https://example.com").build().unwrap();
assert_eq!(builder.method, Method::GET);
assert_eq!(builder.url, "https://example.com");
}
#[test]
fn test_request_post() {
let builder = Request::post("https://example.com").build().unwrap();
assert_eq!(builder.method, Method::POST);
assert_eq!(builder.url, "https://example.com");
}
#[test]
fn test_request_put() {
let builder = Request::put("https://example.com").build().unwrap();
assert_eq!(builder.method, Method::PUT);
assert_eq!(builder.url, "https://example.com");
}
#[test]
fn test_request_delete() {
let builder = Request::delete("https://example.com").build().unwrap();
assert_eq!(builder.method, Method::DELETE);
assert_eq!(builder.url, "https://example.com");
}
#[test]
fn test_request_builder_default() {
let builder = RequestBuilder::default();
assert_eq!(builder.method, Method::GET);
assert_eq!(builder.url, None);
}
#[test]
fn test_request_builder_method() {
let builder = Request::builder().method(Method::POST);
assert_eq!(builder.method, Method::POST);
}
#[test]
fn test_request_builder_url() {
let builder = Request::builder().url("https://example.com");
assert_eq!(builder.url, Some("https://example.com".to_string()));
}
#[test]
fn test_request_builder_header() {
let builder = Request::builder()
.header("X-Custom", "value")
.header("X-Another", "test");
assert_eq!(builder.headers.get("X-Custom"), Some(&"value".to_string()));
assert_eq!(builder.headers.get("X-Another"), Some(&"test".to_string()));
}
#[test]
fn test_request_builder_body_text() {
let builder = Request::builder()
.url("https://example.com")
.body("hello world")
.build()
.unwrap();
matches!(builder.body, Body::Text(_));
assert_eq!(builder.headers.get("Content-Length"), Some(&"11".to_string()));
}
#[test]
fn test_request_builder_body_bytes() {
let bytes = Bytes::from(&b"test data"[..]);
let builder = Request::builder()
.url("https://example.com")
.body_bytes(bytes.clone())
.build()
.unwrap();
matches!(builder.body, Body::Bytes(_));
assert_eq!(builder.headers.get("Content-Length"), Some(&"9".to_string()));
}
#[test]
fn test_request_builder_json() {
let data = serde_json::json!({"name": "test", "value": 42});
let builder = Request::builder()
.url("https://example.com")
.json(&data)
.unwrap()
.build()
.unwrap();
matches!(builder.body, Body::Json(_));
assert_eq!(builder.headers.get("Content-Type"), Some(&"application/json".to_string()));
}
#[test]
fn test_request_builder_form() {
let form = vec![("username", "john"), ("password", "secret")];
let builder = Request::builder()
.url("https://example.com")
.form(&form)
.build()
.unwrap();
matches!(builder.body, Body::Text(_));
assert_eq!(builder.headers.get("Content-Type"), Some(&"application/x-www-form-urlencoded".to_string()));
}
#[test]
fn test_request_builder_content_type() {
let builder = Request::builder()
.url("https://example.com")
.content_type("text/plain")
.build()
.unwrap();
assert_eq!(builder.headers.get("Content-Type"), Some(&"text/plain".to_string()));
}
#[test]
fn test_request_builder_update_content_length() {
let builder = Request::builder()
.url("https://example.com")
.body("test");
assert_eq!(builder.headers.get("Content-Length"), Some(&"4".to_string()));
}
#[test]
fn test_request_builder_no_content_length_on_empty() {
let builder = Request::builder()
.url("https://example.com")
.build()
.unwrap();
assert!(builder.headers.get("Content-Length").is_none());
}
#[test]
fn test_request_builder_build_success() {
let builder = Request::builder()
.url("https://example.com")
.header("X-Test", "value");
let request = builder.build().unwrap();
assert_eq!(request.url, "https://example.com");
assert_eq!(request.headers.get("X-Test"), Some(&"value".to_string()));
}
#[test]
fn test_request_builder_build_no_url() {
let builder = Request::builder();
let result = builder.build();
assert!(result.is_err());
}
#[test]
fn test_request_builder_chain() {
let request = Request::post("https://api.example.com/data")
.header("Authorization", "Bearer token")
.header("X-Custom", "value")
.content_type("application/json")
.json(&serde_json::json!({"key": "value"}))
.unwrap()
.build()
.unwrap();
assert_eq!(request.method, Method::POST);
assert_eq!(request.url, "https://api.example.com/data");
assert_eq!(request.headers.get("Authorization"), Some(&"Bearer token".to_string()));
assert_eq!(request.headers.get("X-Custom"), Some(&"value".to_string()));
assert_eq!(request.headers.get("Content-Type"), Some(&"application/json".to_string()));
}
#[test]
fn test_default_headers() {
let builder = Request::builder().url("https://example.com").build().unwrap();
assert!(builder.headers.contains_key("User-Agent"));
assert!(builder.headers.contains_key("Accept"));
assert!(builder.headers.contains_key("Accept-Encoding"));
}
}