use topcoat::context::Cx;
use super::{Ability, Policy};
type RequestCheck = Box<dyn Fn(&Cx) -> bool + Send + Sync>;
type RecordCheck<M> = Box<dyn Fn(&Cx, &M) -> bool + Send + Sync>;
struct Pair<M> {
any: Option<RequestCheck>,
record: Option<RecordCheck<M>>,
}
impl<M> Default for Pair<M> {
fn default() -> Self {
Self {
any: None,
record: None,
}
}
}
impl<M> Pair<M> {
fn any_allows(&self, cx: &Cx) -> bool {
match &self.any {
Some(check) => check(cx),
None => self.record.is_some(),
}
}
fn record_allows(&self, cx: &Cx, record: &M) -> bool {
self.record.as_ref().is_some_and(|check| check(cx, record))
}
}
pub struct Abilities<M> {
view_any: Option<RequestCheck>,
view: Option<RecordCheck<M>>,
create: Option<RequestCheck>,
update: Option<RecordCheck<M>>,
delete: Pair<M>,
restore: Pair<M>,
force_delete: Pair<M>,
actions: Vec<(&'static str, Pair<M>)>,
header_actions: Vec<(&'static str, RequestCheck)>,
}
impl<M> Default for Abilities<M> {
fn default() -> Self {
Self {
view_any: None,
view: None,
create: None,
update: None,
delete: Pair::default(),
restore: Pair::default(),
force_delete: Pair::default(),
actions: Vec::new(),
header_actions: Vec::new(),
}
}
}
impl<M> Abilities<M> {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn view_any(mut self, check: impl Fn(&Cx) -> bool + Send + Sync + 'static) -> Self {
self.view_any = Some(Box::new(check));
self
}
#[must_use]
pub fn view(mut self, check: impl Fn(&Cx, &M) -> bool + Send + Sync + 'static) -> Self {
self.view = Some(Box::new(check));
self
}
#[must_use]
pub fn create(mut self, check: impl Fn(&Cx) -> bool + Send + Sync + 'static) -> Self {
self.create = Some(Box::new(check));
self
}
#[must_use]
pub fn update(mut self, check: impl Fn(&Cx, &M) -> bool + Send + Sync + 'static) -> Self {
self.update = Some(Box::new(check));
self
}
#[must_use]
pub fn delete_any(mut self, check: impl Fn(&Cx) -> bool + Send + Sync + 'static) -> Self {
self.delete.any = Some(Box::new(check));
self
}
#[must_use]
pub fn delete(mut self, check: impl Fn(&Cx, &M) -> bool + Send + Sync + 'static) -> Self {
self.delete.record = Some(Box::new(check));
self
}
#[must_use]
pub fn restore_any(mut self, check: impl Fn(&Cx) -> bool + Send + Sync + 'static) -> Self {
self.restore.any = Some(Box::new(check));
self
}
#[must_use]
pub fn restore(mut self, check: impl Fn(&Cx, &M) -> bool + Send + Sync + 'static) -> Self {
self.restore.record = Some(Box::new(check));
self
}
#[must_use]
pub fn force_delete_any(mut self, check: impl Fn(&Cx) -> bool + Send + Sync + 'static) -> Self {
self.force_delete.any = Some(Box::new(check));
self
}
#[must_use]
pub fn force_delete(mut self, check: impl Fn(&Cx, &M) -> bool + Send + Sync + 'static) -> Self {
self.force_delete.record = Some(Box::new(check));
self
}
fn action_mut(&mut self, name: &'static str) -> &mut Pair<M> {
let index = match self.actions.iter().position(|(action, _)| *action == name) {
Some(index) => index,
None => {
self.actions.push((name, Pair::default()));
self.actions.len() - 1
}
};
&mut self.actions[index].1
}
pub(crate) fn set_action_any(&mut self, name: &'static str, check: RequestCheck) {
self.action_mut(name).any = Some(check);
}
pub(crate) fn set_action_record(&mut self, name: &'static str, check: RecordCheck<M>) {
self.action_mut(name).record = Some(check);
}
pub(crate) fn set_header_action(&mut self, name: &'static str, check: RequestCheck) {
self.header_actions.retain(|(action, _)| *action != name);
self.header_actions.push((name, check));
}
fn action(&self, name: &str) -> Option<&Pair<M>> {
self.actions
.iter()
.find(|(action, _)| *action == name)
.map(|(_, pair)| pair)
}
}
impl<M> std::fmt::Debug for Abilities<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let declared = [
("view_any", self.view_any.is_some()),
("view", self.view.is_some()),
("create", self.create.is_some()),
("update", self.update.is_some()),
("delete_any", self.delete.any.is_some()),
("delete", self.delete.record.is_some()),
("restore_any", self.restore.any.is_some()),
("restore", self.restore.record.is_some()),
("force_delete_any", self.force_delete.any.is_some()),
("force_delete", self.force_delete.record.is_some()),
];
f.debug_struct("Abilities")
.field(
"declared",
&declared
.iter()
.filter_map(|(name, set)| set.then_some(*name))
.collect::<Vec<_>>(),
)
.field(
"actions",
&self
.actions
.iter()
.map(|(name, _)| name)
.collect::<Vec<_>>(),
)
.field(
"header_actions",
&self
.header_actions
.iter()
.map(|(name, _)| name)
.collect::<Vec<_>>(),
)
.finish()
}
}
impl<M: 'static> Policy<M> for Abilities<M> {
fn allows(&self, cx: &Cx, ability: Ability<'_, M>) -> bool {
match ability {
Ability::ViewAny => self.view_any.as_ref().is_some_and(|check| check(cx)),
Ability::View(record) => self.view.as_ref().is_some_and(|check| check(cx, record)),
Ability::Create => self.create.as_ref().is_some_and(|check| check(cx)),
Ability::Update(record) => self.update.as_ref().is_some_and(|check| check(cx, record)),
Ability::DeleteAny => self.delete.any_allows(cx),
Ability::Delete(record) => self.delete.record_allows(cx, record),
Ability::RestoreAny => self.restore.any_allows(cx),
Ability::Restore(record) => self.restore.record_allows(cx, record),
Ability::ForceDeleteAny => self.force_delete.any_allows(cx),
Ability::ForceDelete(record) => self.force_delete.record_allows(cx, record),
Ability::RunAny { action } => self.action(action).is_some_and(|p| p.any_allows(cx)),
Ability::Run { action, record } => self
.action(action)
.is_some_and(|pair| pair.record_allows(cx, record)),
Ability::RunHeader { action } => self
.header_actions
.iter()
.find(|(name, _)| *name == action)
.is_some_and(|(_, check)| check(cx)),
}
}
}