use frunk::{HCons, HNil, hlist::HList};
use crate::{
app::App,
capability::{ApplyHooks, CapStore, Capability, mount_with_hooks},
tag::Tagged,
traits::add::{AddCapability, CapTagAbsent},
};
pub struct AppsTag;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PluginType {
App,
Addon,
}
#[derive(Clone, Debug)]
pub struct AppTile {
pub key: String,
pub verbose_name: String,
pub href: String,
pub icon: String,
pub plugin_type: PluginType,
pub roles: Vec<String>,
}
pub trait AppsRegistrar: Sized {
fn register_apps(self, apps: AppsCapability) -> AppsCapability;
}
#[derive(Clone, Debug, Default)]
pub struct AppsCapability {
apps: Vec<AppTile>,
}
impl AppsCapability {
pub fn new() -> Self {
Self { apps: Vec::new() }
}
pub fn register(mut self, tile: AppTile) -> Self {
if let Some(existing) = self.apps.iter_mut().find(|a| a.key == tile.key) {
*existing = tile;
} else {
self.apps.push(tile);
}
self
}
pub fn apps(&self) -> &[AppTile] {
&self.apps
}
pub fn visible_apps(&self, role: &str, is_superuser: bool, is_staff: bool) -> Vec<AppTile> {
let mut apps: Vec<_> = self
.apps
.iter()
.filter(|a| a.plugin_type == PluginType::App)
.filter(|a| {
if is_superuser {
return true;
}
if a.roles.is_empty() {
is_staff
} else {
a.roles.iter().any(|r| r == role)
}
})
.cloned()
.collect();
apps.sort_by(|a, b| a.verbose_name.cmp(&b.verbose_name));
apps
}
}
pub type AppsCap<Hooks> = CapStore<AppsTag, Hooks, AppsCapability>;
impl<Hooks> AppsCap<Hooks> {
pub fn resolve_hooks<Proof>(self) -> AppsCap<HNil>
where
Hooks: ApplyHooks<AppsCapability, Proof, Output = AppsCapability>,
{
CapStore::with_items(self.hooks.apply_hooks(self.items))
}
}
impl<Plugin, H, Tail, TailProof> ApplyHooks<AppsCapability, (TailProof, ())>
for HCons<Tagged<Plugin, H>, Tail>
where
Tail: ApplyHooks<AppsCapability, TailProof, Output = AppsCapability>,
H: AppsRegistrar,
{
type Output = AppsCapability;
fn apply_hooks(self, items: AppsCapability) -> Self::Output {
let items = self.tail.apply_hooks(items);
self.head.value.register_apps(items)
}
}
impl<Hooks> Capability for AppsCap<Hooks>
where
Hooks: ApplyHooks<AppsCapability, (), Output = AppsCapability>,
{
type Value = AppsCapability;
type Output = Tagged<AppsTag, AppsCapability>;
type Hooks = Hooks;
type Items = AppsCapability;
fn mount(self) -> Self::Output {
mount_with_hooks(self, |items| items)
}
}
#[macro_export]
macro_rules! define_register_apps {
(
plugin: $plugin:ty;
key: $key:expr;
name: $name:expr;
href: $href:expr;
icon: $icon:expr;
$(plugin_type: $ptype:expr;)?
roles: [$($role:expr),* $(,)?];
) => {
#[derive(Clone, Copy, Default)]
pub struct Hook;
impl $crate::apps::AppsRegistrar for Hook {
fn register_apps(self, apps: $crate::apps::AppsCapability) -> $crate::apps::AppsCapability {
apps.register($crate::apps::AppTile {
key: ::std::convert::Into::into($key),
verbose_name: ::std::convert::Into::into($name),
href: ::std::convert::Into::into($href),
icon: ::std::convert::Into::into($icon),
plugin_type: $crate::define_register_apps!(@plugin_type $($ptype)?),
roles: vec![$(::std::convert::Into::into($role)),*],
})
}
}
};
(@plugin_type) => {
$crate::apps::PluginType::App
};
(@plugin_type $ptype:expr) => {
$ptype
};
}
pub use crate::define_register_apps;
pub fn with_apps<L, Proof>(app: App<L>) -> App<HCons<AppsCap<HNil>, L>>
where
L: HList + CapTagAbsent<AppsTag, Proof>,
{
app.add_capability(CapStore::with_items(AppsCapability::new()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugins::blog::BlogTag;
use crate::plugins::users::UsersTag;
#[test]
fn visible_after_register_apps() {
let apps = AppsCapability::new();
let apps = crate::plugins::users::apps::Hook.register_apps(apps);
let apps = crate::plugins::blog::apps::Hook.register_apps(apps);
assert_eq!(apps.apps().len(), 2);
let visible = apps.visible_apps("admin", false, true);
let keys: Vec<_> = visible.iter().map(|t| t.key.as_str()).collect();
assert!(keys.contains(&"p_users"), "{keys:?}");
assert!(keys.contains(&"p_blog"), "{keys:?}");
}
#[test]
fn unassigned_role_sees_no_empty_role_apps() {
let apps = AppsCapability::new();
let apps = crate::plugins::users::apps::Hook.register_apps(apps);
let apps = crate::plugins::otp::apps::Hook.register_apps(apps);
let visible = apps.visible_apps("unassigned", false, false);
assert!(visible.is_empty(), "{visible:?}");
}
}