use std::future::Future;
use toasty::Executor;
use topcoat::{Result, context::Cx};
use super::{Panel, state::current};
use crate::{
BoxFuture, ModelColumns, Text,
resource::{Change, Resource, require_mounted},
};
pub trait CommitHook: 'static {
fn before_commit(
cx: &Cx,
change: &PanelChange,
ex: &mut dyn Executor,
) -> impl Future<Output = Result<()>> + Send;
}
#[derive(Debug, Clone, PartialEq)]
pub struct PanelChange {
slug: String,
label: Text,
change: Change<ChangedRecord>,
}
impl PanelChange {
pub fn slug(&self) -> &str {
&self.slug
}
pub fn label(&self) -> &Text {
&self.label
}
pub fn change(&self) -> &Change<ChangedRecord> {
&self.change
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangedRecord {
key: String,
url: Option<String>,
fields: Vec<ChangedField>,
}
impl ChangedRecord {
pub fn key(&self) -> &str {
&self.key
}
pub fn url(&self) -> Option<&str> {
self.url.as_deref()
}
pub fn fields(&self) -> &[ChangedField] {
&self.fields
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangedField {
name: String,
label: Text,
before: Option<String>,
after: Option<String>,
}
impl ChangedField {
pub fn name(&self) -> &str {
&self.name
}
pub fn label(&self) -> &Text {
&self.label
}
pub fn before(&self) -> Option<&str> {
self.before.as_deref()
}
pub fn after(&self) -> Option<&str> {
self.after.as_deref()
}
}
fn changed_fields<M: ModelColumns>(
cx: &Cx,
before: Option<&M>,
after: Option<&M>,
) -> Vec<ChangedField> {
let table = M::table();
let (before_record, after_record) = (before, after);
let spell = |record: Option<&M>| record.map(|record| table.spell(cx, record));
let (before, after) = (spell(before), spell(after));
let columns = after.as_ref().or(before.as_ref()).map_or(0, Vec::len);
let held = |spelled: &Option<Vec<(String, Text, String)>>, index: usize| {
spelled
.as_ref()
.map(|spelled| spelled[index].2.clone())
.filter(|text| !text.is_empty())
};
let mut changed: Vec<ChangedField> = (0..columns)
.filter_map(|index| {
let (was, is) = (held(&before, index), held(&after, index));
let (name, label, _) = after.as_ref().or(before.as_ref())?[index].clone();
(was != is).then_some(ChangedField {
name,
label,
before: was,
after: is,
})
})
.collect();
let references = |record: Option<&M>| record.map(ModelColumns::references);
let (was, is) = (references(before_record), references(after_record));
let names = is.as_ref().or(was.as_ref()).cloned().unwrap_or_default();
for (index, (name, _)) in names.into_iter().enumerate() {
let held = |spelled: &Option<Vec<(&str, Option<String>)>>| {
spelled
.as_ref()
.and_then(|spelled| spelled[index].1.clone())
};
let (before, after) = (held(&was), held(&is));
if before != after && changed.iter().all(|field| field.name != name) {
changed.push(ChangedField {
name: name.to_owned(),
label: Text::field(name),
before,
after,
});
}
}
changed
}
fn changed_records<R: Resource>(
cx: &Cx,
change: &Change<R::Model>,
key: impl Fn(&R::Model) -> String,
) -> Change<ChangedRecord> {
let named = |record: &R::Model, fields| ChangedRecord {
key: key(record),
url: crate::url::record::<R>(cx, record),
fields,
};
let held = |records: &[R::Model]| {
records
.iter()
.map(|record| named(record, changed_fields(cx, Some(record), None)))
.collect()
};
match change {
Change::Create(record) => Change::Create(named(
record,
changed_fields::<R::Model>(cx, None, Some(record)),
)),
Change::Update { before, after } => Change::Update {
before: named(before, Vec::new()),
after: named(after, changed_fields(cx, Some(before), Some(after))),
},
Change::Delete(records) => Change::Delete(held(records)),
Change::ForceDelete(records) => Change::ForceDelete(held(records)),
other => other.map(|record| named(record, Vec::new())),
}
}
pub(crate) type CommitHookFn =
for<'a> fn(&'a Cx, &'a PanelChange, &'a mut dyn Executor) -> BoxFuture<'a, Result<()>>;
fn erased<'a, H: CommitHook>(
cx: &'a Cx,
change: &'a PanelChange,
ex: &'a mut dyn Executor,
) -> BoxFuture<'a, Result<()>> {
Box::pin(H::before_commit(cx, change, ex))
}
impl Panel {
pub fn before_commit<H: CommitHook>(mut self) -> Self {
self.commit_hooks.push(erased::<H>);
self
}
}
pub(crate) async fn before_commit<R: Resource>(
cx: &Cx,
change: &Change<R::Model>,
ex: &mut dyn Executor,
) -> Result<()> {
R::before_commit(cx, change, &mut *ex).await?;
let hooks = current(cx).map_or(&[][..], |panel| panel.commit_hooks.as_slice());
if hooks.is_empty() {
return Ok(());
}
let mounted = require_mounted::<R>(cx)?;
let change = PanelChange {
slug: mounted.slug.clone(),
label: mounted.label.clone(),
change: changed_records::<R>(cx, change, |record| mounted.table.key_of(record)),
};
run(cx, hooks, &change, ex).await
}
fn run<'a>(
cx: &'a Cx,
hooks: &'a [CommitHookFn],
change: &'a PanelChange,
ex: &'a mut dyn Executor,
) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
for hook in hooks {
hook(cx, change, &mut *ex).await?;
}
Ok(())
})
}
#[cfg(test)]
mod tests;