use super::error::{Error, Result};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Method {
Get,
Post,
Put,
Patch,
Delete,
Head,
Options,
}
impl std::fmt::Display for Method {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Get => write!(f, "GET"),
Self::Post => write!(f, "POST"),
Self::Put => write!(f, "PUT"),
Self::Patch => write!(f, "PATCH"),
Self::Delete => write!(f, "DELETE"),
Self::Head => write!(f, "HEAD"),
Self::Options => write!(f, "OPTIONS"),
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct Request {
method: Method,
url: String,
headers: HashMap<String, String>,
body: Option<Vec<u8>>,
}
impl Request {
fn new(method: Method, url: impl Into<String>) -> Self {
Self {
method,
url: url.into(),
headers: HashMap::new(),
body: None,
}
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn content_type(self, content_type: &str) -> Self {
self.header("Content-Type", content_type)
}
pub fn bearer_token(self, token: &str) -> Self {
self.header("Authorization", format!("Bearer {}", token))
}
pub fn json<T: Serialize>(mut self, body: &T) -> Result<Self> {
let json = serde_json::to_vec(body)?;
self.body = Some(json);
Ok(self.content_type("application/json"))
}
pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = Some(body.into());
self
}
pub fn form(mut self, data: &HashMap<String, String>) -> Self {
let encoded = data
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&");
self.body = Some(encoded.into_bytes());
self.content_type("application/x-www-form-urlencoded")
}
#[cfg(target_arch = "wasm32")]
pub fn send(self) -> Result<Response> {
let method_str = self.method.to_string();
let headers_json = serde_json::to_vec(&self.headers)?;
let body = self.body.unwrap_or_default();
let result_ptr = unsafe {
super::ffi::http_request(
method_str.as_ptr() as i32,
method_str.len() as i32,
self.url.as_ptr() as i32,
self.url.len() as i32,
headers_json.as_ptr() as i32,
headers_json.len() as i32,
body.as_ptr() as i32,
body.len() as i32,
)
};
if result_ptr == 0 {
return Err(Error::http("HTTP request failed"));
}
let result_bytes = unsafe { super::ffi::read_length_prefixed(result_ptr) };
let response: Response = serde_json::from_slice(&result_bytes)?;
Ok(response)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn send(self) -> Result<Response> {
Err(Error::http("HTTP not available outside WASM"))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub status: u16,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub body: Vec<u8>,
#[serde(default)]
pub error: Option<String>,
}
impl Response {
#[inline]
pub const fn is_success(&self) -> bool {
self.status >= 200 && self.status < 300
}
#[inline]
pub fn is_error(&self) -> bool {
self.error.is_some() || self.status >= 400
}
pub fn text(&self) -> Result<String> {
String::from_utf8(self.body.clone()).map_err(Error::from)
}
pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
serde_json::from_slice(&self.body).map_err(Error::from)
}
pub fn header(&self, name: &str) -> Option<&str> {
let name_lower = name.to_lowercase();
self.headers
.iter()
.find(|(k, _)| k.to_lowercase() == name_lower)
.map(|(_, v)| v.as_str())
}
pub fn error_for_status(self) -> Result<Self> {
if self.is_success() {
Ok(self)
} else {
let msg = self.error.clone().unwrap_or_else(|| {
format!("HTTP {}: {}", self.status, self.text().unwrap_or_default())
});
Err(Error::http(msg))
}
}
}
#[inline]
pub fn get(url: impl Into<String>) -> Request {
Request::new(Method::Get, url)
}
#[inline]
pub fn post(url: impl Into<String>) -> Request {
Request::new(Method::Post, url)
}
#[inline]
pub fn put(url: impl Into<String>) -> Request {
Request::new(Method::Put, url)
}
#[inline]
pub fn patch(url: impl Into<String>) -> Request {
Request::new(Method::Patch, url)
}
#[inline]
pub fn delete(url: impl Into<String>) -> Request {
Request::new(Method::Delete, url)
}
mod urlencoding {
pub fn encode(s: &str) -> String {
let mut result = String::with_capacity(s.len() * 3);
for c in s.chars() {
match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => {
result.push(c);
}
' ' => result.push('+'),
_ => {
for b in c.to_string().as_bytes() {
result.push_str(&format!("%{:02X}", b));
}
}
}
}
result
}
}