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