1use std::future::Future;
5
6use toasty::Executor;
7use topcoat::{Result, context::Cx};
8
9use super::{Panel, state::current};
10use crate::{
11 BoxFuture, ModelColumns, Text,
12 resource::{Change, Resource, require_mounted},
13};
14
15pub trait CommitHook: 'static {
48 fn before_commit(
51 cx: &Cx,
52 change: &PanelChange,
53 ex: &mut dyn Executor,
54 ) -> impl Future<Output = Result<()>> + Send;
55}
56
57#[derive(Debug, Clone, PartialEq)]
60pub struct PanelChange {
61 slug: String,
62 label: Text,
63 change: Change<ChangedRecord>,
64}
65
66impl PanelChange {
67 pub fn slug(&self) -> &str {
69 &self.slug
70 }
71
72 pub fn label(&self) -> &Text {
74 &self.label
75 }
76
77 pub fn change(&self) -> &Change<ChangedRecord> {
79 &self.change
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct ChangedRecord {
86 key: String,
87 url: Option<String>,
88 fields: Vec<ChangedField>,
89}
90
91impl ChangedRecord {
92 pub fn key(&self) -> &str {
94 &self.key
95 }
96
97 pub fn url(&self) -> Option<&str> {
100 self.url.as_deref()
101 }
102
103 pub fn fields(&self) -> &[ChangedField] {
114 &self.fields
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct ChangedField {
122 name: String,
123 label: Text,
124 before: Option<String>,
125 after: Option<String>,
126}
127
128impl ChangedField {
129 pub fn name(&self) -> &str {
131 &self.name
132 }
133
134 pub fn label(&self) -> &Text {
136 &self.label
137 }
138
139 pub fn before(&self) -> Option<&str> {
141 self.before.as_deref()
142 }
143
144 pub fn after(&self) -> Option<&str> {
146 self.after.as_deref()
147 }
148}
149
150fn changed_fields<M: ModelColumns>(
153 cx: &Cx,
154 before: Option<&M>,
155 after: Option<&M>,
156) -> Vec<ChangedField> {
157 let table = M::table();
158 let (before_record, after_record) = (before, after);
159 let spell = |record: Option<&M>| record.map(|record| table.spell(cx, record));
160 let (before, after) = (spell(before), spell(after));
161 let columns = after.as_ref().or(before.as_ref()).map_or(0, Vec::len);
162 let held = |spelled: &Option<Vec<(String, Text, String)>>, index: usize| {
163 spelled
164 .as_ref()
165 .map(|spelled| spelled[index].2.clone())
166 .filter(|text| !text.is_empty())
167 };
168 let mut changed: Vec<ChangedField> = (0..columns)
169 .filter_map(|index| {
170 let (was, is) = (held(&before, index), held(&after, index));
171 let (name, label, _) = after.as_ref().or(before.as_ref())?[index].clone();
172 (was != is).then_some(ChangedField {
173 name,
174 label,
175 before: was,
176 after: is,
177 })
178 })
179 .collect();
180 let references = |record: Option<&M>| record.map(ModelColumns::references);
181 let (was, is) = (references(before_record), references(after_record));
182 let names = is.as_ref().or(was.as_ref()).cloned().unwrap_or_default();
183 for (index, (name, _)) in names.into_iter().enumerate() {
184 let held = |spelled: &Option<Vec<(&str, Option<String>)>>| {
185 spelled
186 .as_ref()
187 .and_then(|spelled| spelled[index].1.clone())
188 };
189 let (before, after) = (held(&was), held(&is));
190 if before != after && changed.iter().all(|field| field.name != name) {
191 changed.push(ChangedField {
192 name: name.to_owned(),
193 label: Text::field(name),
194 before,
195 after,
196 });
197 }
198 }
199 changed
200}
201
202fn changed_records<R: Resource>(
204 cx: &Cx,
205 change: &Change<R::Model>,
206 key: impl Fn(&R::Model) -> String,
207) -> Change<ChangedRecord> {
208 let named = |record: &R::Model, fields| ChangedRecord {
209 key: key(record),
210 url: crate::url::record::<R>(cx, record),
211 fields,
212 };
213 let held = |records: &[R::Model]| {
214 records
215 .iter()
216 .map(|record| named(record, changed_fields(cx, Some(record), None)))
217 .collect()
218 };
219 match change {
220 Change::Create(record) => Change::Create(named(
221 record,
222 changed_fields::<R::Model>(cx, None, Some(record)),
223 )),
224 Change::Update { before, after } => Change::Update {
225 before: named(before, Vec::new()),
226 after: named(after, changed_fields(cx, Some(before), Some(after))),
227 },
228 Change::Delete(records) => Change::Delete(held(records)),
229 Change::ForceDelete(records) => Change::ForceDelete(held(records)),
230 other => other.map(|record| named(record, Vec::new())),
231 }
232}
233
234pub(crate) type CommitHookFn =
236 for<'a> fn(&'a Cx, &'a PanelChange, &'a mut dyn Executor) -> BoxFuture<'a, Result<()>>;
237
238fn erased<'a, H: CommitHook>(
239 cx: &'a Cx,
240 change: &'a PanelChange,
241 ex: &'a mut dyn Executor,
242) -> BoxFuture<'a, Result<()>> {
243 Box::pin(H::before_commit(cx, change, ex))
244}
245
246impl Panel {
247 pub fn before_commit<H: CommitHook>(mut self) -> Self {
251 self.commit_hooks.push(erased::<H>);
252 self
253 }
254}
255
256pub(crate) async fn before_commit<R: Resource>(
259 cx: &Cx,
260 change: &Change<R::Model>,
261 ex: &mut dyn Executor,
262) -> Result<()> {
263 R::before_commit(cx, change, &mut *ex).await?;
264 let hooks = current(cx).map_or(&[][..], |panel| panel.commit_hooks.as_slice());
265 if hooks.is_empty() {
266 return Ok(());
267 }
268 let mounted = require_mounted::<R>(cx)?;
269 let change = PanelChange {
270 slug: mounted.slug.clone(),
271 label: mounted.label.clone(),
272 change: changed_records::<R>(cx, change, |record| mounted.table.key_of(record)),
273 };
274 run(cx, hooks, &change, ex).await
275}
276
277fn run<'a>(
279 cx: &'a Cx,
280 hooks: &'a [CommitHookFn],
281 change: &'a PanelChange,
282 ex: &'a mut dyn Executor,
283) -> BoxFuture<'a, Result<()>> {
284 Box::pin(async move {
285 for hook in hooks {
286 hook(cx, change, &mut *ex).await?;
287 }
288 Ok(())
289 })
290}
291
292#[cfg(test)]
293mod tests;