use crate::prelude::*;
use beet_core::prelude::*;
use beet_flow::prelude::*;
use bevy::ecs::relationship::RelatedSpawner;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Component, Reflect)]
#[reflect(Component)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "tokens", derive(ToTokens))]
pub enum ContentType {
Html,
Json,
}
#[derive(Debug, Clone, Component, PartialEq, Eq, Reflect)]
#[reflect(Component)]
pub struct Endpoint {
description: Option<String>,
params: ParamsPattern,
path: PathPattern,
method: Option<HttpMethod>,
cache_strategy: Option<CacheStrategy>,
content_type: Option<ContentType>,
is_canonical: bool,
}
impl Endpoint {
#[cfg(test)]
pub(crate) fn new(
path: PathPattern,
params: ParamsPattern,
method: Option<HttpMethod>,
cache_strategy: Option<CacheStrategy>,
content_type: Option<ContentType>,
is_canonical: bool,
) -> Self {
Self {
path,
params,
method,
cache_strategy,
content_type,
is_canonical,
description: None,
}
}
pub fn description(&self) -> Option<&str> { self.description.as_deref() }
pub fn path(&self) -> &PathPattern { &self.path }
pub fn params(&self) -> &ParamsPattern { &self.params }
pub fn method(&self) -> Option<HttpMethod> { self.method }
pub fn cache_strategy(&self) -> Option<CacheStrategy> {
self.cache_strategy
}
pub fn content_type(&self) -> Option<ContentType> { self.content_type }
pub fn is_canonical(&self) -> bool { self.is_canonical }
pub fn is_static_get(&self) -> bool {
self.path.is_static()
&& self.method.map(|m| m == HttpMethod::Get).unwrap_or(true)
&& self
.cache_strategy
.map(|s| s == CacheStrategy::Static)
.unwrap_or(false)
}
pub fn is_static_get_html(&self) -> bool {
self.is_static_get() && self.content_type == Some(ContentType::Html)
}
}
#[derive(BundleEffect)]
pub struct EndpointBuilder {
insert: Box<dyn 'static + Send + Sync + FnOnce(&mut EntityWorldMut)>,
path: Option<PathPartial>,
params: Option<ParamsPartial>,
method: Option<HttpMethod>,
cache_strategy: Option<CacheStrategy>,
content_type: Option<ContentType>,
exact_path: bool,
description: Option<String>,
is_canonical: bool,
additional_predicates: Vec<
Box<
dyn 'static
+ Send
+ Sync
+ FnOnce(&mut RelatedSpawner<'_, ChildOf>),
>,
>,
}
impl Default for EndpointBuilder {
fn default() -> Self {
Self {
insert: Box::new(|entity| {
entity.insert(StatusCode::Ok.into_endpoint_handler());
}),
path: None,
params: None,
method: Some(HttpMethod::Get),
cache_strategy: None,
content_type: None,
exact_path: true,
description: None,
is_canonical: true,
additional_predicates: Vec::new(),
}
}
}
impl EndpointBuilder {
pub fn new<M>(
handler: impl 'static + Send + Sync + IntoEndpointHandler<M>,
) -> Self {
Self::default().with_handler(handler)
}
pub fn get() -> Self { Self::default().with_method(HttpMethod::Get) }
pub fn post() -> Self { Self::default().with_method(HttpMethod::Post) }
pub fn any_method() -> Self { Self::default().with_any_method() }
pub fn middleware(
path: impl AsRef<str>,
handler: impl 'static + Send + Sync + Bundle,
) -> impl Bundle {
(
Name::new(format!("Middleware: {}", path.as_ref())),
Sequence,
PathPartial::new(path.as_ref()),
children![partial_path_match(), handler],
)
}
pub fn with_handler<M>(
self,
handler: impl 'static + Send + Sync + IntoEndpointHandler<M>,
) -> Self {
self.with_handler_bundle(handler.into_endpoint_handler())
}
pub fn with_handler_bundle(mut self, endpoint: impl Bundle) -> Self {
self.insert = Box::new(move |entity| {
entity.insert(endpoint);
});
self
}
pub fn with_path(mut self, path: impl AsRef<str>) -> Self {
self.path = Some(PathPartial::new(path.as_ref()));
self
}
pub fn with_params<T: bevy_reflect::Typed>(mut self) -> Self {
self.params = Some(ParamsPartial::new::<T>());
self
}
pub fn with_method(mut self, method: HttpMethod) -> Self {
self.method = Some(method);
self
}
pub fn with_any_method(mut self) -> Self {
self.method = None;
self
}
pub fn with_predicate(
mut self,
predicate: impl Bundle + 'static + Send + Sync,
) -> Self {
self.additional_predicates.push(Box::new(move |spawner| {
spawner.spawn(predicate);
}));
self
}
pub fn with_cache_strategy(mut self, strategy: CacheStrategy) -> Self {
self.cache_strategy = Some(strategy);
self
}
pub fn with_content_type(mut self, content_type: ContentType) -> Self {
self.content_type = Some(content_type);
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_trailing_path(mut self) -> Self {
self.exact_path = false;
self
}
pub fn non_canonical(mut self) -> Self {
self.is_canonical = false;
self
}
fn effect(self, entity: &mut EntityWorldMut) {
if let Some(pattern) = self.path {
entity.insert(pattern);
}
if let Some(params) = self.params {
entity.insert(params);
}
let id = entity.id();
let path: PathPattern = entity.world_scope(|world| {
world
.run_system_cached_with(PathPattern::collect_system, id)
.unwrap()
});
let params = entity
.world_scope(|world| -> Result<ParamsPattern> {
world
.run_system_cached_with(ParamsPattern::collect_system, id)
.unwrap()
})
.unwrap();
entity
.insert((
Name::new(format!("Endpoint: {}", path.annotated_route_path())),
Endpoint {
path,
params,
description: self.description,
method: self.method,
cache_strategy: self.cache_strategy,
content_type: self.content_type,
is_canonical: self.is_canonical,
},
Sequence,
))
.with_children(|spawner| {
spawner.spawn(path_match(self.exact_path));
if let Some(method) = self.method {
spawner.spawn(check_method(method));
}
for predicate in self.additional_predicates {
(predicate)(spawner);
}
let mut handler_entity =
spawner.spawn(Name::new("Route Handler"));
if let Some(cache_strategy) = self.cache_strategy {
handler_entity.insert(cache_strategy);
}
if let Some(content_type) = self.content_type {
handler_entity.insert(content_type);
}
if let Some(method) = self.method {
handler_entity.insert(method);
}
(self.insert)(&mut handler_entity);
});
}
}
pub fn exact_path_match() -> impl Bundle { path_match(true) }
pub fn partial_path_match() -> impl Bundle { path_match(false) }
fn path_match(must_exact_match: bool) -> impl Bundle {
(
Name::new("Check Path Match"),
OnSpawn::observe(
move |ev: On<GetOutcome>,
mut commands: Commands,
query: RouteQuery| {
let action = ev.target();
let outcome = match query.path_match(action) {
Ok(path_match)
if must_exact_match && !path_match.exact_match() =>
{
Outcome::Fail
}
Ok(_) => Outcome::Pass,
Err(_err) => Outcome::Fail,
};
commands.entity(action).trigger_target(outcome);
},
),
)
}
fn check_method(method: HttpMethod) -> impl Bundle {
(
Name::new("Method Check"),
method,
OnSpawn::observe(
|ev: On<GetOutcome>,
query: RouteQuery,
actions: Query<&HttpMethod>,
mut commands: Commands|
-> Result {
let action = ev.target();
let method = actions.get(action)?;
let outcome = match query.method(action)? == *method {
true => Outcome::Pass,
false => Outcome::Fail,
};
commands.entity(action).trigger_target(outcome);
Ok(())
},
),
)
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use beet_core::prelude::*;
use beet_flow::prelude::*;
use beet_net::prelude::*;
#[beet_core::test]
async fn simple() {
let _ = EndpointBuilder::new(|| {});
let _ = EndpointBuilder::new(|| -> Result<(), String> { Ok(()) });
RouterPlugin::world()
.spawn(ExchangeSpawner::new_flow(|| EndpointBuilder::get()))
.oneshot(Request::get("/"))
.await
.status()
.xpect_eq(StatusCode::Ok);
}
#[beet_core::test]
async fn dynamic_path() {
RouterPlugin::world()
.spawn(ExchangeSpawner::new_flow(|| {
EndpointBuilder::get().with_path("/:path").with_handler(
async |_req: (),
action: AsyncEntity|
-> Result<Html<String>> {
let path =
RouteQuery::dyn_segment_async(action, "path")
.await?;
Html(path).xok()
},
)
}))
.oneshot_str(Request::get("/bing"))
.await
.xpect_eq("bing");
}
#[beet_core::test]
async fn children() {
use beet_flow::prelude::*;
let mut world = RouterPlugin::world();
let mut entity = world.spawn(ExchangeSpawner::new_flow(|| {
(InfallibleSequence, children![
EndpointBuilder::get()
.with_path("foo")
.with_handler(|| "foo"),
EndpointBuilder::get()
.with_path("bar")
.with_handler(|| "bar"),
])
}));
entity.oneshot_str("/foo").await.xpect_eq("foo");
entity.oneshot_str("/bar").await.xpect_eq("bar");
}
#[beet_core::test]
async fn works() {
let mut world = RouterPlugin::world();
let mut entity = world.spawn(ExchangeSpawner::new_flow(|| {
EndpointBuilder::post().with_path("foo")
}));
entity
.oneshot(Request::post("/foo"))
.await
.status()
.xpect_eq(StatusCode::Ok);
entity
.oneshot(Request::get("/foo"))
.await
.status()
.xpect_eq(StatusCode::InternalError);
entity
.oneshot(Request::get("/bar"))
.await
.status()
.xpect_eq(StatusCode::InternalError);
entity
.oneshot(Request::get("/foo/bar"))
.await
.status()
.xpect_eq(StatusCode::InternalError);
}
#[beet_core::test]
async fn middleware_allows_trailing() {
use beet_flow::prelude::*;
let mut world = RouterPlugin::world();
let mut entity = world.spawn(ExchangeSpawner::new_flow(|| {
(InfallibleSequence, children![
EndpointBuilder::middleware(
"api",
OnSpawn::observe(
|ev: On<GetOutcome>, mut commands: Commands| {
commands
.entity(ev.target())
.trigger_target(Outcome::Pass);
},
),
),
EndpointBuilder::get()
.with_path("api/users")
.with_handler(|| "users"),
])
}));
entity
.oneshot(Request::get("/api/users"))
.await
.status()
.xpect_eq(StatusCode::Ok);
}
#[test]
fn test_collect_route_segments() {
let mut world = World::new();
world.spawn((
PathPartial::new("foo"),
EndpointBuilder::get(),
children![
children![
(PathPartial::new("*bar"), EndpointBuilder::get()),
PathPartial::new("bazz")
],
(PathPartial::new("qux"),),
(PathPartial::new(":quax"), EndpointBuilder::get()),
],
));
let mut paths = world
.query_once::<&Endpoint>()
.into_iter()
.map(|endpoint| endpoint.path().annotated_route_path())
.collect::<Vec<_>>();
paths.sort();
paths.xpect_eq(vec![
RoutePath::new("/foo"),
RoutePath::new("/foo/*bar"),
RoutePath::new("/foo/:quax"),
]);
}
#[beet_core::test]
async fn response_exists() {
RouterPlugin::world()
.spawn(ExchangeSpawner::new_flow(|| {
(InfallibleSequence, children![
EndpointBuilder::get()
.with_handler(|| StatusCode::Ok.into_response()),
OnSpawn::observe(
|ev: On<GetOutcome>,
agents: AgentQuery,
response_query: Query<&Response>,
mut commands: Commands|
-> Result {
let action = ev.target();
let agent = agents.entity(action);
response_query.contains(agent).xpect_true();
commands
.entity(action)
.trigger_target(Outcome::Pass);
Ok(())
},
),
])
}))
.oneshot(Request::get("/"))
.await
.status()
.xpect_eq(StatusCode::Ok);
}
}