use crate::localization::TextRef;
use crate::routing::HttpMethod;
use std::any::Any;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ParamMeta {
pub name: &'static str,
pub source: &'static str,
pub type_hint: &'static str,
pub required: bool,
pub json_key: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub struct AuthorizeRule {
pub roles: &'static [&'static str],
pub permissions: &'static [&'static str],
}
impl AuthorizeRule {
pub const fn authenticated() -> Self {
Self {
roles: &[],
permissions: &[],
}
}
pub fn matches(&self, roles: &[String], permissions: &[String]) -> bool {
let role_ok = self.roles.is_empty()
|| self.roles.iter().any(|required| {
*required == "authenticated" || roles.iter().any(|actual| actual == required)
});
let permission_ok = self.permissions.is_empty()
|| self
.permissions
.iter()
.any(|required| permissions.iter().any(|actual| actual == required));
role_ok && permission_ok
}
}
#[derive(Debug, Clone)]
pub struct RouteEntry {
pub method: HttpMethod,
pub path: &'static str,
pub req_type_id: std::any::TypeId,
pub handler_type: &'static str,
pub rsp_type: &'static str,
pub name: TextRef,
pub description: TextRef,
pub category: TextRef,
pub params: &'static [ParamMeta],
pub authorization: &'static [AuthorizeRule],
}
inventory::collect!(RouteEntry);
#[derive(Debug, Clone)]
pub struct RequestParamEntry {
pub request_type: &'static str,
pub req_type_id: std::any::TypeId,
pub params: &'static [ParamMeta],
}
inventory::collect!(RequestParamEntry);
pub struct HandlerRegistration {
pub req_type_id: std::any::TypeId,
pub req_type_name: &'static str,
pub factory:
fn(&dyn rust_dix::IServiceResolver) -> crate::error::Result<Box<dyn std::any::Any + Send>>,
#[allow(clippy::type_complexity)]
pub call: fn(
handler: Box<dyn std::any::Any + Send>,
request: Box<dyn std::any::Any + Send>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = crate::error::Result<Box<dyn std::any::Any + Send>>>
+ Send,
>,
>,
}
inventory::collect!(HandlerRegistration);
pub type RouteDispatchFn = fn(
ctx: &mut dyn crate::http::IHttpContext,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = crate::error::Result<crate::action::Response>> + Send + '_,
>,
>;
pub struct RouteDispatch {
pub handler_type: &'static str,
pub dispatch: RouteDispatchFn,
}
inventory::collect!(RouteDispatch);
impl RouteEntry {
#[allow(clippy::too_many_arguments)]
pub const fn new(
method: HttpMethod,
path: &'static str,
handler_type: &'static str,
rsp_type: &'static str,
name: TextRef,
description: TextRef,
category: TextRef,
params: &'static [ParamMeta],
authorization: &'static [AuthorizeRule],
) -> Self {
Self {
method,
path,
req_type_id: std::any::TypeId::of::<()>(),
handler_type,
rsp_type,
name,
description,
category,
params,
authorization,
}
}
#[allow(clippy::too_many_arguments)]
pub const fn with_type_id(
method: HttpMethod,
path: &'static str,
req_type_id: std::any::TypeId,
handler_type: &'static str,
rsp_type: &'static str,
name: TextRef,
description: TextRef,
category: TextRef,
params: &'static [ParamMeta],
authorization: &'static [AuthorizeRule],
) -> Self {
Self {
method,
path,
req_type_id,
handler_type,
rsp_type,
name,
description,
category,
params,
authorization,
}
}
}
pub type BoxedDispatchFuture = std::pin::Pin<
Box<
dyn std::future::Future<Output = crate::error::Result<Box<dyn std::any::Any + Send>>>
+ Send,
>,
>;
pub type BoxedDispatchFn = fn(
std::sync::Arc<rust_dix::ServiceProvider>,
std::sync::Arc<HandlerCache>,
Box<dyn std::any::Any + Send>,
) -> BoxedDispatchFuture;
pub struct BoxedDispatch {
pub req_type_id: std::any::TypeId,
pub dispatch: BoxedDispatchFn,
}
inventory::collect!(BoxedDispatch);
pub struct HandlerCache {
pub entries: HashMap<&'static str, HandlerEntry>,
pub entries_by_id: HashMap<std::any::TypeId, HandlerEntry>,
boxed_dispatch: HashMap<std::any::TypeId, BoxedDispatchFn>,
}
impl HandlerCache {
pub fn build() -> Self {
let mut entries = HashMap::new();
let mut entries_by_id = HashMap::new();
for reg in inventory::iter::<HandlerRegistration> {
let entry = HandlerEntry {
factory: reg.factory,
call: reg.call,
};
entries.insert(reg.req_type_name, entry.clone());
entries_by_id.insert(reg.req_type_id, entry);
}
let mut boxed_dispatch = HashMap::new();
for reg in inventory::iter::<BoxedDispatch> {
boxed_dispatch.insert(reg.req_type_id, reg.dispatch);
}
Self {
entries,
entries_by_id,
boxed_dispatch,
}
}
pub fn get(&self, req_type_name: &str) -> Option<&HandlerEntry> {
self.entries.get(req_type_name)
}
pub fn get_by_type_id(&self, type_id: std::any::TypeId) -> Option<&HandlerEntry> {
self.entries_by_id.get(&type_id)
}
pub fn boxed_dispatch(&self, type_id: std::any::TypeId) -> Option<BoxedDispatchFn> {
self.boxed_dispatch.get(&type_id).copied()
}
}
pub type HandlerRegistry = HandlerCache;
#[derive(Clone)]
pub struct HandlerEntry {
pub factory: fn(&dyn rust_dix::IServiceResolver) -> crate::error::Result<Box<dyn Any + Send>>,
#[allow(clippy::type_complexity)]
pub call: fn(
handler: Box<dyn Any + Send>,
request: Box<dyn Any + Send>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = crate::error::Result<Box<dyn Any + Send>>> + Send>,
>,
}
#[cfg(test)]
mod tests {
struct AlwaysFails;
impl serde::Serialize for AlwaysFails {
fn serialize<S: serde::Serializer>(
&self,
_serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("DTO cannot be serialized"))
}
}
struct DiscardResponse;
#[async_trait::async_trait]
impl crate::http::IHttpResponse for DiscardResponse {
fn status(&self) -> u16 {
200
}
fn set_status(&mut self, _code: u16) {}
fn set_header(&mut self, _key: &str, _value: &str) {}
async fn write_bytes(&mut self, _data: Vec<u8>) -> crate::error::Result<()> {
Ok(())
}
}
#[tokio::test]
async fn json_response_serialization_failure_is_a_server_error() {
let err = crate::http::write_json_response(&mut DiscardResponse, &AlwaysFails)
.await
.unwrap_err();
assert!(
matches!(err, crate::error::Error::ResponseSerialization(_)),
"response serialization must not reuse the request-parse variant: {err:?}"
);
assert_eq!(err.status_code(), 500);
assert_eq!(err.public_detail(), None);
}
}