use bytes::Bytes;
use crate::{
rt::{read_bytes_source_as, read_bytes_source_into},
IterBuf,
};
use spacetimedb_lib::{bsatn, http as st_http, TimeDuration};
pub type Request<T = Body> = http::Request<T>;
pub type Response<T = Body> = http::Response<T>;
#[non_exhaustive]
pub struct HttpClient {}
impl HttpClient {
pub fn send<B: Into<Body>>(&self, request: http::Request<B>) -> Result<Response, Error> {
let (request, body) = request.map(Into::into).into_parts();
let request = convert_request(request);
let request = bsatn::to_vec(&request).expect("Failed to BSATN-serialize `spacetimedb_lib::http::Request`");
match spacetimedb_bindings_sys::procedure::http_request(&request, &body.into_bytes()) {
Ok((response_source, body_source)) => {
let response = read_bytes_source_as::<st_http::Response>(response_source);
let response = convert_response(response).expect("Invalid http response returned from host");
let body = if body_source == spacetimedb_bindings_sys::raw::BytesSource::INVALID {
Body::from_bytes(Vec::<u8>::new())
} else {
let mut buf = IterBuf::take();
read_bytes_source_into(body_source, &mut buf);
Body::from_bytes(buf.clone())
};
Ok(http::Response::from_parts(response, body))
}
Err(err_source) => {
let message = read_bytes_source_as::<String>(err_source);
Err(Error { message })
}
}
}
pub fn get(&self, uri: impl TryInto<http::Uri, Error: Into<http::Error>>) -> Result<Response, Error> {
self.send(
http::Request::builder()
.method(http::Method::GET)
.uri(uri)
.body(Body::empty())?,
)
}
}
fn convert_request(parts: http::request::Parts) -> st_http::Request {
let http::request::Parts {
method,
uri,
version,
headers,
mut extensions,
..
} = parts;
let timeout = extensions.remove::<Timeout>();
if !extensions.is_empty() {
log::warn!("Converting HTTP `Request` with unrecognized extensions");
}
st_http::Request {
method: match method {
http::Method::GET => st_http::Method::Get,
http::Method::HEAD => st_http::Method::Head,
http::Method::POST => st_http::Method::Post,
http::Method::PUT => st_http::Method::Put,
http::Method::DELETE => st_http::Method::Delete,
http::Method::CONNECT => st_http::Method::Connect,
http::Method::OPTIONS => st_http::Method::Options,
http::Method::TRACE => st_http::Method::Trace,
http::Method::PATCH => st_http::Method::Patch,
_ => st_http::Method::Extension(method.to_string()),
},
headers: headers
.into_iter()
.map(|(k, v)| (k.map(|k| k.as_str().into()), v.as_bytes().into()))
.collect(),
timeout: timeout.map(Into::into),
uri: uri.to_string(),
version: match version {
http::Version::HTTP_09 => st_http::Version::Http09,
http::Version::HTTP_10 => st_http::Version::Http10,
http::Version::HTTP_11 => st_http::Version::Http11,
http::Version::HTTP_2 => st_http::Version::Http2,
http::Version::HTTP_3 => st_http::Version::Http3,
_ => unreachable!("Unknown HTTP version: {version:?}"),
},
}
}
fn convert_response(response: st_http::Response) -> http::Result<http::response::Parts> {
let st_http::Response { headers, version, code } = response;
let (mut response, ()) = http::Response::new(()).into_parts();
response.version = match version {
st_http::Version::Http09 => http::Version::HTTP_09,
st_http::Version::Http10 => http::Version::HTTP_10,
st_http::Version::Http11 => http::Version::HTTP_11,
st_http::Version::Http2 => http::Version::HTTP_2,
st_http::Version::Http3 => http::Version::HTTP_3,
};
response.status = http::StatusCode::from_u16(code)?;
response.headers = headers
.into_iter()
.map(|(k, v)| Ok((k.into_string().try_into()?, v.into_vec().try_into()?)))
.collect::<http::Result<_>>()?;
Ok(response)
}
pub struct Body {
inner: BodyInner,
}
impl Body {
pub fn into_bytes(self) -> Bytes {
match self.inner {
BodyInner::Bytes(bytes) => bytes,
}
}
pub fn into_string(self) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.into_bytes().into())
}
pub fn into_string_lossy(self) -> String {
self.into_string()
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
}
pub fn from_bytes(bytes: impl Into<Bytes>) -> Body {
Body {
inner: BodyInner::Bytes(bytes.into()),
}
}
pub fn empty() -> Body {
().into()
}
pub fn is_empty(&self) -> bool {
match &self.inner {
BodyInner::Bytes(bytes) => bytes.is_empty(),
}
}
}
impl Default for Body {
fn default() -> Self {
Self::empty()
}
}
macro_rules! impl_body_from_bytes {
($bytes:ident : $t:ty => $conv:expr) => {
impl From<$t> for Body {
fn from($bytes: $t) -> Body {
Body::from_bytes($conv)
}
}
};
($t:ty) => {
impl_body_from_bytes!(bytes : $t => bytes);
};
}
impl_body_from_bytes!(String);
impl_body_from_bytes!(Vec<u8>);
impl_body_from_bytes!(Box<[u8]>);
impl_body_from_bytes!(&'static [u8]);
impl_body_from_bytes!(&'static str);
impl_body_from_bytes!(_unit: () => Bytes::new());
enum BodyInner {
Bytes(Bytes),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Timeout(pub TimeDuration);
impl From<TimeDuration> for Timeout {
fn from(timeout: TimeDuration) -> Timeout {
Timeout(timeout)
}
}
impl From<Timeout> for TimeDuration {
fn from(Timeout(timeout): Timeout) -> TimeDuration {
timeout
}
}
#[derive(Clone, Debug)]
pub struct Error {
message: String,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let Error { message } = self;
f.write_str(message)
}
}
impl std::error::Error for Error {}
impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
Error {
message: err.to_string(),
}
}
}