const MAX_HEADERS: usize = 16;
#[derive(Debug, Clone)]
pub struct HttpRequest {
pub url: String,
pub method: String,
pub headers: Vec<(String, String)>,
}
impl HttpRequest {
pub fn get(url: impl Into<String>) -> Self {
Self {
url: url.into(),
method: "GET".into(),
headers: Vec::new(),
}
}
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
if self.headers.len() < MAX_HEADERS {
self.headers.push((name.into(), value.into()));
}
self
}
}
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub body: Vec<u8>,
pub headers: Vec<(String, String)>,
}
impl HttpResponse {
#[inline]
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
#[inline]
pub fn is_client_error(&self) -> bool {
(400..500).contains(&self.status)
}
#[inline]
pub fn is_server_error(&self) -> bool {
(500..600).contains(&self.status)
}
pub fn header(&self, name: &str) -> Option<&str> {
let lower = name.to_ascii_lowercase();
self.headers
.iter()
.find(|(k, _)| k.to_ascii_lowercase() == lower)
.map(|(_, v)| v.as_str())
}
}
pub trait HttpClient: Send + Sync {
fn send(&self, request: HttpRequest);
fn poll(&self) -> Vec<(String, Result<HttpResponse, String>)>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_request_defaults() {
let req = HttpRequest::get("https://example.com/tile.png");
assert_eq!(req.url, "https://example.com/tile.png");
assert_eq!(req.method, "GET");
assert!(req.headers.is_empty());
}
#[test]
fn request_with_headers() {
let req = HttpRequest::get("https://example.com")
.with_header("User-Agent", "rustial/0.1")
.with_header("Authorization", "Bearer token");
assert_eq!(req.headers.len(), 2);
assert_eq!(req.headers[0].0, "User-Agent");
assert_eq!(req.headers[1].0, "Authorization");
}
#[test]
fn request_header_limit() {
let mut req = HttpRequest::get("https://example.com");
for i in 0..20 {
req = req.with_header(format!("X-Header-{i}"), "value");
}
assert_eq!(req.headers.len(), MAX_HEADERS);
}
#[test]
fn response_status_helpers() {
assert!(HttpResponse {
status: 200,
body: vec![],
headers: vec![]
}
.is_success());
assert!(HttpResponse {
status: 204,
body: vec![],
headers: vec![]
}
.is_success());
assert!(!HttpResponse {
status: 301,
body: vec![],
headers: vec![]
}
.is_success());
assert!(HttpResponse {
status: 404,
body: vec![],
headers: vec![]
}
.is_client_error());
assert!(!HttpResponse {
status: 200,
body: vec![],
headers: vec![]
}
.is_client_error());
assert!(HttpResponse {
status: 500,
body: vec![],
headers: vec![]
}
.is_server_error());
assert!(HttpResponse {
status: 503,
body: vec![],
headers: vec![]
}
.is_server_error());
assert!(!HttpResponse {
status: 200,
body: vec![],
headers: vec![]
}
.is_server_error());
}
#[test]
fn response_header_lookup() {
let resp = HttpResponse {
status: 200,
body: vec![],
headers: vec![
("Content-Type".into(), "image/png".into()),
("Cache-Control".into(), "max-age=3600".into()),
],
};
assert_eq!(resp.header("content-type"), Some("image/png"));
assert_eq!(resp.header("CACHE-CONTROL"), Some("max-age=3600"));
assert_eq!(resp.header("X-Missing"), None);
}
#[test]
fn trait_is_object_safe() {
struct Dummy;
impl HttpClient for Dummy {
fn send(&self, _request: HttpRequest) {}
fn poll(&self) -> Vec<(String, Result<HttpResponse, String>)> {
vec![]
}
}
let _boxed: Box<dyn HttpClient> = Box::new(Dummy);
}
#[test]
fn trait_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
struct Dummy;
impl HttpClient for Dummy {
fn send(&self, _request: HttpRequest) {}
fn poll(&self) -> Vec<(String, Result<HttpResponse, String>)> {
vec![]
}
}
assert_send_sync::<Dummy>();
}
}