use crate::prelude::*;
use beet_core::prelude::*;
use beet_net::prelude::*;
use serde::Serialize;
use serde::de::DeserializeOwned;
pub struct ServerAction;
pub trait IntoServerActionOut<M> {
fn into_action_response(self) -> Response;
}
pub struct SerdeResultIntoServerActionOut;
impl<T, E> IntoServerActionOut<(SerdeResultIntoServerActionOut, E)>
for Result<T, E>
where
T: Serialize,
E: Serialize,
{
fn into_action_response(self) -> Response {
JsonResult::new(self).into_response()
}
}
pub struct BevyResultIntoServerActionOut;
impl<T> IntoServerActionOut<Self> for Result<T, BevyError>
where
T: Serialize,
{
fn into_action_response(self) -> Response {
self.map(|val| {
serde_json::to_string(&val)
.map(|val| Response::ok_body(val, "application/json"))
.unwrap_or_else(|_| {
Response::from_status_body(
StatusCode::InternalError,
"Failed to serialize response body",
"text/plain",
)
})
.into_response()
})
.into_response()
}
}
pub struct TypeIntoServerActionOut;
impl<T> IntoServerActionOut<(TypeIntoServerActionOut,)> for T
where
T: Serialize,
{
fn into_action_response(self) -> Response { Json(self).into_response() }
}
impl ServerAction {
pub fn new<T, Input, Out, M1, M2>(
method: HttpMethod,
handler: T,
) -> EndpointBuilder
where
T: 'static + Send + Sync + Clone + IntoSystem<Input, Out, M1>,
Input: 'static + Send + SystemInput,
for<'a> Input::Inner<'a>: 'static + Send + Sync + DeserializeOwned,
Out: 'static + Send + Sync + IntoServerActionOut<M2>,
{
let builder = EndpointBuilder::default().with_method(method);
match method.has_body() {
true => builder.with_handler(
async move |req: Json<Input::Inner<'_>>,
action: AsyncEntity|
-> Result<Response> {
let out = action
.world()
.run_system_cached_with(handler.clone(), req.0)
.await?;
Ok(out.into_action_response())
},
),
false => builder.with_handler(
async move |req: JsonQueryParams<Input::Inner<'_>>,
action: AsyncEntity|
-> Result<Response> {
let out = action
.world()
.run_system_cached_with(handler.clone(), req.0)
.await?;
Ok(out.into_action_response())
},
),
}
}
pub fn new_async<T, Input, Fut, Out, M2>(
method: HttpMethod,
handler: T,
) -> EndpointBuilder
where
T: 'static + Send + Sync + Clone + Fn(Input, AsyncEntity) -> Fut,
Input: 'static + Send + Sync + DeserializeOwned,
Out: 'static + Send + Sync + IntoServerActionOut<M2>,
Fut: 'static + Send + Future<Output = Out>,
{
let builder = EndpointBuilder::default().with_method(method);
match method.has_body() {
true => builder.with_handler(
async move |req: Json<Input>, action: AsyncEntity| {
handler.clone()(req.0, action).await.into_action_response()
},
),
false => builder.with_handler(
async move |req: JsonQueryParams<Input>,
action: AsyncEntity| {
handler.clone()(req.0, action).await.into_action_response()
},
),
}
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use beet_core::prelude::*;
use beet_net::prelude::*;
#[beet_core::test]
async fn no_input() {
RouterPlugin::world()
.spawn(ExchangeSpawner::new_flow(|| {
ServerAction::new(HttpMethod::Post, || 2)
}))
.oneshot(
Request::post("/")
.with_json_body(&())
.unwrap(),
)
.await
.into_result()
.await
.unwrap()
.body
.into_json::<u32>()
.await
.unwrap()
.xpect_eq(2);
}
#[beet_core::test]
async fn post() {
let mut world = RouterPlugin::world();
let mut entity = world.spawn(ExchangeSpawner::new_flow(|| {
ServerAction::new(HttpMethod::Post, |val: In<u32>| val.0 + 2)
.with_path("foo")
}));
entity
.oneshot(Request::post("/foo").with_json_body(&3).unwrap())
.await
.into_result()
.await
.unwrap()
.body
.into_json::<u32>()
.await
.unwrap()
.xpect_eq(5);
entity
.oneshot(Request::post("/foo"))
.await
.status()
.xpect_eq(StatusCode::MalformedRequest);
}
#[beet_core::test]
async fn get_sync() {
let mut world = RouterPlugin::world();
let mut entity = world.spawn(ExchangeSpawner::new_flow(|| {
ServerAction::new_async(HttpMethod::Get, async |val: u32, _| {
val + 2
})
}));
entity
.oneshot(Request::get("/?data=3"))
.await
.into_result()
.await
.unwrap()
.body
.into_json::<u32>()
.await
.unwrap()
.xpect_eq(5);
entity
.oneshot(Request::get("/"))
.await
.status()
.xpect_eq(StatusCode::MalformedRequest);
}
}