1use std::borrow::Cow;
2use std::sync::Arc;
3
4use serde_json::Value;
5use sofie_core::{Error, FieldError, Instant, Result, Uuid};
6
7use crate::heard::Known;
8use crate::messages::queue_steps;
9use crate::{Dialect, GuardCheck, MessageKind, RaisedEvent, Reach, Reply, Step, UniqueViolation};
10
11pub struct Plan<'d> {
12 dialect: &'d dyn Dialect,
13 reach: Reach,
14 stamp: Stamp,
15 steps: Vec<Step>,
16 leading: usize,
17 guards: Vec<Step>,
18 guard_checks: Vec<Arc<GuardCheck>>,
19 pub(crate) events: Vec<RaisedEvent>,
20 pub(crate) unique_fields: Vec<UniqueField>,
21 queues_messages: bool,
22 pub(crate) heard: Option<Arc<Known>>,
23 pub(crate) announcements: crate::commit::Announcements,
24 #[cfg(feature = "module-roles")]
25 role: Option<&'static str>,
26 #[cfg(feature = "module-roles")]
27 role_switches: usize,
28 #[cfg(feature = "search-engine")]
29 pub(crate) indexes_searches: bool,
30}
31
32pub(crate) struct Planned {
33 pub(crate) steps: Vec<Step>,
34 pub(crate) statements: usize,
35 pub(crate) guard_checks: Vec<Arc<GuardCheck>>,
36 pub(crate) unique_fields: Vec<UniqueField>,
37 pub(crate) queues_messages: bool,
38 pub(crate) announcements: crate::commit::Announcements,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct Stamp {
43 pub now: Instant,
44 pub actor: Option<Uuid>,
45 pub action: Option<&'static str>,
46}
47
48const UNIQUE_KEY: &str = "validation.unique";
49#[cfg(feature = "module-roles")]
50pub(crate) const FRAMEWORK_MODULE: &str = "sofie";
51const TAKEN: &str = "is already taken";
52
53pub(crate) struct UniqueField {
54 index: Cow<'static, str>,
55 module: Option<&'static str>,
57 field: &'static str,
58 message: &'static str,
59}
60
61impl<'d> Plan<'d> {
62 pub(crate) fn new(
63 dialect: &'d dyn Dialect,
64 reach: Reach,
65 now: Instant,
66 steps: Vec<Step>,
67 heard: Option<Arc<Known>>,
68 ) -> Self {
69 Self {
70 dialect,
71 reach,
72 stamp: Stamp {
73 now,
74 actor: reach.actor(),
75 action: crate::action::current(),
76 },
77 leading: steps.len(),
78 steps,
79 guards: Vec::new(),
80 guard_checks: Vec::new(),
81 events: Vec::new(),
82 unique_fields: Vec::new(),
83 queues_messages: false,
84 heard,
85 announcements: crate::commit::Announcements::default(),
86 #[cfg(feature = "module-roles")]
87 role: None,
88 #[cfg(feature = "module-roles")]
89 role_switches: 0,
90 #[cfg(feature = "search-engine")]
91 indexes_searches: false,
92 }
93 }
94
95 #[cfg(feature = "module-roles")]
97 pub(crate) fn act_as(&mut self, module: &'static str) {
98 let role = (module != FRAMEWORK_MODULE).then_some(module);
99 if role == self.role {
100 return;
101 }
102 if let Some(statement) = self.dialect.enter_role(role) {
103 self.steps.push(Step::Script(statement));
104 self.role_switches += 1;
105 }
106 self.role = role;
107 }
108
109 #[must_use]
110 pub fn dialect(&self) -> &'d dyn Dialect {
111 self.dialect
112 }
113
114 #[must_use]
115 pub fn reach(&self) -> Reach {
116 self.reach
117 }
118
119 #[must_use]
120 pub fn stamp(&self) -> Stamp {
121 self.stamp
122 }
123
124 pub fn push(&mut self, step: Step) {
126 self.steps.push(step);
127 }
128
129 pub fn push_as<A: crate::Aggregate>(&mut self, step: Step) {
131 #[cfg(feature = "module-roles")]
132 self.act_as(A::MODULE);
133 self.steps.push(step);
134 }
135
136 pub(crate) fn guard(&mut self, steps: Vec<Step>, check: Arc<GuardCheck>) {
137 self.guards.extend(steps);
138 self.guard_checks.push(check);
139 }
140
141 pub fn raise(&mut self, events: &[RaisedEvent]) {
142 self.events.extend_from_slice(events);
143 }
144
145 pub(crate) fn queue<'a>(
146 &mut self,
147 kind: MessageKind,
148 messages: impl IntoIterator<Item = (&'a str, Value)>,
149 ) -> Result<()> {
150 self.queue_at(kind, messages, self.stamp.now.timestamp())
151 }
152
153 pub(crate) fn queue_at<'a>(
154 &mut self,
155 kind: MessageKind,
156 messages: impl IntoIterator<Item = (&'a str, Value)>,
157 at: sofie_core::Timestamp,
158 ) -> Result<()> {
159 let heard = self.heard.as_deref();
160 let queuing = self.stamp.queuing(kind, at);
161 self.queues_messages |=
162 queue_steps(self.dialect, &queuing, messages, heard, &mut self.steps)?;
163 Ok(())
164 }
165
166 pub(crate) fn push_message(&mut self, step: Step) {
167 self.queues_messages = true;
168 self.steps.push(step);
169 }
170
171 #[cfg_attr(not(feature = "live"), expect(unused_variables))]
173 pub fn touched<A: crate::Aggregate>(&mut self, owner: Option<Uuid>) {
174 #[cfg(feature = "cache")]
175 self.announcements.changed(crate::ChangedRecord {
176 module: A::MODULE,
177 aggregate: A::NAME,
178 id: None,
179 });
180 #[cfg(feature = "live")]
181 self.announcements
182 .live_change(crate::TableChange::of::<A>(owner, self.reach));
183 }
184
185 pub fn unique(&mut self, index: &'static str, field: &'static str, message: &'static str) {
187 self.unique_fields.push(UniqueField {
188 index: Cow::Borrowed(index),
189 module: None,
190 field,
191 message,
192 });
193 }
194
195 pub(crate) fn into_parts(mut self) -> Result<Planned> {
196 let events = std::mem::take(&mut self.events);
197 #[cfg(feature = "module-roles")]
199 if !events.is_empty() {
200 self.act_as(FRAMEWORK_MODULE);
201 }
202 let named = events.into_iter().map(|event| (event.name, event.payload));
203 self.queue(MessageKind::Event, named)?;
204 let mut steps = self.steps;
205 let writes = steps.split_off(self.leading);
206 steps.extend(self.guards);
207 steps.extend(writes);
208 #[cfg(not(feature = "module-roles"))]
209 let statements = steps.len();
210 #[cfg(feature = "module-roles")]
211 let statements = steps.len() - self.role_switches;
212 Ok(Planned {
213 steps,
214 statements,
215 guard_checks: self.guard_checks,
216 unique_fields: self.unique_fields,
217 queues_messages: self.queues_messages,
218 announcements: self.announcements,
219 })
220 }
221}
222
223impl UniqueField {
224 pub(crate) fn column(module: &'static str, index: String, field: &'static str) -> Self {
226 Self {
227 index: Cow::Owned(index),
228 module: Some(module),
229 field,
230 message: TAKEN,
231 }
232 }
233}
234
235pub(crate) fn explain_duplicate(error: Error, unique_fields: &[UniqueField]) -> Error {
236 let Error::Internal {
237 source: Some(source),
238 ..
239 } = &error
240 else {
241 return error;
242 };
243 let Some(violation) = source.downcast_ref::<UniqueViolation>() else {
244 return error;
245 };
246 let index = violation.index.as_str();
247 let index = index.strip_suffix("_key").unwrap_or(index);
248 let prefixed = |unique: &&UniqueField| match unique.module {
250 Some(module) => {
251 index
252 .strip_prefix(module)
253 .and_then(|rest| rest.strip_prefix('_'))
254 == Some(&*unique.index)
255 }
256 None => index.ends_with(&format!("_{}", unique.index)),
257 };
258 let exact = unique_fields.iter().find(|unique| index == unique.index);
259 match exact.or_else(|| unique_fields.iter().find(prefixed)) {
260 Some(unique) => Error::Invalid(vec![FieldError::new(
261 unique.field,
262 UNIQUE_KEY,
263 unique.message,
264 )]),
265 None => error,
266 }
267}
268
269pub trait Outcome: Send {
270 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
271 let _ = plan;
272 Ok(())
273 }
274
275 fn committed(&mut self, stamp: Stamp) {
276 let _ = stamp;
277 }
278
279 fn reply(self) -> Result<Reply>
281 where
282 Self: Sized,
283 {
284 Ok(Reply::Empty)
285 }
286}
287
288#[diagnostic::on_unimplemented(
290 message = "a GET, screen or island handler cannot return `{Self}`: a GET never writes",
291 label = "a writable outcome",
292 note = "return a read model, `Paginated`, `Screen` or another reply; a write belongs in a POST, or in a job a domain event triggers"
293)]
294pub trait ReadOnly: Outcome {}
295
296impl Outcome for () {}
297
298impl ReadOnly for () {}
299
300impl<T: ReadOnly> ReadOnly for Vec<T> {}
301
302impl Outcome for serde_json::Value {
303 fn reply(self) -> Result<Reply> {
304 Ok(Reply::Json(self))
305 }
306}
307
308impl ReadOnly for serde_json::Value {}
309
310impl<T: Outcome> Outcome for Vec<T> {
311 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
312 self.iter().try_for_each(|outcome| outcome.plan(plan))
313 }
314
315 fn committed(&mut self, stamp: Stamp) {
316 for outcome in self {
317 outcome.committed(stamp);
318 }
319 }
320
321 fn reply(self) -> Result<Reply> {
322 self.into_iter()
323 .map(Outcome::reply)
324 .collect::<Result<_>>()
325 .map(Reply::List)
326 }
327}
328
329impl<T: Outcome> Outcome for Option<T> {
330 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
331 self.as_ref().map_or(Ok(()), |outcome| outcome.plan(plan))
332 }
333
334 fn committed(&mut self, stamp: Stamp) {
335 if let Some(outcome) = self {
336 outcome.committed(stamp);
337 }
338 }
339
340 fn reply(self) -> Result<Reply> {
341 self.map_or(Ok(Reply::Empty), Outcome::reply)
342 }
343}
344
345macro_rules! tuple_outcome {
346 ($($name:ident),+) => {
347 #[allow(non_snake_case)]
348 impl<$($name: Outcome),+> Outcome for ($($name,)+) {
349 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
350 let ($($name,)+) = self;
351 $($name.plan(plan)?;)+
352 Ok(())
353 }
354
355 fn committed(&mut self, stamp: Stamp) {
356 let ($($name,)+) = self;
357 $($name.committed(stamp);)+
358 }
359
360 fn reply(self) -> Result<Reply> {
362 let ($($name,)+) = self;
363 $(
364 let reply = $name.reply()?;
365 if !matches!(reply, Reply::Empty) {
366 return Ok(reply);
367 }
368 )+
369 Ok(Reply::Empty)
370 }
371 }
372
373 impl<$($name: ReadOnly),+> ReadOnly for ($($name,)+) {}
374 };
375}
376
377tuple_outcome!(A, B);
378tuple_outcome!(A, B, C);
379tuple_outcome!(A, B, C, D);
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 fn email_field() -> Vec<UniqueField> {
386 vec![UniqueField {
387 index: Cow::Borrowed("account_email"),
388 module: Some("shop"),
389 field: "email",
390 message: "is already registered",
391 }]
392 }
393
394 fn violation(index: &str) -> Error {
395 Error::internal(
396 "running an insert",
397 UniqueViolation {
398 index: index.to_owned(),
399 },
400 )
401 }
402
403 #[test]
404 fn an_absent_outcome_replies_empty_and_a_present_one_replies_its_own() {
405 let absent: Option<Vec<()>> = None;
406
407 assert!(matches!(absent.reply().unwrap(), Reply::Empty));
408 assert!(matches!(
409 Some(vec![()]).reply().unwrap(),
410 Reply::List(items) if matches!(items[..], [Reply::Empty])
411 ));
412 }
413
414 struct Saved;
415
416 impl Outcome for Saved {
417 fn reply(self) -> Result<Reply> {
418 Ok(Reply::Json(serde_json::json!({ "id": "1" })))
419 }
420 }
421
422 #[test]
423 fn a_tuple_replies_its_first_part_that_answers_anything() {
424 let reply = ((), Saved, Saved).reply().unwrap();
425
426 assert!(
427 matches!(&reply, Reply::Json(value) if value["id"] == "1"),
428 "{:?}",
429 reply.to_json()
430 );
431 assert!(matches!(((), ()).reply().unwrap(), Reply::Empty));
432 }
433
434 #[test]
435 fn a_violation_named_after_the_module_s_table_is_invalid_on_its_field() {
436 let error = explain_duplicate(violation("shop_account_email"), &email_field());
437
438 assert!(matches!(&error, Error::Invalid(fields) if fields[0].field == "email"));
439 }
440
441 #[test]
442 fn a_violation_on_a_table_whose_name_ends_like_another_s_is_not_put_on_its_field() {
443 let error = explain_duplicate(violation("billing_account_email_key"), &email_field());
444
445 assert!(matches!(error, Error::Internal { .. }), "{error}");
446 }
447
448 #[test]
449 fn a_violation_of_a_named_index_is_invalid_on_its_field() {
450 let error = explain_duplicate(violation("account_email"), &email_field());
451
452 assert!(
453 matches!(&error, Error::Invalid(fields) if fields[0].field == "email"
454 && fields[0].key == "validation.unique"
455 && fields[0].message == "is already registered"),
456 "{error}"
457 );
458 }
459
460 #[test]
461 fn any_other_failure_is_left_as_it_was() {
462 let unnamed = explain_duplicate(violation("other_index"), &email_field());
463 let plain = explain_duplicate(Error::internal_message("lost"), &email_field());
464
465 assert!(matches!(unnamed, Error::Internal { .. }), "{unnamed}");
466 assert!(matches!(plain, Error::Internal { .. }), "{plain}");
467 }
468}