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<T: Outcome> Outcome for Vec<T> {
303 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
304 self.iter().try_for_each(|outcome| outcome.plan(plan))
305 }
306
307 fn committed(&mut self, stamp: Stamp) {
308 for outcome in self {
309 outcome.committed(stamp);
310 }
311 }
312
313 fn reply(self) -> Result<Reply> {
314 self.into_iter()
315 .map(Outcome::reply)
316 .collect::<Result<_>>()
317 .map(Reply::List)
318 }
319}
320
321impl<T: Outcome> Outcome for Option<T> {
322 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
323 self.as_ref().map_or(Ok(()), |outcome| outcome.plan(plan))
324 }
325
326 fn committed(&mut self, stamp: Stamp) {
327 if let Some(outcome) = self {
328 outcome.committed(stamp);
329 }
330 }
331
332 fn reply(self) -> Result<Reply> {
333 self.map_or(Ok(Reply::Empty), Outcome::reply)
334 }
335}
336
337macro_rules! tuple_outcome {
338 ($($name:ident),+) => {
339 #[allow(non_snake_case)]
340 impl<$($name: Outcome),+> Outcome for ($($name,)+) {
341 fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
342 let ($($name,)+) = self;
343 $($name.plan(plan)?;)+
344 Ok(())
345 }
346
347 fn committed(&mut self, stamp: Stamp) {
348 let ($($name,)+) = self;
349 $($name.committed(stamp);)+
350 }
351
352 fn reply(self) -> Result<Reply> {
354 let ($($name,)+) = self;
355 $(
356 let reply = $name.reply()?;
357 if !matches!(reply, Reply::Empty) {
358 return Ok(reply);
359 }
360 )+
361 Ok(Reply::Empty)
362 }
363 }
364
365 impl<$($name: ReadOnly),+> ReadOnly for ($($name,)+) {}
366 };
367}
368
369tuple_outcome!(A, B);
370tuple_outcome!(A, B, C);
371tuple_outcome!(A, B, C, D);
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 fn email_field() -> Vec<UniqueField> {
378 vec![UniqueField {
379 index: Cow::Borrowed("account_email"),
380 module: Some("shop"),
381 field: "email",
382 message: "is already registered",
383 }]
384 }
385
386 fn violation(index: &str) -> Error {
387 Error::internal(
388 "running an insert",
389 UniqueViolation {
390 index: index.to_owned(),
391 },
392 )
393 }
394
395 #[test]
396 fn an_absent_outcome_replies_empty_and_a_present_one_replies_its_own() {
397 let absent: Option<Vec<()>> = None;
398
399 assert!(matches!(absent.reply().unwrap(), Reply::Empty));
400 assert!(matches!(
401 Some(vec![()]).reply().unwrap(),
402 Reply::List(items) if matches!(items[..], [Reply::Empty])
403 ));
404 }
405
406 struct Saved;
407
408 impl Outcome for Saved {
409 fn reply(self) -> Result<Reply> {
410 Ok(Reply::Json(serde_json::json!({ "id": "1" })))
411 }
412 }
413
414 #[test]
415 fn a_tuple_replies_its_first_part_that_answers_anything() {
416 let reply = ((), Saved, Saved).reply().unwrap();
417
418 assert!(
419 matches!(&reply, Reply::Json(value) if value["id"] == "1"),
420 "{:?}",
421 reply.to_json()
422 );
423 assert!(matches!(((), ()).reply().unwrap(), Reply::Empty));
424 }
425
426 #[test]
427 fn a_violation_named_after_the_module_s_table_is_invalid_on_its_field() {
428 let error = explain_duplicate(violation("shop_account_email"), &email_field());
429
430 assert!(matches!(&error, Error::Invalid(fields) if fields[0].field == "email"));
431 }
432
433 #[test]
434 fn a_violation_on_a_table_whose_name_ends_like_another_s_is_not_put_on_its_field() {
435 let error = explain_duplicate(violation("billing_account_email_key"), &email_field());
436
437 assert!(matches!(error, Error::Internal { .. }), "{error}");
438 }
439
440 #[test]
441 fn a_violation_of_a_named_index_is_invalid_on_its_field() {
442 let error = explain_duplicate(violation("account_email"), &email_field());
443
444 assert!(
445 matches!(&error, Error::Invalid(fields) if fields[0].field == "email"
446 && fields[0].key == "validation.unique"
447 && fields[0].message == "is already registered"),
448 "{error}"
449 );
450 }
451
452 #[test]
453 fn any_other_failure_is_left_as_it_was() {
454 let unnamed = explain_duplicate(violation("other_index"), &email_field());
455 let plain = explain_duplicate(Error::internal_message("lost"), &email_field());
456
457 assert!(matches!(unnamed, Error::Internal { .. }), "{unnamed}");
458 assert!(matches!(plain, Error::Internal { .. }), "{plain}");
459 }
460}