use std::time::Duration;
use crate::error::Error;
pub use crate::error::Result;
#[cfg(not(target_arch = "wasm32"))]
mod native;
#[cfg(not(target_arch = "wasm32"))]
mod ws;
#[cfg(not(target_arch = "wasm32"))]
pub use crate::io::runtime::{AsyncConn, Runtime};
#[cfg(all(feature = "tokio-rt", not(target_arch = "wasm32")))]
pub use crate::io::tokio::{TokioConn, TokioRuntime};
#[cfg(not(target_arch = "wasm32"))]
pub use native::{get, post, request};
#[cfg(not(target_arch = "wasm32"))]
pub use ws::WebSocket;
#[cfg(all(feature = "tokio-rt", not(target_arch = "wasm32")))]
pub type TokioWebSocket = WebSocket<TokioConn>;
#[cfg(target_arch = "wasm32")]
mod wasm;
#[cfg(target_arch = "wasm32")]
pub use wasm::{get, post, request, WebSocket, WsSink, WsStream};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Response {
pub status: u16,
pub reason: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl Response {
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn text(&self) -> Result<String> {
match self.charset().as_deref() {
None | Some("utf-8") | Some("utf8") | Some("us-ascii") | Some("ascii") => {
Ok(String::from_utf8_lossy(&self.body).into_owned())
}
Some("iso-8859-1") | Some("iso8859-1") | Some("latin1") => {
Ok(self.body.iter().map(|&b| b as char).collect())
}
Some(other) => Err(Error::Decode(format!(
"unsupported Content-Type charset {other:?}; \
use Response::body for the raw {} bytes",
self.body.len()
))),
}
}
#[cfg(feature = "json")]
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
serde_json::from_slice(&self.body).map_err(|e| Error::Decode(format!("json: {e}")))
}
pub fn error_for_status(self) -> Result<Self> {
if self.status >= 400 {
Err(Error::Status {
code: self.status,
reason: self.reason.clone(),
})
} else {
Ok(self)
}
}
pub fn into_body(self) -> Vec<u8> {
self.body
}
fn charset(&self) -> Option<String> {
let ct = self.header("content-type")?;
ct.split(';').skip(1).find_map(|param| {
let (k, v) = param.split_once('=')?;
if !k.trim().eq_ignore_ascii_case("charset") {
return None;
}
Some(v.trim().trim_matches('"').to_ascii_lowercase())
})
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Request {
pub method: String,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
pub follow_redirects: bool,
pub decompress: bool,
pub timeout: Option<Duration>,
}
pub const MAX_REDIRECTS: usize = 10;
impl Request {
pub fn new(method: impl Into<String>, url: impl Into<String>) -> Self {
Request {
method: method.into(),
url: url.into(),
headers: Vec::new(),
body: Vec::new(),
follow_redirects: false,
decompress: true,
timeout: None,
}
}
pub fn get(url: impl Into<String>) -> Self {
Request::new("GET", url)
}
pub fn post(url: impl Into<String>, body: impl Into<Vec<u8>>) -> Self {
Request::new("POST", url).body(body)
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = body.into();
self
}
#[deprecated(since = "0.1.8", note = "renamed to `body`, matching the blocking API")]
pub fn with_body(self, body: impl Into<Vec<u8>>) -> Self {
self.body(body)
}
pub fn follow_redirects(mut self, on: bool) -> Self {
self.follow_redirects = on;
self
}
pub fn decompress(mut self, on: bool) -> Self {
self.decompress = on;
self
}
pub fn timeout(mut self, dur: impl Into<Option<Duration>>) -> Self {
self.timeout = dur.into();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WsMessage {
Text(String),
Binary(Vec<u8>),
}
impl WsMessage {
pub fn as_bytes(&self) -> &[u8] {
match self {
WsMessage::Text(s) => s.as_bytes(),
WsMessage::Binary(b) => b,
}
}
pub fn as_text(&self) -> Option<&str> {
match self {
WsMessage::Text(s) => Some(s),
WsMessage::Binary(_) => None,
}
}
pub fn is_text(&self) -> bool {
matches!(self, WsMessage::Text(_))
}
pub fn is_binary(&self) -> bool {
matches!(self, WsMessage::Binary(_))
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
WsMessage::Text(s) => s.into_bytes(),
WsMessage::Binary(b) => b,
}
}
}
impl From<String> for WsMessage {
fn from(s: String) -> Self {
WsMessage::Text(s)
}
}
impl From<&str> for WsMessage {
fn from(s: &str) -> Self {
WsMessage::Text(s.to_string())
}
}
impl From<Vec<u8>> for WsMessage {
fn from(b: Vec<u8>) -> Self {
WsMessage::Binary(b)
}
}
impl From<&[u8]> for WsMessage {
fn from(b: &[u8]) -> Self {
WsMessage::Binary(b.to_vec())
}
}
#[cfg(test)]
mod shared_tests {
use super::*;
fn resp(headers: Vec<(String, String)>, body: Vec<u8>) -> Response {
Response {
status: 200,
reason: "OK".into(),
headers,
body,
}
}
#[test]
fn response_header_lookup_is_case_insensitive() {
let r = resp(
vec![("Content-Type".into(), "text/plain".into())],
Vec::new(),
);
assert_eq!(r.header("content-TYPE"), Some("text/plain"));
assert_eq!(r.header("missing"), None);
assert!(r.is_success());
}
#[test]
fn text_honours_declared_charset() {
let latin1 = resp(
vec![(
"Content-Type".into(),
"text/plain; charset=ISO-8859-1".into(),
)],
vec![0xE9], );
assert_eq!(latin1.text().unwrap(), "é");
let utf8 = resp(
vec![("Content-Type".into(), "text/plain".into())],
vec![0xE9],
);
assert_eq!(utf8.text().unwrap(), "\u{fffd}");
let unknown = resp(
vec![(
"Content-Type".into(),
"text/plain; charset=shift_jis".into(),
)],
vec![0xE9],
);
assert!(matches!(unknown.text(), Err(Error::Decode(_))));
}
#[test]
fn error_for_status_maps_4xx_to_err() {
let mut bad = resp(Vec::new(), Vec::new());
bad.status = 404;
bad.reason = "Not Found".into();
assert!(!bad.is_success());
assert!(matches!(
bad.error_for_status(),
Err(Error::Status { code: 404, .. })
));
}
#[test]
fn request_builder_defaults_and_overrides() {
let req = Request::get("http://x/")
.header("A", "1")
.body(b"hi".to_vec())
.follow_redirects(true)
.decompress(false)
.timeout(Duration::from_secs(5));
assert_eq!(req.method, "GET");
assert_eq!(req.headers, vec![("A".to_string(), "1".to_string())]);
assert_eq!(req.body, b"hi");
assert!(req.follow_redirects);
assert!(!req.decompress);
assert_eq!(req.timeout, Some(Duration::from_secs(5)));
assert_eq!(Request::get("http://x/").timeout(None).timeout, None);
}
#[test]
fn ws_message_conversions() {
assert_eq!(WsMessage::from("hi"), WsMessage::Text("hi".into()));
assert_eq!(WsMessage::from(vec![1u8, 2]), WsMessage::Binary(vec![1, 2]));
assert!(WsMessage::from("hi").is_text());
assert!(WsMessage::from(&b"x"[..]).is_binary());
assert_eq!(WsMessage::Text("hi".into()).into_bytes(), b"hi");
}
}