Skip to main content

autumn_web/
hooks.rs

1//! Mutation hook types for repository lifecycle callbacks.
2//!
3//! This module provides the types and trait that power before/after mutation
4//! hooks on generated repositories (create, update, delete).
5//!
6//! # Opting in
7//!
8//! By default, repositories generated by `#[repository(Model)]` perform
9//! plain CRUD with no hook overhead. To enable hooks, pass a hooks type:
10//!
11//! ```rust,ignore
12//! #[derive(Clone, Default)]
13//! struct ArticleHooks;
14//!
15//! impl MutationHooks for ArticleHooks {
16//!     type Model = Article;
17//!     type NewModel = NewArticle;
18//!     type UpdateModel = UpdateArticle;
19//!
20//!     // override only the callbacks you need …
21//! }
22//!
23//! #[repository(Article, hooks = ArticleHooks)]
24//! pub trait ArticleRepository {}
25//! ```
26//!
27//! The durable `after_*_commit` hooks are opt-in because they require Autumn's
28//! framework-owned commit-hook queue table. Enable them explicitly with
29//! `#[repository(Article, hooks = ArticleHooks, commit_hooks = true)]`.
30//!
31//! The hooks type **must** implement [`Default`] and [`Clone`] (the
32//! generated extractor constructs it via `Default::default()` and the
33//! repository struct derives `Clone`).
34//!
35//! # Lifecycle
36//!
37//! Every mutating CRUD operation follows the same lifecycle:
38//!
39//! 1. **`before_*`** -- called before the mutation with a mutable
40//!    reference to the [`MutationContext`] and the input/model. The hook
41//!    may validate, enrich, or reject (by returning `Err`).
42//! 2. **persist** -- the actual `INSERT`, `UPDATE`, or `DELETE` runs.
43//!
44//! # Error semantics
45//!
46//! - `before_*` errors prevent the mutation entirely.
47//!
48//! # Helper types
49//!
50//! - [`Patch<T>`] -- tri-state value for partial-update (PATCH) payloads.
51//! - [`FieldDiff<T>`] -- per-field before/after diff for inspecting and
52//!   overriding changes inside hooks.
53//! - [`MutationOp`] -- discriminant for create / update / delete.
54//! - [`MutationContext`] -- carries actor identity, request ID, and
55//!   timestamp into every hook invocation.
56
57use std::borrow::Cow;
58
59use serde::de::Deserializer;
60use serde::{Deserialize, Serialize};
61
62// ── Mutation operation & context ─────────────────────────────────────
63
64/// The kind of mutation being performed on a repository record.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66#[non_exhaustive]
67pub enum MutationOp {
68    /// A new record is being created.
69    Create,
70    /// An existing record is being updated.
71    Update,
72    /// An existing record is being deleted.
73    Delete,
74}
75
76impl MutationOp {
77    /// Returns the operation name as a static string slice.
78    #[must_use]
79    pub const fn as_str(self) -> &'static str {
80        match self {
81            Self::Create => "create",
82            Self::Update => "update",
83            Self::Delete => "delete",
84        }
85    }
86}
87
88impl std::fmt::Display for MutationOp {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.write_str(self.as_str())
91    }
92}
93
94/// Context available to mutation hooks.
95///
96/// Carries actor identity, request metadata, and timestamps so that
97/// hook implementations can perform auditing, validation, or enrichment.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct MutationContext {
100    /// The mutation operation type.
101    pub op: MutationOp,
102    /// Actor identity (user ID or service name). `None` for anonymous.
103    pub actor: Option<String>,
104    /// Correlation / request ID for tracing.
105    pub request_id: Option<String>,
106    /// Timestamp of the mutation.
107    pub now: chrono::DateTime<chrono::Utc>,
108    /// Cache keys to invalidate after the mutation.
109    pub invalidate_keys: Vec<String>,
110    /// Framework-scoped idempotency key for this mutation, when the mutation
111    /// came from an idempotent HTTP request or a hook set one explicitly.
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub idempotency_key: Option<String>,
114}
115
116impl MutationContext {
117    /// Create a new context for the given operation.
118    ///
119    /// Auto-populates `now` with `Utc::now()`, `request_id` with a freshly
120    /// generated UUID v4, and `actor` from the ambient
121    /// [`Current::actor`](crate::current::Current::actor) (the authenticated
122    /// principal when inside a request; `None` otherwise).
123    #[must_use]
124    pub fn new(op: MutationOp) -> Self {
125        Self {
126            op,
127            // Seed the actor from the ambient request-scoped current actor so
128            // generated writes auto-attribute to the authenticated principal
129            // with zero per-call wiring. `None` outside any scope, preserving
130            // the previous behavior. A `before_*` hook still overrides this,
131            // since hooks run after construction.
132            actor: crate::current::Current::actor(),
133            request_id: Some(uuid::Uuid::new_v4().to_string()),
134            now: chrono::Utc::now(),
135            invalidate_keys: Vec::new(),
136            idempotency_key: None,
137        }
138    }
139
140    /// Add a cache key to the invalidation list.
141    pub fn invalidate(&mut self, key: impl Into<String>) {
142        self.invalidate_keys.push(key.into());
143    }
144
145    /// Set a scoped idempotency key for durable side-effect deduplication.
146    pub fn set_idempotency_key(&mut self, key: impl Into<String>) {
147        self.idempotency_key = Some(key.into());
148    }
149}
150
151// ── Mutation hooks trait ─────────────────────────────────────────────
152
153use crate::AutumnResult;
154use std::future::Future;
155
156/// Repository-scoped mutation lifecycle hooks.
157///
158/// All methods have default no-op implementations, so you only need to
159/// override the callbacks you care about. Each method receives a
160/// [`MutationContext`] (mutable for "before" hooks) and the relevant
161/// model/changeset.
162///
163/// # Associated types
164///
165/// - `Model` -- the read model returned by queries (e.g., `User`).
166/// - `NewModel` -- the insertable changeset (e.g., `NewUser`).
167/// - `UpdateModel` -- the partial-update changeset (e.g., `UpdateUser`).
168pub trait MutationHooks: Send + Sync + 'static {
169    /// The read model (e.g., the `Queryable` struct).
170    type Model: Send + Sync;
171    /// The insertable changeset for new records.
172    type NewModel: Send + Sync;
173    /// The partial-update changeset.
174    type UpdateModel: Send + Sync;
175
176    /// Called before a new record is inserted.
177    fn before_create(
178        &self,
179        _ctx: &mut MutationContext,
180        _new: &mut Self::NewModel,
181    ) -> impl Future<Output = AutumnResult<()>> + Send {
182        async { Ok(()) }
183    }
184
185    /// Called before an existing record is updated.
186    ///
187    /// The `draft` holds the merged before/after state. Use per-field
188    /// accessors (generated by `#[model]`) to inspect and rewrite:
189    ///
190    /// ```rust,ignore
191    /// if draft.status().changed_to(&Status::Approved) {
192    ///     draft.approved_at().set(Some(ctx.now));
193    /// }
194    /// ```
195    fn before_update(
196        &self,
197        _ctx: &mut MutationContext,
198        _draft: &mut UpdateDraft<Self::Model>,
199    ) -> impl Future<Output = AutumnResult<()>> + Send
200    where
201        Self::Model: Clone,
202    {
203        async { Ok(()) }
204    }
205
206    /// Called before an existing record is deleted.
207    fn before_delete(
208        &self,
209        _ctx: &mut MutationContext,
210        _record: &Self::Model,
211    ) -> impl Future<Output = AutumnResult<()>> + Send {
212        async { Ok(()) }
213    }
214
215    /// Called after a new record is inserted.
216    fn after_create(
217        &self,
218        _ctx: &mut MutationContext,
219        _record: &Self::Model,
220    ) -> impl Future<Output = AutumnResult<()>> + Send {
221        async { Ok(()) }
222    }
223
224    /// Called after an existing record is updated.
225    fn after_update(
226        &self,
227        _ctx: &mut MutationContext,
228        _record: &Self::Model,
229    ) -> impl Future<Output = AutumnResult<()>> + Send {
230        async { Ok(()) }
231    }
232
233    /// Called after a new record is inserted **and the transaction commits**.
234    ///
235    /// Unlike `after_create`, this hook fires only when the surrounding
236    /// database transaction has been durably committed. Use this variant for
237    /// side-effects (job enqueues, emails, cache invalidation) that must not
238    /// execute if the transaction rolls back.
239    ///
240    /// When the repository is declared with `commit_hooks = true`, generated
241    /// repository code writes this hook's intent to Autumn's framework-owned
242    /// durable commit-hook queue in the same transaction as the mutation.
243    /// Replicas claim queued hooks with Postgres row locks, so a process-local
244    /// task disappearing is recovered by retrying or dead-lettering the row
245    /// instead of silently losing the side effect.
246    fn after_create_commit(
247        &self,
248        _ctx: &mut MutationContext,
249        _record: &Self::Model,
250    ) -> impl Future<Output = AutumnResult<()>> + Send {
251        async { Ok(()) }
252    }
253
254    /// Called after an existing record is updated **and the transaction commits**.
255    ///
256    /// The same transactional guarantee as `after_create_commit` applies:
257    /// this hook is not called when the surrounding transaction rolls back.
258    fn after_update_commit(
259        &self,
260        _ctx: &mut MutationContext,
261        _record: &Self::Model,
262    ) -> impl Future<Output = AutumnResult<()>> + Send {
263        async { Ok(()) }
264    }
265
266    /// Called after a record is deleted **and the transaction commits**.
267    ///
268    /// The same transactional guarantee as `after_create_commit` applies:
269    /// this hook is not called when the surrounding transaction rolls back.
270    fn after_delete_commit(
271        &self,
272        _ctx: &mut MutationContext,
273        _record: &Self::Model,
274    ) -> impl Future<Output = AutumnResult<()>> + Send {
275        async { Ok(()) }
276    }
277}
278
279/// Stable hook-construction contract used by generated repositories.
280///
281/// This indirection keeps compile-time diagnostics anchored on Autumn-owned
282/// trait names instead of compiler-rendered `Default` wording, which is more
283/// brittle across platforms and environments.
284pub trait RepositoryHooksDefault: Sized {
285    /// Construct a hook instance for generated repository state.
286    fn autumn_default() -> Self;
287}
288
289impl<T> RepositoryHooksDefault for T
290where
291    T: Default,
292{
293    fn autumn_default() -> Self {
294        Self::default()
295    }
296}
297
298/// Stable hook-cloning contract used by generated repositories.
299///
300/// Generated repository implementations clone their hook state through this
301/// trait so compile-fail diagnostics stay deterministic across toolchains.
302pub trait RepositoryHooksClone: Sized {
303    /// Clone hook state for generated repository values.
304    #[must_use]
305    fn autumn_clone(&self) -> Self;
306}
307
308impl<T> RepositoryHooksClone for T
309where
310    T: Clone,
311{
312    fn autumn_clone(&self) -> Self {
313        self.clone()
314    }
315}
316
317// ── Default no-op hooks ──────────────────────────────────────────────
318
319/// Zero-cost no-op implementation of [`MutationHooks`].
320///
321/// Used by generated repository code when the user has not configured any
322/// hooks. All methods use the trait defaults (immediate `Ok(())`).
323pub struct NoHooks<M, N, U> {
324    _phantom: std::marker::PhantomData<(M, N, U)>,
325}
326
327impl<M, N, U> Default for NoHooks<M, N, U> {
328    fn default() -> Self {
329        Self {
330            _phantom: std::marker::PhantomData,
331        }
332    }
333}
334
335impl<M, N, U> MutationHooks for NoHooks<M, N, U>
336where
337    M: Send + Sync + Clone + 'static,
338    N: Send + Sync + 'static,
339    U: Send + Sync + 'static,
340{
341    type Model = M;
342    type NewModel = N;
343    type UpdateModel = U;
344}
345
346/// Tri-state sparse update value.
347///
348/// `Patch<T>` distinguishes between "field not mentioned" ([`Unchanged`](Patch::Unchanged)),
349/// "field explicitly set" ([`Set`](Patch::Set)), and "field explicitly cleared"
350/// ([`Clear`](Patch::Clear), mapping to SQL `NULL`).
351///
352/// This is the building block for partial-update (PATCH) payloads where
353/// omitting a field means "leave it alone" rather than "set it to its
354/// default".
355#[derive(Debug, Clone, Default, PartialEq, Eq)]
356pub enum Patch<T> {
357    /// The field was not included in the update payload.
358    #[default]
359    Unchanged,
360    /// The field was explicitly set to a new value.
361    Set(T),
362    /// The field was explicitly cleared (maps to SQL `NULL`).
363    Clear,
364}
365
366impl<T> Patch<T> {
367    /// Returns `true` if the field was not included in the update.
368    #[must_use]
369    pub const fn is_unchanged(&self) -> bool {
370        matches!(self, Self::Unchanged)
371    }
372
373    /// Returns `true` if the field was explicitly set to a new value.
374    #[must_use]
375    pub const fn is_set(&self) -> bool {
376        matches!(self, Self::Set(_))
377    }
378
379    /// Returns `true` if the field was explicitly cleared.
380    #[must_use]
381    pub const fn is_clear(&self) -> bool {
382        matches!(self, Self::Clear)
383    }
384
385    /// Returns a reference to the inner value if [`Set`](Patch::Set), or `None`.
386    #[must_use]
387    pub const fn as_set(&self) -> Option<&T> {
388        match self {
389            Self::Set(v) => Some(v),
390            _ => None,
391        }
392    }
393
394    /// Converts into a nested `Option`:
395    ///
396    /// - `Set(v)` -> `Some(Some(v))`
397    /// - `Clear` -> `Some(None)`
398    /// - `Unchanged` -> `None`
399    #[must_use]
400    pub fn into_option(self) -> Option<Option<T>> {
401        match self {
402            Self::Set(v) => Some(Some(v)),
403            Self::Clear => Some(None),
404            Self::Unchanged => None,
405        }
406    }
407}
408
409impl<T: Serialize> Serialize for Patch<T> {
410    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
411        match self {
412            Self::Unchanged | Self::Clear => serializer.serialize_none(),
413            Self::Set(v) => v.serialize(serializer),
414        }
415    }
416}
417
418impl<'de, T: Deserialize<'de>> Deserialize<'de> for Patch<T> {
419    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
420        // When this method is called, the field WAS present in the JSON.
421        // Absent fields use the Default impl (→ Unchanged) via #[serde(default)].
422        let opt: Option<T> = Option::deserialize(deserializer)?;
423        Ok(opt.map_or_else(|| Self::Clear, Self::Set))
424    }
425}
426
427// ── `Patch<T>` validator per-field trait impls (#1719) ───────────────
428//
429// The `#[repository(api = ...)]` macro generates an `UpdateModel` whose mutable
430// fields are `Patch<T>`. So that the model's declarative `#[validate(...)]`
431// rules are enforced on PATCH/PUT (returning a 422 with the same per-field
432// error map as create), the generated `UpdateModel` derives
433// `validator::Validate` and carries the field `#[validate]` attributes. That
434// derive expands to per-field calls like `self.title.validate_length(..)`,
435// so `Patch<T>` must implement validator's per-field traits.
436//
437// These impls mirror validator's own `Option<T>` impls: an absent field
438// (`Unchanged`/`Clear`) is skipped — it behaves like `None` and always passes —
439// while `Set(v)` delegates to the inner value's rule. `Patch` is a local type,
440// so implementing these foreign traits raises no orphan-rule issue.
441//
442// Only the *declarative, single-field* validators are implemented.
443// `must_match`/`nested`/`custom` are intentionally omitted: they are
444// meaningless or ill-typed on a tri-state patch field. `required` IS implemented
445// (below), but with distinct, non-skip semantics: unlike the other rules, an
446// absent field is not uniformly "pass" — `Clear` (explicit null on a required
447// field) must FAIL so a PATCH/PUT cannot violate the model's `required` contract
448// by nulling the column. `ValidateDoesNotContain`
449// is deliberately NOT implemented here — validator provides it automatically via
450// its blanket `impl<T: ValidateContains> ValidateDoesNotContain for T`, which
451// covers `Patch<T>` through the `ValidateContains` impl below (a manual impl
452// would collide with that blanket). `ValidateCreditCard` (validator's `card`
453// feature) and `ValidateNonControlCharacter` (its `unic` feature) are not
454// exported under this workspace's `validator` feature set, so they are omitted.
455
456impl<T: validator::ValidateLength<u64>> validator::ValidateLength<u64> for Patch<T> {
457    fn length(&self) -> Option<u64> {
458        match self {
459            Self::Set(v) => validator::ValidateLength::length(v),
460            _ => None,
461        }
462    }
463}
464
465impl<N, T: validator::ValidateRange<N>> validator::ValidateRange<N> for Patch<T> {
466    fn greater_than(&self, max: N) -> Option<bool> {
467        match self {
468            Self::Set(v) => validator::ValidateRange::greater_than(v, max),
469            _ => None,
470        }
471    }
472
473    fn less_than(&self, min: N) -> Option<bool> {
474        match self {
475            Self::Set(v) => validator::ValidateRange::less_than(v, min),
476            _ => None,
477        }
478    }
479}
480
481impl<T: validator::ValidateEmail> validator::ValidateEmail for Patch<T> {
482    fn as_email_string(&self) -> Option<Cow<'_, str>> {
483        match self {
484            Self::Set(v) => validator::ValidateEmail::as_email_string(v),
485            _ => None,
486        }
487    }
488}
489
490impl<T: validator::ValidateUrl> validator::ValidateUrl for Patch<T> {
491    fn as_url_string(&self) -> Option<Cow<'_, str>> {
492        match self {
493            Self::Set(v) => validator::ValidateUrl::as_url_string(v),
494            _ => None,
495        }
496    }
497}
498
499impl<T: validator::ValidateContains> validator::ValidateContains for Patch<T> {
500    fn validate_contains(&self, needle: &str) -> bool {
501        match self {
502            Self::Set(v) => validator::ValidateContains::validate_contains(v, needle),
503            _ => true,
504        }
505    }
506}
507
508impl<T: validator::ValidateIp> validator::ValidateIp for Patch<T> {
509    fn validate_ipv4(&self) -> bool {
510        match self {
511            Self::Set(v) => validator::ValidateIp::validate_ipv4(v),
512            _ => true,
513        }
514    }
515
516    fn validate_ipv6(&self) -> bool {
517        match self {
518            Self::Set(v) => validator::ValidateIp::validate_ipv6(v),
519            _ => true,
520        }
521    }
522
523    fn validate_ip(&self) -> bool {
524        match self {
525            Self::Set(v) => validator::ValidateIp::validate_ip(v),
526            _ => true,
527        }
528    }
529}
530
531impl<T: validator::ValidateRegex> validator::ValidateRegex for Patch<T> {
532    fn validate_regex(&self, regex: impl validator::AsRegex) -> bool {
533        match self {
534            Self::Set(v) => validator::ValidateRegex::validate_regex(v, regex),
535            _ => true,
536        }
537    }
538}
539
540// `required` has *tri-state* semantics that differ from the skip-on-absent rules
541// above (#1719 / Codex P2). `required` is meaningful only on `Option`-typed model
542// fields, whose `UpdateModel` field is `Patch<Option<T>>`, so the bound is
543// `T: ValidateRequired` (satisfied by validator's `impl<T> ValidateRequired for
544// Option<T>`). The derive calls `self.field.validate_required()`:
545//   - `Unchanged` -> `true`: the field was omitted from the patch, so the
546//     existing (non-null) value is kept — nothing to reject.
547//   - `Clear`     -> `false`: an explicit JSON `null` on a required field would
548//     write SQL `NULL`, violating the `required` contract — reject (422).
549//   - `Set(v)`    -> delegate to the inner `Option<T>`: `Set(Some)` passes,
550//     `Set(None)` fails (same as create).
551impl<T: validator::ValidateRequired> validator::ValidateRequired for Patch<T> {
552    fn validate_required(&self) -> bool {
553        match self {
554            Self::Unchanged => true,
555            Self::Clear => false,
556            Self::Set(v) => validator::ValidateRequired::validate_required(v),
557        }
558    }
559
560    fn is_some(&self) -> bool {
561        match self {
562            Self::Set(v) => validator::ValidateRequired::is_some(v),
563            _ => false,
564        }
565    }
566}
567
568/// Per-field before/after diff accessor for mutation hooks.
569///
570/// `FieldDiff<T>` holds the previous and proposed values for a single field,
571/// allowing hook authors to inspect what changed and optionally override the
572/// new value via [`set`](FieldDiff::set).
573#[derive(Debug, Clone, PartialEq, Eq)]
574pub struct FieldDiff<T> {
575    before: T,
576    after: T,
577}
578
579impl<T: PartialEq> FieldDiff<T> {
580    /// Create a new diff from before and after values.
581    #[must_use]
582    pub const fn new(before: T, after: T) -> Self {
583        Self { before, after }
584    }
585
586    /// Reference to the value before the mutation.
587    #[must_use]
588    pub const fn before(&self) -> &T {
589        &self.before
590    }
591
592    /// Reference to the (possibly overridden) value after the mutation.
593    #[must_use]
594    pub const fn after(&self) -> &T {
595        &self.after
596    }
597
598    /// Returns `true` if the field value changed.
599    #[must_use]
600    pub fn changed(&self) -> bool {
601        self.before != self.after
602    }
603
604    /// Returns `true` if the field value did not change.
605    #[must_use]
606    pub fn unchanged(&self) -> bool {
607        self.before == self.after
608    }
609
610    /// Returns `true` if the field changed **and** the new value equals `value`.
611    #[must_use]
612    pub fn changed_to(&self, value: &T) -> bool {
613        self.changed() && self.after == *value
614    }
615
616    /// Returns `true` if the field changed **and** the old value equals `value`.
617    #[must_use]
618    pub fn changed_from(&self, value: &T) -> bool {
619        self.changed() && self.before == *value
620    }
621
622    /// Override the after value. Does not affect `before`.
623    pub fn set(&mut self, value: T) {
624        self.after = value;
625    }
626}
627
628impl<T: PartialEq> FieldDiff<Option<T>> {
629    /// Returns `true` if the field went from `None` to `Some`.
630    #[must_use]
631    pub const fn was_set(&self) -> bool {
632        self.before.is_none() && self.after.is_some()
633    }
634
635    /// Returns `true` if the field went from `Some` to `None`.
636    #[must_use]
637    pub const fn was_cleared(&self) -> bool {
638        self.before.is_some() && self.after.is_none()
639    }
640}
641
642// ── UpdateDraft & DraftField ─────────────────────────────────────────
643
644/// Merged before/after snapshot of a model for `before_update` hooks.
645///
646/// `UpdateDraft<T>` holds the original (`before`) and proposed (`after`)
647/// state of a model. The `after` copy starts as a clone of `before` and
648/// can be mutated via [`after_mut`](UpdateDraft::after_mut) or through
649/// per-field [`DraftField`] accessors generated by `#[model]`.
650///
651/// # Usage
652///
653/// ```rust,ignore
654/// let mut draft = UpdateDraft::new(existing_article);
655/// // apply partial changes from the request …
656/// draft.after_mut().title = new_title;
657///
658/// // per-field inspection via DraftField:
659/// let title = DraftField::new(&draft.before().title, &mut draft.after_mut().title);
660/// if title.changed() { /* … */ }
661/// ```
662#[derive(Debug, Clone)]
663pub struct UpdateDraft<T: Clone> {
664    /// The original (pre-mutation) model state.
665    ///
666    /// Public so that `#[model]`-generated per-field `DraftField` accessors
667    /// can split-borrow `before` and `after` simultaneously.
668    pub before: T,
669    /// The proposed (post-mutation) model state.
670    ///
671    /// Public so that `#[model]`-generated per-field `DraftField` accessors
672    /// can split-borrow `before` and `after` simultaneously.
673    pub after: T,
674}
675
676impl<T: Clone> UpdateDraft<T> {
677    /// Create a new draft from the current model state.
678    ///
679    /// Clones `before` into `after` so that `after` starts as an
680    /// identical copy that can be selectively mutated.
681    #[must_use]
682    pub fn new(before: T) -> Self {
683        let after = before.clone();
684        Self { before, after }
685    }
686
687    /// Create a draft with explicit before and after values.
688    ///
689    /// Useful in tests or when changes have already been applied.
690    #[must_use]
691    pub const fn new_with_changes(before: T, after: T) -> Self {
692        Self { before, after }
693    }
694
695    /// Reference to the original (pre-mutation) model.
696    #[must_use]
697    pub const fn before(&self) -> &T {
698        &self.before
699    }
700
701    /// Reference to the proposed (post-mutation) model.
702    #[must_use]
703    pub const fn after(&self) -> &T {
704        &self.after
705    }
706
707    /// Mutable reference to the proposed (post-mutation) model.
708    ///
709    /// Use this to apply changes before the update is persisted.
710    #[must_use]
711    pub const fn after_mut(&mut self) -> &mut T {
712        &mut self.after
713    }
714
715    /// Consume the draft and return the proposed model.
716    #[must_use]
717    pub fn into_after(self) -> T {
718        self.after
719    }
720}
721
722/// Borrowing per-field accessor into an [`UpdateDraft`].
723///
724/// `DraftField` borrows `before` immutably and `after` mutably from the
725/// parent draft. Because each `DraftField` is a temporary that drops at
726/// statement end, you can inspect and mutate fields one at a time without
727/// running into overlapping borrow issues:
728///
729/// ```rust,ignore
730/// // generated by #[model]:
731/// draft.status().changed_to(&Status::Published);
732/// draft.title().set("New title".into());
733/// ```
734///
735/// # Lifetime
736///
737/// `'a` ties the field references back to the `UpdateDraft` they came
738/// from. The borrow is released when the `DraftField` is dropped.
739#[derive(Debug)]
740pub struct DraftField<'a, T> {
741    before: &'a T,
742    after: &'a mut T,
743}
744
745impl<'a, T> DraftField<'a, T> {
746    /// Create a new field accessor borrowing from a draft.
747    #[must_use]
748    pub const fn new(before: &'a T, after: &'a mut T) -> Self {
749        Self { before, after }
750    }
751
752    /// Reference to the original (pre-mutation) field value.
753    #[must_use]
754    pub const fn before(&self) -> &T {
755        self.before
756    }
757
758    /// Reference to the proposed (post-mutation) field value.
759    #[must_use]
760    pub const fn after(&self) -> &T {
761        self.after
762    }
763
764    /// Override the proposed value for this field.
765    pub fn set(&mut self, value: T) {
766        *self.after = value;
767    }
768}
769
770impl<T: PartialEq> DraftField<'_, T> {
771    /// Returns `true` if the field value changed.
772    #[must_use]
773    pub fn changed(&self) -> bool {
774        self.before != self.after
775    }
776
777    /// Returns `true` if the field value did not change.
778    #[must_use]
779    pub fn unchanged(&self) -> bool {
780        self.before == self.after
781    }
782
783    /// Returns `true` if the field changed **and** the new value equals `value`.
784    #[must_use]
785    pub fn changed_to(&self, value: &T) -> bool {
786        self.changed() && *self.after == *value
787    }
788
789    /// Returns `true` if the field changed **and** the old value equals `value`.
790    #[must_use]
791    pub fn changed_from(&self, value: &T) -> bool {
792        self.changed() && *self.before == *value
793    }
794}
795
796impl<T: PartialEq> DraftField<'_, Option<T>> {
797    /// Returns `true` if the field went from `None` to `Some`.
798    #[must_use]
799    pub const fn was_set(&self) -> bool {
800        self.before.is_none() && self.after.is_some()
801    }
802
803    /// Returns `true` if the field went from `Some` to `None`.
804    #[must_use]
805    pub const fn was_cleared(&self) -> bool {
806        self.before.is_some() && self.after.is_none()
807    }
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813
814    // ── Patch tests ──────────────────────────────────────────────
815
816    #[test]
817    fn patch_unchanged_is_default() {
818        let p: Patch<String> = Patch::default();
819        assert!(p.is_unchanged());
820        assert!(!p.is_set());
821        assert!(!p.is_clear());
822    }
823
824    #[test]
825    fn patch_set_holds_value() {
826        let p = Patch::Set("hello");
827        assert!(p.is_set());
828        assert!(!p.is_unchanged());
829        assert!(!p.is_clear());
830        assert_eq!(p.as_set(), Some(&"hello"));
831    }
832
833    #[test]
834    fn patch_clear_is_clear() {
835        let p: Patch<i32> = Patch::Clear;
836        assert!(p.is_clear());
837        assert!(!p.is_set());
838        assert!(!p.is_unchanged());
839    }
840
841    #[test]
842    fn patch_into_option_set() {
843        assert_eq!(Patch::Set(42).into_option(), Some(Some(42)));
844    }
845
846    #[test]
847    fn patch_into_option_clear() {
848        assert_eq!(Patch::<i32>::Clear.into_option(), Some(None));
849    }
850
851    #[test]
852    fn patch_into_option_unchanged() {
853        assert_eq!(Patch::<i32>::Unchanged.into_option(), None);
854    }
855
856    // ── Patch validator-trait tests (#1719) ─────────────────────
857    //
858    // `Patch<T>` mirrors validator's `Option<T>` impls: an absent field
859    // (`Unchanged`/`Clear`) always passes; a `Set(v)` field delegates to the
860    // inner value's rule. This lets the generated `UpdateModel` derive
861    // `validator::Validate` and enforce `#[validate(...)]` on PATCH/PUT.
862
863    #[test]
864    fn patch_validate_length_set_delegates_to_inner() {
865        use validator::ValidateLength;
866        // `Set` with an empty string fails a `length(min = 1)` rule …
867        assert!(!Patch::Set(String::new()).validate_length(Some(1), None, None));
868        // … while a `Set` with a satisfying value passes.
869        assert!(Patch::Set(String::from("ok")).validate_length(Some(1), None, None));
870    }
871
872    #[test]
873    fn patch_validate_length_absent_passes() {
874        use validator::ValidateLength;
875        // Absent variants behave like `None`: the rule is skipped (passes).
876        assert!(Patch::<String>::Unchanged.validate_length(Some(1), None, None));
877        assert!(Patch::<String>::Clear.validate_length(Some(1), None, None));
878    }
879
880    // `required` has tri-state semantics (#1719 / Codex P2): unlike the
881    // skip-on-absent rules, `Clear` and `Set(None)` must FAIL so a PATCH/PUT
882    // cannot null a `#[validate(required)]` column; `Unchanged` skips.
883    #[test]
884    fn patch_validate_required_unchanged_passes() {
885        use validator::ValidateRequired;
886        assert!(Patch::<Option<String>>::Unchanged.validate_required());
887    }
888
889    #[test]
890    fn patch_validate_required_clear_fails() {
891        use validator::ValidateRequired;
892        assert!(!Patch::<Option<String>>::Clear.validate_required());
893    }
894
895    #[test]
896    fn patch_validate_required_set_some_passes() {
897        use validator::ValidateRequired;
898        assert!(Patch::Set(Some(String::from("x"))).validate_required());
899    }
900
901    #[test]
902    fn patch_validate_required_set_none_fails() {
903        use validator::ValidateRequired;
904        assert!(!Patch::<Option<String>>::Set(None).validate_required());
905    }
906
907    // ── FieldDiff tests ──────────────────────────────────────────
908
909    #[test]
910    fn field_diff_unchanged() {
911        let diff = FieldDiff::new(1, 1);
912        assert!(diff.unchanged());
913        assert!(!diff.changed());
914    }
915
916    #[test]
917    fn field_diff_changed() {
918        let diff = FieldDiff::new(1, 2);
919        assert!(diff.changed());
920    }
921
922    #[test]
923    fn field_diff_changed_to() {
924        let diff = FieldDiff::new(1, 2);
925        assert!(diff.changed_to(&2));
926    }
927
928    #[test]
929    fn field_diff_changed_from() {
930        let diff = FieldDiff::new(1, 2);
931        assert!(diff.changed_from(&1));
932    }
933
934    #[test]
935    fn field_diff_set_updates_after() {
936        let mut diff = FieldDiff::new(1, 1);
937        assert!(diff.unchanged());
938        diff.set(5);
939        assert!(diff.changed());
940        assert_eq!(diff.after(), &5);
941        assert_eq!(diff.before(), &1);
942    }
943
944    #[test]
945    fn field_diff_option_was_set() {
946        let diff = FieldDiff::new(None, Some(42));
947        assert!(diff.was_set());
948        let diff2 = FieldDiff::new(Some(42), Some(42));
949        assert!(!diff2.was_set());
950        let diff3 = FieldDiff::new(None::<i32>, None);
951        assert!(!diff3.was_set());
952        let diff4 = FieldDiff::new(Some(42), None);
953        assert!(!diff4.was_set());
954    }
955
956    #[test]
957    fn field_diff_option_was_cleared() {
958        let diff = FieldDiff::new(Some(42), None);
959        assert!(diff.was_cleared());
960        let diff2 = FieldDiff::new(Some(42), Some(42));
961        assert!(!diff2.was_cleared());
962        let diff3 = FieldDiff::new(None::<i32>, None);
963        assert!(!diff3.was_cleared());
964        let diff4 = FieldDiff::new(None, Some(42));
965        assert!(!diff4.was_cleared());
966    }
967
968    // ── MutationOp tests ────────────────────────────────────────────
969
970    #[test]
971    fn mutation_op_as_str() {
972        assert_eq!(MutationOp::Create.as_str(), "create");
973        assert_eq!(MutationOp::Update.as_str(), "update");
974        assert_eq!(MutationOp::Delete.as_str(), "delete");
975    }
976
977    #[test]
978    fn mutation_op_display() {
979        assert_eq!(format!("{}", MutationOp::Create), "create");
980    }
981
982    // ── MutationContext tests ───────────────────────────────────────
983
984    #[tokio::test]
985    async fn mutation_context_auto_populates() {
986        // Construct inside an (empty) request scope so `actor` is deterministically
987        // `None`: an in-scope read never consults the process-global default actor,
988        // which a concurrent test (`current::…default_actor_is_used_only_out_of_scope`)
989        // may transiently have set. Avoids a test-isolation race on that global.
990        crate::current::scope_request(async {
991            let ctx = MutationContext::new(MutationOp::Create);
992            assert!(ctx.actor.is_none());
993            assert!(ctx.request_id.is_some());
994            // UUID v4 format: 8-4-4-4-12 = 36 chars
995            assert_eq!(ctx.request_id.as_ref().unwrap().len(), 36);
996            assert!(matches!(ctx.op, MutationOp::Create));
997            assert!(ctx.invalidate_keys.is_empty());
998        })
999        .await;
1000    }
1001
1002    #[test]
1003    fn mutation_context_invalidate_pushes_key() {
1004        let mut ctx = MutationContext::new(MutationOp::Create);
1005        assert!(ctx.invalidate_keys.is_empty());
1006        ctx.invalidate("cache:key");
1007        assert_eq!(ctx.invalidate_keys, vec!["cache:key".to_string()]);
1008    }
1009
1010    #[test]
1011    fn mutation_context_with_actor() {
1012        let mut ctx = MutationContext::new(MutationOp::Update);
1013        ctx.actor = Some("user-123".into());
1014        assert_eq!(ctx.actor.as_deref(), Some("user-123"));
1015    }
1016
1017    #[tokio::test]
1018    async fn mutation_context_seeds_actor_from_ambient_scope() {
1019        // Inside an (empty) request scope the constructor yields `None`
1020        // deterministically — an in-scope read never consults the process-global
1021        // default actor a concurrent test may transiently have set (isolation).
1022        crate::current::scope_request(async {
1023            assert!(MutationContext::new(MutationOp::Create).actor.is_none());
1024        })
1025        .await;
1026
1027        // Inside `with_actor(...)` the constructor auto-populates the actor.
1028        crate::current::with_actor("u1", async {
1029            let ctx = MutationContext::new(MutationOp::Create);
1030            assert_eq!(ctx.actor.as_deref(), Some("u1"));
1031        })
1032        .await;
1033    }
1034
1035    #[test]
1036    fn mutation_context_carries_scoped_idempotency_key() {
1037        let mut ctx = MutationContext::new(MutationOp::Create);
1038        assert!(ctx.idempotency_key.is_none());
1039
1040        ctx.set_idempotency_key("v2:scoped-http-key");
1041
1042        assert_eq!(ctx.idempotency_key.as_deref(), Some("v2:scoped-http-key"));
1043    }
1044
1045    #[test]
1046    fn mutation_context_deserializes_without_idempotency_key() {
1047        let ctx: MutationContext = serde_json::from_value(serde_json::json!({
1048            "op": "Create",
1049            "actor": null,
1050            "request_id": "request-1",
1051            "now": "2026-05-17T00:00:00Z",
1052            "invalidate_keys": []
1053        }))
1054        .expect("old durable hook payloads should deserialize");
1055
1056        assert!(ctx.idempotency_key.is_none());
1057    }
1058
1059    // ── MutationHooks / NoHooks tests ───────────────────────────────
1060
1061    #[tokio::test]
1062    async fn no_hooks_all_methods_are_noop() {
1063        let hooks: NoHooks<(), (), ()> = NoHooks::default();
1064        let mut ctx = MutationContext::new(MutationOp::Create);
1065        let mut new_model = ();
1066        let model = ();
1067        let mut draft = UpdateDraft::new(());
1068
1069        assert!(hooks.before_create(&mut ctx, &mut new_model).await.is_ok());
1070        assert!(hooks.before_update(&mut ctx, &mut draft).await.is_ok());
1071        assert!(hooks.before_delete(&mut ctx, &model).await.is_ok());
1072        assert!(hooks.after_create(&mut ctx, &model).await.is_ok());
1073        assert!(hooks.after_update(&mut ctx, &model).await.is_ok());
1074    }
1075
1076    #[tokio::test]
1077    async fn no_hooks_commit_variants_are_noop() {
1078        // after_*_commit variants must also be no-ops on NoHooks
1079        let hooks: NoHooks<(), (), ()> = NoHooks::default();
1080        let mut ctx = MutationContext::new(MutationOp::Create);
1081        let model = ();
1082
1083        assert!(
1084            hooks.after_create_commit(&mut ctx, &model).await.is_ok(),
1085            "after_create_commit must default to Ok(())"
1086        );
1087        assert!(
1088            hooks.after_update_commit(&mut ctx, &model).await.is_ok(),
1089            "after_update_commit must default to Ok(())"
1090        );
1091        assert!(
1092            hooks.after_delete_commit(&mut ctx, &model).await.is_ok(),
1093            "after_delete_commit must default to Ok(())"
1094        );
1095    }
1096
1097    #[tokio::test]
1098    async fn custom_hooks_can_override_commit_variants() {
1099        use std::sync::Arc;
1100        use std::sync::atomic::{AtomicU32, Ordering};
1101
1102        static CALLS: AtomicU32 = AtomicU32::new(0);
1103
1104        #[derive(Clone, Default)]
1105        struct CountingHooks;
1106
1107        impl MutationHooks for CountingHooks {
1108            type Model = ();
1109            type NewModel = ();
1110            type UpdateModel = ();
1111
1112            async fn after_create_commit(
1113                &self,
1114                _ctx: &mut MutationContext,
1115                _record: &Self::Model,
1116            ) -> AutumnResult<()> {
1117                CALLS.fetch_add(1, Ordering::SeqCst);
1118                Ok(())
1119            }
1120
1121            async fn after_update_commit(
1122                &self,
1123                _ctx: &mut MutationContext,
1124                _record: &Self::Model,
1125            ) -> AutumnResult<()> {
1126                CALLS.fetch_add(1, Ordering::SeqCst);
1127                Ok(())
1128            }
1129
1130            async fn after_delete_commit(
1131                &self,
1132                _ctx: &mut MutationContext,
1133                _record: &Self::Model,
1134            ) -> AutumnResult<()> {
1135                CALLS.fetch_add(1, Ordering::SeqCst);
1136                Ok(())
1137            }
1138        }
1139
1140        CALLS.store(0, Ordering::SeqCst);
1141        let hooks = CountingHooks;
1142        let mut ctx = MutationContext::new(MutationOp::Create);
1143        let model = ();
1144
1145        hooks.after_create_commit(&mut ctx, &model).await.unwrap();
1146        hooks.after_update_commit(&mut ctx, &model).await.unwrap();
1147        hooks.after_delete_commit(&mut ctx, &model).await.unwrap();
1148
1149        assert_eq!(CALLS.load(Ordering::SeqCst), 3);
1150        let _ = Arc::new(CountingHooks); // ensure Arc usage works (Clone check)
1151    }
1152
1153    // ── Patch serde tests ──────────────────────────────────────────
1154
1155    #[test]
1156    fn patch_serde_set_roundtrip() {
1157        let p = Patch::Set(42);
1158        let json = serde_json::to_string(&p).unwrap();
1159        assert_eq!(json, "42");
1160        let back: Patch<i32> = serde_json::from_str(&json).unwrap();
1161        assert_eq!(back, Patch::Set(42));
1162    }
1163
1164    #[test]
1165    fn patch_serde_clear_serializes_as_null() {
1166        let p: Patch<i32> = Patch::Clear;
1167        let json = serde_json::to_string(&p).unwrap();
1168        assert_eq!(json, "null");
1169    }
1170
1171    #[test]
1172    fn patch_serde_null_deserializes_as_clear() {
1173        let p: Patch<i32> = serde_json::from_str("null").unwrap();
1174        assert_eq!(p, Patch::Clear);
1175    }
1176
1177    #[test]
1178    fn patch_serde_absent_field_is_unchanged() {
1179        #[derive(Deserialize, PartialEq, Debug)]
1180        struct Payload {
1181            #[serde(default)]
1182            name: Patch<String>,
1183            #[serde(default)]
1184            age: Patch<i32>,
1185        }
1186        let p: Payload = serde_json::from_str(r#"{"name": "Alice"}"#).unwrap();
1187        assert_eq!(p.name, Patch::Set("Alice".to_string()));
1188        assert_eq!(p.age, Patch::Unchanged);
1189    }
1190
1191    #[test]
1192    fn patch_serde_explicit_null_is_clear() {
1193        #[derive(Deserialize, PartialEq, Debug)]
1194        struct Payload {
1195            #[serde(default)]
1196            name: Patch<String>,
1197        }
1198        let p: Payload = serde_json::from_str(r#"{"name": null}"#).unwrap();
1199        assert_eq!(p.name, Patch::Clear);
1200    }
1201
1202    // ── UpdateDraft tests ───────────────────────────────────────────
1203
1204    #[test]
1205    fn update_draft_before_after() {
1206        let draft = UpdateDraft::new_with_changes("old".to_string(), "new".to_string());
1207        assert_eq!(draft.before(), "old");
1208        assert_eq!(draft.after(), "new");
1209    }
1210
1211    #[test]
1212    fn update_draft_into_after() {
1213        let draft = UpdateDraft::new_with_changes(1, 2);
1214        assert_eq!(draft.into_after(), 2);
1215    }
1216
1217    #[test]
1218    fn update_draft_new_clones() {
1219        let draft = UpdateDraft::new(42);
1220        assert_eq!(draft.before(), &42);
1221        assert_eq!(draft.after(), &42);
1222    }
1223
1224    #[test]
1225    fn update_draft_after_mut() {
1226        let mut draft = UpdateDraft::new_with_changes(1, 2);
1227        *draft.after_mut() = 3;
1228        assert_eq!(draft.after(), &3);
1229    }
1230
1231    // ── DraftField tests ────────────────────────────────────────────
1232
1233    #[test]
1234    fn draft_field_before_after() {
1235        let before = 1;
1236        let mut after = 2;
1237        let field = DraftField::new(&before, &mut after);
1238        assert_eq!(field.before(), &1);
1239        assert_eq!(field.after(), &2);
1240    }
1241
1242    #[test]
1243    fn draft_field_changed() {
1244        let before = 1;
1245        let mut after = 2;
1246        let field = DraftField::new(&before, &mut after);
1247        assert!(field.changed());
1248        assert!(!field.unchanged());
1249    }
1250
1251    #[test]
1252    fn draft_field_unchanged() {
1253        let before = 1;
1254        let mut after = 1;
1255        let field = DraftField::new(&before, &mut after);
1256        assert!(field.unchanged());
1257        assert!(!field.changed());
1258    }
1259
1260    #[test]
1261    fn draft_field_changed_to() {
1262        let before = "draft".to_string();
1263        let mut after = "published".to_string();
1264        let field = DraftField::new(&before, &mut after);
1265        assert!(field.changed_to(&"published".to_string()));
1266        assert!(!field.changed_to(&"draft".to_string()));
1267    }
1268
1269    #[test]
1270    fn draft_field_changed_from() {
1271        let before = "draft".to_string();
1272        let mut after = "published".to_string();
1273        let field = DraftField::new(&before, &mut after);
1274        assert!(field.changed_from(&"draft".to_string()));
1275        assert!(!field.changed_from(&"published".to_string()));
1276    }
1277
1278    #[test]
1279    fn draft_field_set_mutates_after() {
1280        let before = 10;
1281        let mut after = 10;
1282        {
1283            let mut field = DraftField::new(&before, &mut after);
1284            assert!(field.unchanged());
1285            field.set(20);
1286            assert!(field.changed());
1287            assert_eq!(field.after(), &20);
1288        }
1289        // Verify mutation propagated to the original variable
1290        assert_eq!(after, 20);
1291    }
1292
1293    #[test]
1294    fn draft_field_option_was_set() {
1295        let before: Option<i32> = None;
1296        let mut after: Option<i32> = Some(42);
1297        let field = DraftField::new(&before, &mut after);
1298        assert!(field.was_set());
1299        assert!(!field.was_cleared());
1300
1301        let before2: Option<i32> = Some(42);
1302        let mut after2: Option<i32> = Some(42);
1303        let field2 = DraftField::new(&before2, &mut after2);
1304        assert!(!field2.was_set());
1305
1306        let before3: Option<i32> = None;
1307        let mut after3: Option<i32> = None;
1308        let field3 = DraftField::new(&before3, &mut after3);
1309        assert!(!field3.was_set());
1310
1311        let before4: Option<i32> = Some(42);
1312        let mut after4: Option<i32> = None;
1313        let field4 = DraftField::new(&before4, &mut after4);
1314        assert!(!field4.was_set());
1315    }
1316
1317    #[test]
1318    fn draft_field_option_was_cleared() {
1319        let before: Option<i32> = Some(42);
1320        let mut after: Option<i32> = None;
1321        let field = DraftField::new(&before, &mut after);
1322        assert!(field.was_cleared());
1323        assert!(!field.was_set());
1324
1325        let before2: Option<i32> = Some(42);
1326        let mut after2: Option<i32> = Some(42);
1327        let field2 = DraftField::new(&before2, &mut after2);
1328        assert!(!field2.was_cleared());
1329
1330        let before3: Option<i32> = None;
1331        let mut after3: Option<i32> = None;
1332        let field3 = DraftField::new(&before3, &mut after3);
1333        assert!(!field3.was_cleared());
1334
1335        let before4: Option<i32> = None;
1336        let mut after4: Option<i32> = Some(42);
1337        let field4 = DraftField::new(&before4, &mut after4);
1338        assert!(!field4.was_cleared());
1339    }
1340}