use std::{any::Any, future::Future};
use derive_where::derive_where;
use topcoat::{Result, context::Cx, icon::IconData};
mod abilities;
mod builtin;
mod keys;
mod places;
pub use keys::filter_keys;
pub use places::Places;
use super::{Mounted, Resource, WriteError};
use crate::{
BoxFuture, Color,
form::{FieldError, FieldErrors, FormInput, FormValues},
naming::{derived_id, sentence_case},
policy::Ability,
schema::Schema,
};
pub(crate) const RESERVED_KEYS: [&str; 4] = [crate::csrf::FIELD_NAME, "confirm", "ids", "matching"];
pub trait Action<R: Resource>: 'static {
type Input: FormInput;
const NAME: &'static str;
const PLACES: Places = Places::ALL;
const CONFIRM: bool = false;
const ICON: Option<IconData> = None;
const COLOR: Color = Color::Gray;
fn confirmation() -> crate::Text {
crate::text!("tablo-confirm-run")
}
fn label() -> crate::Text {
crate::Text::derived(derived_id("action", Self::NAME), sentence_case(Self::NAME))
}
fn label_for(_cx: &Cx, _record: &R::Model) -> crate::Text {
Self::label()
}
fn icon_for(_cx: &Cx, _record: &R::Model) -> Option<IconData> {
Self::ICON
}
fn tooltip() -> Option<crate::Text> {
None
}
fn can_run(_cx: &Cx, _record: &R::Model) -> bool {
true
}
fn disabled_reason(_cx: &Cx, _record: &R::Model) -> Option<crate::Text> {
None
}
fn validate_input(
_cx: &Cx,
_input: &Self::Input,
) -> FieldErrors<<Self::Input as FormInput>::Field> {
FieldErrors::new()
}
fn run(
cx: &Cx,
records: &[R::Model],
input: Self::Input,
ex: &mut dyn toasty::Executor,
) -> impl Future<Output = Result<crate::Outcome, WriteError<<Self::Input as FormInput>::Field>>> + Send;
fn success(count: usize) -> crate::Text {
crate::text!(
"tablo-action-on-records",
action = Self::label(),
count = count
)
}
}
pub(crate) type RunAction<R> = for<'a> fn(
&'a Cx,
&'a [<R as Resource>::Model],
ErasedInput,
&'a mut dyn toasty::Executor,
) -> BoxFuture<'a, Result<crate::Outcome, WriteError>>;
pub(crate) type ErasedInput = Box<dyn Any + Send>;
pub(crate) type InputResult = std::result::Result<ErasedInput, Vec<FieldError>>;
#[derive(Clone, Copy)]
pub(crate) struct InputSpec {
pub(crate) schema: fn() -> Schema,
pub(crate) takes_input: bool,
pub(crate) parse: fn(&Cx, &FormValues) -> InputResult,
}
impl InputSpec {
pub(crate) fn of<I: FormInput>(parse: fn(&Cx, &FormValues) -> InputResult) -> Self {
Self {
schema: I::schema,
takes_input: !I::schema().is_empty(),
parse,
}
}
pub(crate) fn none() -> Self {
Self {
schema: Schema::empty,
takes_input: false,
parse: parse_nothing,
}
}
}
pub(crate) struct Actions<R: Resource> {
entries: Vec<ActionEntry<R>>,
}
impl<R: Resource> Default for Actions<R> {
fn default() -> Self {
Self {
entries: Vec::new(),
}
}
}
impl<R: Resource> std::fmt::Debug for Actions<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.entries.iter().map(|e| e.name))
.finish()
}
}
impl<R: Resource> Actions<R> {
pub(crate) fn add<A: Action<R>>(mut self) -> Self {
const {
assert!(
crate::declaration::segment_fault(A::NAME).is_none(),
"`Action::NAME` must be a single path segment"
);
}
self.entries.push(ActionEntry {
name: A::NAME,
label: A::label,
places: A::PLACES,
resource_wide: Ability::RunAny { action: A::NAME },
can_run: can_run_erased::<R, A>,
disabled: disabled_erased::<R, A>,
input: InputSpec::of::<A::Input>(parse_input_erased::<R, A>),
run: run_erased::<R, A>,
success: A::success,
change: super::acted::<R, A>,
failure: "tablo-failed-run",
confirm: A::CONFIRM,
trashed: super::Trashed::Without,
removes: false,
prompt: |_| A::confirmation(),
icon: || A::ICON,
label_for: A::label_for,
icon_for: A::icon_for,
tooltip: A::tooltip,
color: A::COLOR,
});
self
}
pub(crate) fn add_trash_actions(&mut self) {
self.entries.push(ActionEntry::restore());
self.entries.push(ActionEntry::force_delete());
}
pub(crate) fn entries(&self) -> &[ActionEntry<R>] {
&self.entries
}
pub(crate) fn find(&self, name: &str) -> Option<&ActionEntry<R>> {
self.entries.iter().find(|e| e.name == name)
}
}
#[derive_where(Clone, Copy)]
pub(crate) struct ActionEntry<R: Resource> {
pub(crate) name: &'static str,
pub(crate) label: fn() -> crate::Text,
pub(crate) places: Places,
pub(crate) resource_wide: Ability<'static, R::Model>,
pub(crate) can_run: fn(&Mounted<R>, &Cx, &R::Model) -> bool,
pub(crate) disabled: fn(&Mounted<R>, &Cx, &R::Model) -> Option<crate::Text>,
pub(crate) input: InputSpec,
pub(crate) run: RunAction<R>,
pub(crate) success: fn(usize) -> crate::Text,
pub(crate) change: fn(Vec<R::Model>) -> super::Change<R::Model>,
pub(crate) failure: &'static str,
pub(crate) confirm: bool,
pub(crate) trashed: super::Trashed,
pub(crate) removes: bool,
pub(crate) prompt: fn(bool) -> crate::Text,
pub(crate) icon: fn() -> Option<IconData>,
pub(crate) label_for: fn(&Cx, &R::Model) -> crate::Text,
pub(crate) icon_for: fn(&Cx, &R::Model) -> Option<IconData>,
pub(crate) tooltip: fn() -> Option<crate::Text>,
pub(crate) color: Color,
}
impl<R: Resource> ActionEntry<R> {
pub(crate) fn allows(&self, resource: &Mounted<R>, cx: &Cx, record: &R::Model) -> bool {
self.trashed.admits(resource.is_trashed(record)) && (self.can_run)(resource, cx, record)
}
pub(crate) fn disabled_on(
&self,
resource: &Mounted<R>,
cx: &Cx,
record: &R::Model,
) -> Option<crate::Text> {
if !self.trashed.admits(resource.is_trashed(record)) {
return None;
}
(self.disabled)(resource, cx, record)
}
}
fn can_run_erased<R: Resource, A: Action<R>>(
resource: &Mounted<R>,
cx: &Cx,
record: &R::Model,
) -> bool {
resource.can(
cx,
Ability::Run {
action: A::NAME,
record,
},
) && A::can_run(cx, record)
}
fn disabled_erased<R: Resource, A: Action<R>>(
resource: &Mounted<R>,
cx: &Cx,
record: &R::Model,
) -> Option<crate::Text> {
let run = Ability::Run {
action: A::NAME,
record,
};
if !resource.can(cx, run) || A::can_run(cx, record) {
return None;
}
A::disabled_reason(cx, record)
}
fn parse_input_erased<R: Resource, A: Action<R>>(cx: &Cx, values: &FormValues) -> InputResult {
parse_validated(cx, values, A::validate_input)
}
pub(crate) fn parse_validated<I: FormInput>(
cx: &Cx,
values: &FormValues,
validate: fn(&Cx, &I) -> FieldErrors<I::Field>,
) -> InputResult {
let input = I::parse(cx, values)?;
let refused: Vec<FieldError> = validate(cx, &input)
.into_iter()
.map(|error| FieldError {
key: I::key(error.key).to_string(),
kind: error.kind,
})
.collect();
if !refused.is_empty() {
return Err(refused);
}
Ok(Box::new(input))
}
fn parse_nothing(_cx: &Cx, _values: &FormValues) -> InputResult {
Ok(Box::new(()))
}
fn run_erased<'a, R: Resource, A: Action<R>>(
cx: &'a Cx,
records: &'a [R::Model],
input: ErasedInput,
ex: &'a mut dyn toasty::Executor,
) -> BoxFuture<'a, Result<crate::Outcome, WriteError>> {
let input = *input
.downcast::<A::Input>()
.expect("the pipeline parses the input `run` takes");
Box::pin(async move {
A::run(cx, records, input, ex)
.await
.map_err(|error| error.keyed(<A::Input as FormInput>::key))
})
}