Skip to main content

tablo_core/resource/
write_error.rs

1//! [`WriteError`]: why a record fn or an action's run wrote nothing.
2
3use crate::form::FieldErrors;
4
5/// Why a record fn or an action's run wrote nothing: a refusal the form renders, or a failure.
6///
7/// Either way the handler rolls the transaction back, so whatever the fn wrote before it returned
8/// is undone.
9///
10/// - [`Refused`](Self::Refused) renders the form again with status 200, each error under the
11///   control of the field it names, as a [`validate_record`](crate::Resource::validate_record)
12///   error does. A record fn keys its errors by the record form's field enum, an action's run by
13///   its input's form keys.
14/// - [`Failed`](Self::Failed) answers as a failed write does: a record fn's collision on a unique
15///   index renders under the field's control, a save another save overtook renders the stale form,
16///   and any other error flashes the failure toast and answers with the error.
17///
18/// A refusal is the answer to a rule that reads the database: a stock level, a quota, a
19/// uniqueness no index models. A rule the write itself enforces holds against concurrent writes:
20/// an update filtered on the stock it needs that matches no row refuses the very write it would
21/// have made.
22///
23/// `?` turns any error a [`topcoat::Error`] takes into [`Failed`](Self::Failed), and a
24/// [`FieldErrors`] into [`Refused`](Self::Refused):
25///
26/// ```rust
27/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
28/// # struct Order { #[key] #[auto] id: uuid::Uuid, sku: String, quantity: i64 }
29/// # #[derive(Debug, toasty::Model)]
30/// # struct Stock { #[key] sku: String, level: i64 }
31/// # #[derive(Debug, Clone, tablo_core::RecordForm)]
32/// # #[form(model = Order)]
33/// # struct OrderForm { sku: String, quantity: i64 }
34/// # struct OrderResource;
35/// use tablo_core::{FieldErrors, RecordForm, Resource, WriteError, write_create};
36/// use topcoat::context::Cx;
37///
38/// impl Resource for OrderResource {
39///     type Model = Order;
40///     type Form = OrderForm;
41///
42///     async fn create_record(
43///         cx: &Cx,
44///         form: OrderForm,
45///         ex: &mut dyn toasty::Executor,
46///     ) -> Result<Order, WriteError<OrderFormField>> {
47///         let stock = Stock::filter_by_sku(&form.sku)
48///             .first()
49///             .exec(&mut *ex)
50///             .await?;
51///         if stock.is_none_or(|stock| stock.level < form.quantity) {
52///             let mut errors = FieldErrors::new();
53///             errors.add(OrderFormField::Quantity, "Not enough in stock");
54///             return Err(errors.into());
55///         }
56///         Ok(write_create::<Self>(cx, form, ex).await?)
57///     }
58/// }
59/// ```
60#[derive(Debug)]
61pub enum WriteError<K = String> {
62    /// The submission cannot stand: render the form again with these errors.
63    Refused(FieldErrors<K>),
64    /// The write failed: answer with this error and the failure toast.
65    Failed(topcoat::Error),
66}
67
68impl<K> WriteError<K> {
69    /// Refuse the submission with `message` under `key`.
70    pub fn field(key: impl Into<K>, message: impl Into<crate::Text>) -> Self {
71        let mut errors = FieldErrors::new();
72        errors.add(key, message);
73        Self::Refused(errors)
74    }
75}
76
77impl<K> From<FieldErrors<K>> for WriteError<K> {
78    fn from(errors: FieldErrors<K>) -> Self {
79        Self::Refused(errors)
80    }
81}
82
83impl<K, E: Into<topcoat::Error>> From<E> for WriteError<K> {
84    fn from(error: E) -> Self {
85        Self::Failed(error.into())
86    }
87}