use crate::http::ResponseBody;
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<ResponseData>> + Send + '_>,
>;
pub struct RouteDispatch {
pub handler_type: &'static str,
pub dispatch: RouteDispatchFn,
}
#[derive(Debug)]
pub struct ResponseData {
pub status: u16,
pub content_type: String,
pub headers: Vec<(String, String)>,
pub body: ResponseBody,
}
impl ResponseData {
pub fn new(status: u16) -> Self {
Self {
status,
content_type: String::new(),
headers: Vec::new(),
body: ResponseBody::Bytes(Vec::new()),
}
}
pub fn json<T: serde::Serialize + ?Sized>(value: &T) -> crate::error::Result<Self> {
let body = serde_json::to_vec(value).map_err(crate::error::Error::ResponseSerialization)?;
Ok(Self {
status: 200,
content_type: "application/json".to_string(),
headers: Vec::new(),
body: ResponseBody::Bytes(body),
})
}
pub fn no_content() -> Self {
Self {
status: 204,
content_type: "application/json".to_string(),
headers: Vec::new(),
body: ResponseBody::Bytes(Vec::new()),
}
}
pub fn text(body: impl Into<String>) -> Self {
Self {
status: 200,
content_type: "text/plain; charset=utf-8".to_string(),
headers: Vec::new(),
body: ResponseBody::Bytes(body.into().into_bytes()),
}
}
pub fn html(body: impl Into<String>) -> Self {
Self {
status: 200,
content_type: "text/html; charset=utf-8".to_string(),
headers: Vec::new(),
body: ResponseBody::Bytes(body.into().into_bytes()),
}
}
pub fn bytes(body: impl Into<Vec<u8>>) -> Self {
Self {
status: 200,
content_type: "application/octet-stream".to_string(),
headers: Vec::new(),
body: ResponseBody::Bytes(body.into()),
}
}
pub fn file(path: impl Into<std::path::PathBuf>) -> Self {
Self::with_file(crate::http::FileBody::path(path))
}
pub fn file_stream(
reader: impl tokio::io::AsyncRead + Send + Unpin + 'static,
content_type: impl Into<String>,
) -> Self {
Self::with_file(crate::http::FileBody::stream(reader).content_type(content_type))
}
pub fn file_seekable_stream(
reader: impl tokio::io::AsyncRead + tokio::io::AsyncSeek + Send + Unpin + 'static,
len: u64,
content_type: impl Into<String>,
) -> Self {
Self::with_file(
crate::http::FileBody::seekable_stream(reader, len).content_type(content_type),
)
}
pub fn with_file(file: crate::http::FileBody) -> Self {
Self {
status: 200,
content_type: String::new(),
headers: Vec::new(),
body: ResponseBody::File(file),
}
}
pub fn status(mut self, status: u16) -> Self {
self.status = status;
self
}
pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
self.content_type = content_type.into();
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((key.into(), value.into()));
self
}
pub fn download_name(mut self, file_name: impl Into<String>) -> Self {
self.set_disposition(crate::http::Disposition::Attachment, file_name.into());
self
}
pub fn inline_name(mut self, file_name: impl Into<String>) -> Self {
self.set_disposition(crate::http::Disposition::Inline, file_name.into());
self
}
pub fn enable_range_processing(mut self, enabled: bool) -> Self {
if let ResponseBody::File(file) = &mut self.body {
file.enable_range_processing = enabled;
}
self
}
pub fn last_modified(mut self, last_modified: std::time::SystemTime) -> Self {
if let ResponseBody::File(file) = &mut self.body {
file.last_modified = Some(last_modified);
} else {
let value = httpdate::fmt_http_date(last_modified);
self.headers.push(("last-modified".to_string(), value));
}
self
}
pub fn entity_tag(mut self, entity_tag: impl Into<String>) -> Self {
let entity_tag = entity_tag.into();
if let ResponseBody::File(file) = &mut self.body {
file.entity_tag = Some(entity_tag);
} else {
self.headers.push(("etag".to_string(), entity_tag));
}
self
}
fn set_disposition(&mut self, disposition: crate::http::Disposition, file_name: String) {
if let ResponseBody::File(file) = &mut self.body {
file.file_name = Some(file_name);
file.disposition = disposition;
} else {
let value = crate::http::content_disposition_value(disposition, &file_name);
self.headers
.push(("content-disposition".to_string(), value));
}
}
}
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 {
use super::*;
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"))
}
}
#[test]
fn response_data_json_failure_is_a_server_error() {
let err = ResponseData::json(&AlwaysFails).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);
}
}