mod delete;
mod detail;
mod method;
mod patch;
mod path;
mod render;
pub use delete::{DeleteEntity, DeleteLayer, HasDeleteState};
pub use detail::{DetailLayer, HasLoadState, LoadById};
pub use method::MethodLayer;
pub use patch::{FoldQueryPatchers, QueryPatcher};
pub use path::{PathLayer, PathParams, PathTag};
pub use render::{html_built_page_or_app_layout, html_built_page_with_slots, render_from_data};
use std::future::Future;
use std::marker::PhantomData;
use axum::{
http::{HeaderMap, Method, Uri},
response::Response,
};
use frunk::{HCons, HNil, hlist::HList};
use serde::de::DeserializeOwned;
use crate::tag::Tagged;
pub trait LayerContrib {
type Contrib: HList;
}
pub trait PrependToType<Acc> {
type Output;
}
impl<Acc> PrependToType<Acc> for HNil {
type Output = Acc;
}
impl<H, T, Acc> PrependToType<Acc> for HCons<H, T>
where
T: PrependToType<Acc>,
{
type Output = HCons<H, <T as PrependToType<Acc>>::Output>;
}
pub trait PrependTo<Acc>: Sized {
type Output;
fn prepend_to(self, acc: Acc) -> Self::Output;
}
impl<Acc> PrependTo<Acc> for HNil {
type Output = Acc;
fn prepend_to(self, acc: Acc) -> Self::Output {
acc
}
}
impl<H, T, Acc> PrependTo<Acc> for HCons<H, T>
where
T: PrependTo<Acc>,
{
type Output = HCons<H, <T as PrependTo<Acc>>::Output>;
fn prepend_to(self, acc: Acc) -> Self::Output {
HCons {
head: self.head,
tail: self.tail.prepend_to(acc),
}
}
}
pub trait FoldLayerData {
type Data: HList;
}
impl FoldLayerData for HNil {
type Data = HNil;
}
impl<L, Tail> FoldLayerData for HCons<L, Tail>
where
L: LayerContrib,
Tail: FoldLayerData,
L::Contrib: PrependToType<Tail::Data>,
<L::Contrib as PrependToType<Tail::Data>>::Output: HList,
{
type Data = <L::Contrib as PrependToType<Tail::Data>>::Output;
}
pub trait BuildFromData<Data>: Sized + 'static {
fn build_from_data(data: &Data) -> Self;
}
pub enum LayerStep<Acc> {
Continue(Acc),
Done(Response),
}
#[derive(Clone, Debug, Default)]
pub struct LayerRequest {
pub method: Method,
pub uri: Uri,
pub headers: HeaderMap,
pub path: PathParams,
pub auth_present: bool,
}
impl LayerRequest {
pub fn new(method: Method, uri: Uri, headers: HeaderMap) -> Self {
Self {
method,
uri,
headers,
path: PathParams::default(),
auth_present: false,
}
}
pub fn query_as<Q>(&self) -> Q
where
Q: DeserializeOwned + Default,
{
let Some(query) = self.uri.query() else {
return Q::default();
};
if query.is_empty() {
return Q::default();
}
let body = query.as_bytes();
crate::html_form::UrlencodedFields::parse(body)
.and_then(|f| f.deserialize())
.unwrap_or_default()
}
pub fn path_i64(&self, name: &str) -> Option<i64> {
self.path.get(name)?.parse().ok()
}
pub fn path_param(&self, name: &str) -> Option<&str> {
self.path.get(name)
}
pub fn with_path_id(mut self, id: i64) -> Self {
self.path.insert("id", id.to_string());
self
}
pub fn with_path(mut self, name: impl Into<String>, value: impl ToString) -> Self {
self.path.insert(name.into(), value.to_string());
self
}
}
pub trait ViewLayer<Ctx, Acc> {
type AccOut: HList;
fn run<'a>(
&'a self,
ctx: &'a mut Ctx,
req: &'a mut LayerRequest,
acc: Acc,
) -> impl Future<Output = LayerStep<Self::AccOut>> + Send + 'a
where
Acc: Send + 'a;
}
pub trait RunLayers<Ctx, Acc>: FoldLayerData {
fn run_all<'a>(
&'a self,
ctx: &'a mut Ctx,
req: &'a mut LayerRequest,
acc: Acc,
) -> impl Future<Output = LayerStep<Self::Data>> + Send + 'a
where
Acc: Send + 'a,
Ctx: Send + 'a;
}
impl<Ctx> RunLayers<Ctx, HNil> for HNil {
fn run_all<'a>(
&'a self,
_ctx: &'a mut Ctx,
_req: &'a mut LayerRequest,
acc: HNil,
) -> impl Future<Output = LayerStep<Self::Data>> + Send + 'a
where
HNil: Send + 'a,
Ctx: Send + 'a,
{
async move { LayerStep::Continue(acc) }
}
}
impl<Ctx, Acc, L, Tail> RunLayers<Ctx, Acc> for HCons<L, Tail>
where
Acc: HList + Send,
Tail: RunLayers<Ctx, Acc> + Sync,
<Tail as FoldLayerData>::Data: Send,
L: ViewLayer<Ctx, <Tail as FoldLayerData>::Data> + Sync,
L::AccOut: HList + Send,
Self: FoldLayerData<Data = L::AccOut>,
Ctx: Send,
{
fn run_all<'a>(
&'a self,
ctx: &'a mut Ctx,
req: &'a mut LayerRequest,
acc: Acc,
) -> impl Future<Output = LayerStep<Self::Data>> + Send + 'a
where
Acc: Send + 'a,
Ctx: Send + 'a,
{
async move {
match self.tail.run_all(ctx, req, acc).await {
LayerStep::Done(r) => LayerStep::Done(r),
LayerStep::Continue(mid) => self.head.run(ctx, req, mid).await,
}
}
}
}
pub async fn run_layers<L, Ctx>(
layers: &L,
ctx: &mut Ctx,
req: &mut LayerRequest,
) -> LayerStep<L::Data>
where
L: RunLayers<Ctx, HNil>,
Ctx: Send,
{
layers.run_all(ctx, req, HNil).await
}
pub struct View<PageTag, Layers> {
pub layers: Layers,
_page: PhantomData<fn() -> PageTag>,
}
impl<PageTag> View<PageTag, HNil> {
pub const fn new() -> Self {
Self {
layers: HNil,
_page: PhantomData,
}
}
}
impl<PageTag> Default for View<PageTag, HNil> {
fn default() -> Self {
Self::new()
}
}
impl<PageTag, Layers> View<PageTag, Layers> {
pub fn layer<L>(self, layer: L) -> View<PageTag, HCons<L, Layers>> {
View {
layers: HCons {
head: layer,
tail: self.layers,
},
_page: PhantomData,
}
}
pub fn layers_ref(&self) -> &Layers {
&self.layers
}
}
pub fn view<PageTag>() -> View<PageTag, HNil> {
View::new()
}
pub fn cons_tagged<Tag, V, Acc>(value: V, acc: Acc) -> HCons<Tagged<Tag, V>, Acc> {
HCons {
head: Tagged::new(value),
tail: acc,
}
}
#[cfg(test)]
mod tests {
use frunk::{HCons, HNil};
use super::*;
use crate::tag::Tagged;
use crate::traits::get::GetByTag;
struct TagA;
struct TagB;
struct LayerA;
struct LayerB;
impl LayerContrib for LayerA {
type Contrib = HCons<Tagged<TagA, u8>, HNil>;
}
impl LayerContrib for LayerB {
type Contrib = HCons<Tagged<TagB, bool>, HNil>;
}
#[test]
fn fold_layer_data_prepending_contribs() {
type Stack = HCons<LayerB, HCons<LayerA, HNil>>;
type Data = <Stack as FoldLayerData>::Data;
fn assert_data(_d: &Data) {}
let data: Data = HCons {
head: Tagged::<TagB, _>::new(true),
tail: HCons {
head: Tagged::<TagA, _>::new(7u8),
tail: HNil,
},
};
assert_data(&data);
assert!(*GetByTag::<TagB, _>::get_by_tag(&data));
assert_eq!(*GetByTag::<TagA, _>::get_by_tag(&data), 7u8);
}
struct Page {
a: u8,
b: bool,
}
impl BuildFromData<HCons<Tagged<TagB, bool>, HCons<Tagged<TagA, u8>, HNil>>> for Page {
fn build_from_data(
data: &HCons<Tagged<TagB, bool>, HCons<Tagged<TagA, u8>, HNil>>,
) -> Self {
Self {
a: *GetByTag::<TagA, _>::get_by_tag(data),
b: *GetByTag::<TagB, _>::get_by_tag(data),
}
}
}
#[test]
fn build_from_data_projects_tags() {
let data = HCons {
head: Tagged::<TagB, _>::new(false),
tail: HCons {
head: Tagged::<TagA, _>::new(3u8),
tail: HNil,
},
};
let page = Page::build_from_data(&data);
assert_eq!(page.a, 3);
assert!(!page.b);
}
struct FancyPage {
a: u8,
banner: &'static str,
}
impl BuildFromData<HCons<Tagged<TagA, u8>, HNil>> for FancyPage {
fn build_from_data(data: &HCons<Tagged<TagA, u8>, HNil>) -> Self {
Self {
a: *GetByTag::<TagA, _>::get_by_tag(data),
banner: "extra",
}
}
}
#[test]
fn build_from_data_allows_extra_page_fields() {
let data = HCons {
head: Tagged::<TagA, _>::new(1u8),
tail: HNil,
};
let page = FancyPage::build_from_data(&data);
assert_eq!(page.a, 1);
assert_eq!(page.banner, "extra");
}
#[test]
fn role_layer_allow_is_per_stack() {
use crate::plugins::users::layers::RoleLayer;
let editors = RoleLayer::allow(&["editor", "admin"]);
let admins = RoleLayer::allow(&["admin"]);
assert_eq!(editors.roles, &["editor", "admin"]);
assert_eq!(admins.roles, &["admin"]);
}
#[test]
fn missing_data_tag_is_a_compile_time_concern() {
let _ = std::any::type_name::<FancyPage>();
}
}