use crate::error::{FetchError, Result, TypeError};
use crate::{Headers, ReadableStream};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResponseType {
Basic,
Cors,
Default,
Error,
Opaque,
OpaqueRedirect,
}
impl Default for ResponseType {
fn default() -> Self {
Self::Basic
}
}
#[derive(Debug, Clone, Default)]
pub struct ResponseInit {
pub status: Option<u16>,
pub status_text: Option<String>,
pub headers: Option<Headers>,
}
impl ResponseInit {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug)]
pub struct Response {
response_type: ResponseType,
url: String,
redirected: bool,
status: u16,
status_text: String,
headers: Headers,
body: Option<ReadableStream>,
}
impl Response {
pub fn new(body: Option<ReadableStream>, init: Option<ResponseInit>) -> Result<Self> {
let init = init.unwrap_or_default();
let status = init.status.unwrap_or(200);
if !(200..=599).contains(&status) {
return Err(FetchError::Type(TypeError::new("Invalid status code")));
}
let status_text = init
.status_text
.unwrap_or_else(|| Self::default_status_text(status));
for byte in status_text.bytes() {
if matches!(byte, b'\r' | b'\n') {
return Err(FetchError::Type(TypeError::new("Invalid status text")));
}
}
Ok(Self {
response_type: ResponseType::Basic,
url: String::new(),
redirected: false,
status,
status_text,
headers: init.headers.unwrap_or_default(),
body,
})
}
pub fn error() -> Self {
Self {
response_type: ResponseType::Error,
url: String::new(),
redirected: false,
status: 0,
status_text: String::new(),
headers: Headers::new(),
body: None,
}
}
pub fn redirect(url: &str, status: Option<u16>) -> Result<Self> {
let status = status.unwrap_or(302);
if !matches!(status, 301 | 302 | 303 | 307 | 308) {
return Err(FetchError::Type(TypeError::new("Invalid redirect status")));
}
let mut headers = Headers::new();
headers.set("location", url)?;
Ok(Self {
response_type: ResponseType::Basic,
url: String::new(),
redirected: false,
status,
status_text: Self::default_status_text(status),
headers,
body: None,
})
}
pub fn response_type(&self) -> ResponseType {
self.response_type
}
pub fn url(&self) -> &str {
&self.url
}
pub fn redirected(&self) -> bool {
self.redirected
}
pub fn status(&self) -> u16 {
self.status
}
pub fn ok(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn status_text(&self) -> &str {
&self.status_text
}
pub fn headers(&self) -> &Headers {
&self.headers
}
pub fn body(&self) -> Option<&ReadableStream> {
self.body.as_ref()
}
pub fn body_used(&self) -> bool {
self.body.as_ref().is_some_and(|b| b.is_used())
}
pub fn clone_response(&self) -> Result<Self> {
if self.body_used() {
return Err(FetchError::Type(TypeError::new(
"Cannot clone a response with a used body",
)));
}
Ok(Clone::clone(self))
}
pub async fn array_buffer(self) -> Result<bytes::Bytes> {
match self.body {
Some(body) => body.array_buffer().await,
None => Ok(bytes::Bytes::new()),
}
}
pub async fn blob(self) -> Result<bytes::Bytes> {
self.array_buffer().await
}
pub async fn form_data(self) -> Result<String> {
match self.body {
Some(body) => body.form_data().await,
None => Ok(String::new()),
}
}
pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
match self.body {
Some(body) => body.json().await,
None => Err(FetchError::Type(TypeError::new(
"Unexpected end of JSON input",
))),
}
}
pub async fn text(self) -> Result<String> {
match self.body {
Some(body) => body.text().await,
None => Ok(String::new()),
}
}
fn default_status_text(status: u16) -> String {
match status {
200 => "OK",
201 => "Created",
204 => "No Content",
301 => "Moved Permanently",
302 => "Found",
303 => "See Other",
304 => "Not Modified",
307 => "Temporary Redirect",
308 => "Permanent Redirect",
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
409 => "Conflict",
410 => "Gone",
422 => "Unprocessable Entity",
429 => "Too Many Requests",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
_ => "",
}
.to_string()
}
pub(crate) fn from_parts(
status: u16,
status_text: String,
headers: Headers,
url: String,
redirected: bool,
) -> Self {
Self {
response_type: ResponseType::Basic,
url,
redirected,
status,
status_text,
headers,
body: None,
}
}
pub(crate) fn set_body(&mut self, body: ReadableStream) {
self.body = Some(body);
}
}
impl Clone for Response {
fn clone(&self) -> Self {
Self {
response_type: self.response_type,
url: self.url.clone(),
redirected: self.redirected,
status: self.status,
status_text: self.status_text.clone(),
headers: self.headers.clone(),
body: self.body.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_response_creation() {
let response = Response::new(None, None).unwrap();
assert_eq!(response.status(), 200);
assert!(response.ok());
assert_eq!(response.status_text(), "OK");
assert!(!response.redirected());
assert_eq!(response.response_type(), ResponseType::Basic);
}
#[test]
fn test_response_with_init() {
let mut headers = Headers::new();
headers.set("x-test", "value").unwrap();
let mut init = ResponseInit::new();
init.status = Some(201);
init.status_text = Some("Created".to_string());
init.headers = Some(headers);
let response =
Response::new(Some(ReadableStream::from_text("created")), Some(init)).unwrap();
assert_eq!(response.status(), 201);
assert!(response.ok());
assert_eq!(response.status_text(), "Created");
assert!(response.headers().has("x-test").unwrap());
}
#[test]
fn test_response_error() {
let response = Response::error();
assert_eq!(response.status(), 0);
assert!(!response.ok());
assert_eq!(response.response_type(), ResponseType::Error);
}
#[test]
fn test_response_redirect() {
let response = Response::redirect("https://example.com", Some(301)).unwrap();
assert_eq!(response.status(), 301);
assert!(!response.ok());
assert_eq!(
response.headers().get("location").unwrap().unwrap(),
"https://example.com"
);
let response = Response::redirect("https://example.com", None).unwrap();
assert_eq!(response.status(), 302);
assert!(Response::redirect("https://example.com", Some(200)).is_err());
}
#[test]
fn test_response_status_validation() {
assert!(Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status = Some(200);
init
})
)
.is_ok());
assert!(Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status = Some(404);
init
})
)
.is_ok());
assert!(Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status = Some(500);
init
})
)
.is_ok());
assert!(Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status = Some(199);
init
})
)
.is_err());
assert!(Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status = Some(600);
init
})
)
.is_err());
}
#[test]
fn test_response_ok_status() {
for status in 200..300 {
let response = Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status = Some(status);
init
}),
)
.unwrap();
assert!(response.ok(), "Status {} should be ok", status);
}
let not_ok_statuses = [300, 400, 404, 500];
for status in not_ok_statuses {
let response = Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status = Some(status);
init
}),
)
.unwrap();
assert!(!response.ok(), "Status {} should not be ok", status);
}
}
#[test]
fn test_response_status_text_validation() {
assert!(Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status_text = Some("OK".to_string());
init
})
)
.is_ok());
assert!(Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status_text = Some("OK\r\n".to_string());
init
})
)
.is_err());
}
#[test]
fn test_default_status_text() {
let response = Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status = Some(404);
init
}),
)
.unwrap();
assert_eq!(response.status_text(), "Not Found");
let response = Response::new(
None,
Some({
let mut init = ResponseInit::new();
init.status = Some(418); init
}),
)
.unwrap();
assert_eq!(response.status_text(), "");
}
#[tokio::test]
async fn test_response_body_methods() {
let response = Response::new(Some(ReadableStream::from_text("test body")), None).unwrap();
let text = response.text().await.unwrap();
assert_eq!(text, "test body");
}
#[tokio::test]
async fn test_response_json_body() {
let data = serde_json::json!({"key": "value"});
let response = Response::new(Some(ReadableStream::from_json(&data)), None).unwrap();
let parsed: serde_json::Value = response.json().await.unwrap();
assert_eq!(parsed["key"], "value");
}
#[tokio::test]
async fn test_response_empty_body() {
let response = Response::new(None, None).unwrap();
let text = response.text().await.unwrap();
assert_eq!(text, "");
let response = Response::new(None, None).unwrap();
let bytes = response.array_buffer().await.unwrap();
assert!(bytes.is_empty());
}
#[test]
fn test_response_init_defaults() {
let init = ResponseInit::new();
assert!(init.status.is_none());
assert!(init.status_text.is_none());
assert!(init.headers.is_none());
}
#[test]
fn test_response_type_default() {
assert_eq!(ResponseType::default(), ResponseType::Basic);
}
#[test]
fn test_response_clone() {
let response = Response::new(None, None).unwrap();
let cloned = response.clone_response().unwrap();
assert_eq!(response.status(), cloned.status());
assert_eq!(response.ok(), cloned.ok());
}
#[test]
fn test_redirect_status_codes() {
let valid_redirects = [301, 302, 303, 307, 308];
for status in valid_redirects {
assert!(Response::redirect("https://example.com", Some(status)).is_ok());
}
let invalid_redirects = [200, 300, 304, 400, 500];
for status in invalid_redirects {
assert!(Response::redirect("https://example.com", Some(status)).is_err());
}
}
#[tokio::test]
async fn test_body_already_used_error() {
let response = Response::new(Some(ReadableStream::from_text("test")), None).unwrap();
let _text = response.text().await.unwrap();
let response_with_body =
Response::new(Some(ReadableStream::from_text("test")), None).unwrap();
let _consumed = response_with_body.text().await.unwrap();
let response = Response::new(Some(ReadableStream::from_text("test")), None).unwrap();
let cloned = response.clone_response().unwrap();
assert_eq!(response.status(), cloned.status());
let _text = response.text().await.unwrap();
let _text2 = cloned.text().await.unwrap();
}
#[tokio::test]
async fn test_json_empty_body_error() {
let response = Response::new(None, None).unwrap();
let result: Result<serde_json::Value> = response.json().await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), FetchError::Type(_)));
}
#[test]
fn test_response_all_status_texts() {
let status_codes = [
200, 201, 204, 301, 302, 303, 304, 307, 308, 400, 401, 403, 404, 405, 409, 410, 422,
429, 500, 501, 502, 503, 504,
];
for status in status_codes {
let text = Response::default_status_text(status);
assert!(
!text.is_empty(),
"Status {} should have status text",
status
);
}
let unknown_text = Response::default_status_text(999);
assert_eq!(unknown_text, "");
}
}