use std::future::Future;
use topcoat::{Result, context::Cx};
use super::{ErasedInput, InputResult, InputSpec, parse_validated};
use crate::{
BoxFuture, FormInput,
form::{FieldErrors, FormValues},
naming::{derived_id, sentence_case},
policy::Ability,
};
pub trait HeaderAction: 'static {
type Input: FormInput;
const NAME: &'static str;
const CONFIRM: bool = false;
fn label(cx: &Cx) -> String {
crate::Text::derived(derived_id("action", Self::NAME), sentence_case(Self::NAME))
.localize(cx)
}
fn can_run(_cx: &Cx) -> bool {
true
}
fn validate_input(_cx: &Cx, _input: &Self::Input) -> FieldErrors {
FieldErrors::new()
}
fn run(
cx: &Cx,
input: Self::Input,
ex: &mut dyn toasty::Executor,
) -> impl Future<Output = Result<crate::Outcome, crate::WriteError>> + Send;
fn success(cx: &Cx) -> String {
crate::t!(cx, "tablo-action-done", action = Self::label(cx))
}
}
impl<M> Ability<'_, M> {
pub fn is_header_action<A: HeaderAction>(self) -> bool {
matches!(self, Ability::RunHeader { action } if action == A::NAME)
}
}
impl<M> crate::policy::Abilities<M> {
#[must_use]
pub fn run_header<A: HeaderAction>(
mut self,
check: impl Fn(&Cx) -> bool + Send + Sync + 'static,
) -> Self {
self.set_header_action(A::NAME, Box::new(check));
self
}
}
#[derive(Default)]
pub struct HeaderActions {
entries: Vec<HeaderEntry>,
}
impl std::fmt::Debug for HeaderActions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.entries.iter().map(|e| e.name))
.finish()
}
}
impl HeaderActions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn add<A: HeaderAction>(mut self) -> Self {
const {
assert!(
crate::declaration::segment_fault(A::NAME).is_none(),
"`HeaderAction::NAME` must be a single path segment"
);
}
self.entries.push(HeaderEntry {
name: A::NAME,
label: A::label,
can_run: A::can_run,
input: InputSpec::of::<A::Input>(parse_header_input::<A>),
run: run_header_erased::<A>,
success: A::success,
confirm: A::CONFIRM,
});
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub(crate) fn entries(&self) -> &[HeaderEntry] {
&self.entries
}
pub(crate) fn find(&self, name: &str) -> Option<&HeaderEntry> {
self.entries.iter().find(|e| e.name == name)
}
}
pub(crate) type RunHeader = for<'a> fn(
&'a Cx,
ErasedInput,
&'a mut dyn toasty::Executor,
) -> BoxFuture<'a, Result<crate::Outcome, crate::WriteError>>;
#[derive(Clone, Copy)]
pub(crate) struct HeaderEntry {
pub(crate) name: &'static str,
pub(crate) label: fn(&Cx) -> String,
pub(crate) can_run: fn(&Cx) -> bool,
pub(crate) input: InputSpec,
pub(crate) run: RunHeader,
pub(crate) success: fn(&Cx) -> String,
pub(crate) confirm: bool,
}
fn parse_header_input<A: HeaderAction>(cx: &Cx, values: &FormValues) -> InputResult {
parse_validated(cx, values, A::validate_input)
}
fn run_header_erased<'a, A: HeaderAction>(
cx: &'a Cx,
input: ErasedInput,
ex: &'a mut dyn toasty::Executor,
) -> BoxFuture<'a, Result<crate::Outcome, crate::WriteError>> {
let input = *input
.downcast::<A::Input>()
.expect("the pipeline parses the input `run` takes");
Box::pin(A::run(cx, input, ex))
}