#[cfg(feature = "json")]
use std::any::type_name;
use std::any::TypeId;
use std::collections::HashSet;
use std::fmt;
use std::sync::{Mutex, MutexGuard, PoisonError};
use bevy_app::App;
use bevy_ecs::resource::Resource;
#[cfg(feature = "json")]
use bevy_ecs::system::Commands;
use http::Method;
use crate::request::{OutgoingRequest, RequestId};
#[cfg(feature = "json")]
use crate::response::{BackendError, RawResponse};
pub(crate) enum Route {
Raw,
#[cfg(feature = "json")]
Json(Box<dyn JsonRoute>),
}
#[cfg(feature = "json")]
pub(crate) trait JsonRoute: Send + Sync + 'static {
fn deliver(&self, id: RequestId, result: Result<RawResponse, BackendError>, commands: &mut Commands);
}
#[cfg(feature = "json")]
struct JsonRouteFor<T>(std::marker::PhantomData<fn() -> T>);
#[cfg(feature = "json")]
impl<T: serde::de::DeserializeOwned + Send + Sync + 'static> JsonRoute for JsonRouteFor<T> {
fn deliver(&self, id: RequestId, result: Result<RawResponse, BackendError>, commands: &mut Commands) {
let result =
result.and_then(|response| response.json::<T>().map_err(|e| BackendError::Decode { message: e.to_string(), response: Box::new(response) }));
commands.queue(move |world: &mut bevy_ecs::world::World| {
if let Err(BackendError::Decode { response, .. }) = &result {
tracing::debug!(">>> NET-BACKEND: {id} -> HTTP {} but the body is not a `{}`", response.status, type_name::<T>());
}
if world.write_message(crate::JsonResponse::<T> { id, result }).is_none() {
tracing::error!(">>> NET-BACKEND: {id}: `JsonResponse<{}>` is not registered; the answer is lost", type_name::<T>());
}
});
}
}
pub(crate) enum Queued {
Send { id: RequestId, request: Box<OutgoingRequest>, route: Route },
}
#[derive(Resource, Default)]
pub struct HttpClient {
queue: Mutex<Vec<Queued>>,
cancels: crate::inflight::CancelList,
json_types: HashSet<TypeId>,
}
impl fmt::Debug for HttpClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpClient").field("queued", &self.lock().len()).field("json_types", &self.json_types.len()).finish()
}
}
impl HttpClient {
fn lock(&self) -> MutexGuard<'_, Vec<Queued>> {
self.queue.lock().unwrap_or_else(PoisonError::into_inner)
}
fn push(&self, request: OutgoingRequest, route: Route) -> RequestId {
let id = RequestId::next();
self.lock().push(Queued::Send { id, request: Box::new(request), route });
id
}
pub(crate) fn drain(&self) -> Vec<Queued> {
std::mem::take(&mut *self.lock())
}
pub fn send(&self, request: OutgoingRequest) -> RequestId {
self.push(request, Route::Raw)
}
pub fn request(&self, method: Method, path: &str, body: Option<Vec<u8>>) -> RequestId {
let mut request = OutgoingRequest::new(method, path);
request.set_body(body);
self.send(request)
}
pub fn get(&self, path: &str) -> RequestId {
self.send(OutgoingRequest::get(path))
}
#[cfg(feature = "http")]
#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
pub fn post_multipart(&self, path: &str, form: &crate::Multipart) -> RequestId {
self.send(OutgoingRequest::post(path).with_multipart(form))
}
#[cfg(feature = "http")]
#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
pub fn send_multipart(&self, method: Method, path: &str, form: &crate::Multipart) -> RequestId {
self.send(OutgoingRequest::new(method, path).with_multipart(form))
}
pub fn cancel(&self, id: RequestId) {
self.cancels.push(id);
}
pub(crate) fn share_cancels(&mut self, cancels: crate::inflight::CancelList) {
self.cancels = cancels;
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub fn send_json<T: serde::de::DeserializeOwned + Send + Sync + 'static>(&self, mut request: OutgoingRequest) -> RequestId {
if !self.json_types.contains(&TypeId::of::<T>()) {
let name = type_name::<T>();
tracing::error!(">>> NET-BACKEND: `{name}` is not registered: call `app.add_json_response::<{name}>()`; answered on HttpResponse");
request.reject(format!("response type `{name}` is not registered; call `app.add_json_response::<{name}>()`"));
return self.push(request, Route::Raw);
}
if !request.headers().contains_key(http::header::ACCEPT) {
request.headers_mut().insert(http::header::ACCEPT, http::HeaderValue::from_static("application/json"));
}
self.push(request, Route::Json(Box::new(JsonRouteFor::<T>(std::marker::PhantomData))))
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub fn get_json<T: serde::de::DeserializeOwned + Send + Sync + 'static>(&self, path: &str) -> RequestId {
self.send_json::<T>(OutgoingRequest::get(path))
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub fn post_json<T: serde::de::DeserializeOwned + Send + Sync + 'static>(&self, path: &str, body: &(impl serde::Serialize + ?Sized)) -> RequestId {
self.send_json::<T>(OutgoingRequest::post(path).with_json(body))
}
#[cfg(all(feature = "http", feature = "json"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "http", feature = "json"))))]
pub fn post_multipart_json<T: serde::de::DeserializeOwned + Send + Sync + 'static>(&self, path: &str, form: &crate::Multipart) -> RequestId {
self.send_json::<T>(OutgoingRequest::post(path).with_multipart(form))
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub fn is_json_registered<T: 'static>(&self) -> bool {
self.json_types.contains(&TypeId::of::<T>())
}
}
mod sealed {
pub trait Sealed {}
impl Sealed for bevy_app::App {}
}
pub trait BackendAppExt: sealed::Sealed {
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
fn add_json_response<T: serde::de::DeserializeOwned + Send + Sync + 'static>(&mut self) -> &mut Self;
#[cfg(all(feature = "ws", feature = "json"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "ws", feature = "json"))))]
fn add_ws_request<R: crate::WsRequest>(&mut self) -> &mut Self;
#[cfg(all(feature = "ws", feature = "json"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "ws", feature = "json"))))]
fn add_ws_push<P: crate::WsPushMessage>(&mut self) -> &mut Self;
}
impl BackendAppExt for App {
#[cfg(feature = "json")]
fn add_json_response<T: serde::de::DeserializeOwned + Send + Sync + 'static>(&mut self) -> &mut Self {
self.add_message::<crate::JsonResponse<T>>();
self.init_resource::<HttpClient>();
if let Some(mut client) = self.world_mut().get_resource_mut::<HttpClient>() {
client.json_types.insert(TypeId::of::<T>());
}
self
}
#[cfg(all(feature = "ws", feature = "json"))]
fn add_ws_request<R: crate::WsRequest>(&mut self) -> &mut Self {
crate::ws::register_request::<R>(self);
self
}
#[cfg(all(feature = "ws", feature = "json"))]
fn add_ws_push<P: crate::WsPushMessage>(&mut self) -> &mut Self {
crate::ws::register_push::<P>(self);
self
}
}