1use serde::de::Deserializer;
58use serde::{Deserialize, Serialize};
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[non_exhaustive]
65pub enum MutationOp {
66 Create,
68 Update,
70 Delete,
72}
73
74impl MutationOp {
75 #[must_use]
77 pub const fn as_str(self) -> &'static str {
78 match self {
79 Self::Create => "create",
80 Self::Update => "update",
81 Self::Delete => "delete",
82 }
83 }
84}
85
86impl std::fmt::Display for MutationOp {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.write_str(self.as_str())
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct MutationContext {
98 pub op: MutationOp,
100 pub actor: Option<String>,
102 pub request_id: Option<String>,
104 pub now: chrono::DateTime<chrono::Utc>,
106 pub invalidate_keys: Vec<String>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub idempotency_key: Option<String>,
112}
113
114impl MutationContext {
115 #[must_use]
120 pub fn new(op: MutationOp) -> Self {
121 Self {
122 op,
123 actor: None,
124 request_id: Some(uuid::Uuid::new_v4().to_string()),
125 now: chrono::Utc::now(),
126 invalidate_keys: Vec::new(),
127 idempotency_key: None,
128 }
129 }
130
131 pub fn invalidate(&mut self, key: impl Into<String>) {
133 self.invalidate_keys.push(key.into());
134 }
135
136 pub fn set_idempotency_key(&mut self, key: impl Into<String>) {
138 self.idempotency_key = Some(key.into());
139 }
140}
141
142use crate::AutumnResult;
145use std::future::Future;
146
147pub trait MutationHooks: Send + Sync + 'static {
160 type Model: Send + Sync;
162 type NewModel: Send + Sync;
164 type UpdateModel: Send + Sync;
166
167 fn before_create(
169 &self,
170 _ctx: &mut MutationContext,
171 _new: &mut Self::NewModel,
172 ) -> impl Future<Output = AutumnResult<()>> + Send {
173 async { Ok(()) }
174 }
175
176 fn before_update(
187 &self,
188 _ctx: &mut MutationContext,
189 _draft: &mut UpdateDraft<Self::Model>,
190 ) -> impl Future<Output = AutumnResult<()>> + Send
191 where
192 Self::Model: Clone,
193 {
194 async { Ok(()) }
195 }
196
197 fn before_delete(
199 &self,
200 _ctx: &mut MutationContext,
201 _record: &Self::Model,
202 ) -> impl Future<Output = AutumnResult<()>> + Send {
203 async { Ok(()) }
204 }
205
206 fn after_create(
208 &self,
209 _ctx: &mut MutationContext,
210 _record: &Self::Model,
211 ) -> impl Future<Output = AutumnResult<()>> + Send {
212 async { Ok(()) }
213 }
214
215 fn after_update(
217 &self,
218 _ctx: &mut MutationContext,
219 _record: &Self::Model,
220 ) -> impl Future<Output = AutumnResult<()>> + Send {
221 async { Ok(()) }
222 }
223
224 fn after_create_commit(
238 &self,
239 _ctx: &mut MutationContext,
240 _record: &Self::Model,
241 ) -> impl Future<Output = AutumnResult<()>> + Send {
242 async { Ok(()) }
243 }
244
245 fn after_update_commit(
250 &self,
251 _ctx: &mut MutationContext,
252 _record: &Self::Model,
253 ) -> impl Future<Output = AutumnResult<()>> + Send {
254 async { Ok(()) }
255 }
256
257 fn after_delete_commit(
262 &self,
263 _ctx: &mut MutationContext,
264 _record: &Self::Model,
265 ) -> impl Future<Output = AutumnResult<()>> + Send {
266 async { Ok(()) }
267 }
268}
269
270pub trait RepositoryHooksDefault: Sized {
276 fn autumn_default() -> Self;
278}
279
280impl<T> RepositoryHooksDefault for T
281where
282 T: Default,
283{
284 fn autumn_default() -> Self {
285 Self::default()
286 }
287}
288
289pub trait RepositoryHooksClone: Sized {
294 #[must_use]
296 fn autumn_clone(&self) -> Self;
297}
298
299impl<T> RepositoryHooksClone for T
300where
301 T: Clone,
302{
303 fn autumn_clone(&self) -> Self {
304 self.clone()
305 }
306}
307
308pub struct NoHooks<M, N, U> {
315 _phantom: std::marker::PhantomData<(M, N, U)>,
316}
317
318impl<M, N, U> Default for NoHooks<M, N, U> {
319 fn default() -> Self {
320 Self {
321 _phantom: std::marker::PhantomData,
322 }
323 }
324}
325
326impl<M, N, U> MutationHooks for NoHooks<M, N, U>
327where
328 M: Send + Sync + Clone + 'static,
329 N: Send + Sync + 'static,
330 U: Send + Sync + 'static,
331{
332 type Model = M;
333 type NewModel = N;
334 type UpdateModel = U;
335}
336
337#[derive(Debug, Clone, Default, PartialEq, Eq)]
347pub enum Patch<T> {
348 #[default]
350 Unchanged,
351 Set(T),
353 Clear,
355}
356
357impl<T> Patch<T> {
358 #[must_use]
360 pub const fn is_unchanged(&self) -> bool {
361 matches!(self, Self::Unchanged)
362 }
363
364 #[must_use]
366 pub const fn is_set(&self) -> bool {
367 matches!(self, Self::Set(_))
368 }
369
370 #[must_use]
372 pub const fn is_clear(&self) -> bool {
373 matches!(self, Self::Clear)
374 }
375
376 #[must_use]
378 pub const fn as_set(&self) -> Option<&T> {
379 match self {
380 Self::Set(v) => Some(v),
381 _ => None,
382 }
383 }
384
385 #[must_use]
391 pub fn into_option(self) -> Option<Option<T>> {
392 match self {
393 Self::Set(v) => Some(Some(v)),
394 Self::Clear => Some(None),
395 Self::Unchanged => None,
396 }
397 }
398}
399
400impl<T: Serialize> Serialize for Patch<T> {
401 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
402 match self {
403 Self::Unchanged | Self::Clear => serializer.serialize_none(),
404 Self::Set(v) => v.serialize(serializer),
405 }
406 }
407}
408
409impl<'de, T: Deserialize<'de>> Deserialize<'de> for Patch<T> {
410 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
411 let opt: Option<T> = Option::deserialize(deserializer)?;
414 Ok(opt.map_or_else(|| Self::Clear, Self::Set))
415 }
416}
417
418#[derive(Debug, Clone, PartialEq, Eq)]
424pub struct FieldDiff<T> {
425 before: T,
426 after: T,
427}
428
429impl<T: PartialEq> FieldDiff<T> {
430 #[must_use]
432 pub const fn new(before: T, after: T) -> Self {
433 Self { before, after }
434 }
435
436 #[must_use]
438 pub const fn before(&self) -> &T {
439 &self.before
440 }
441
442 #[must_use]
444 pub const fn after(&self) -> &T {
445 &self.after
446 }
447
448 #[must_use]
450 pub fn changed(&self) -> bool {
451 self.before != self.after
452 }
453
454 #[must_use]
456 pub fn unchanged(&self) -> bool {
457 self.before == self.after
458 }
459
460 #[must_use]
462 pub fn changed_to(&self, value: &T) -> bool {
463 self.changed() && self.after == *value
464 }
465
466 #[must_use]
468 pub fn changed_from(&self, value: &T) -> bool {
469 self.changed() && self.before == *value
470 }
471
472 pub fn set(&mut self, value: T) {
474 self.after = value;
475 }
476}
477
478impl<T: PartialEq> FieldDiff<Option<T>> {
479 #[must_use]
481 pub const fn was_set(&self) -> bool {
482 self.before.is_none() && self.after.is_some()
483 }
484
485 #[must_use]
487 pub const fn was_cleared(&self) -> bool {
488 self.before.is_some() && self.after.is_none()
489 }
490}
491
492#[derive(Debug, Clone)]
513pub struct UpdateDraft<T: Clone> {
514 pub before: T,
519 pub after: T,
524}
525
526impl<T: Clone> UpdateDraft<T> {
527 #[must_use]
532 pub fn new(before: T) -> Self {
533 let after = before.clone();
534 Self { before, after }
535 }
536
537 #[must_use]
541 pub const fn new_with_changes(before: T, after: T) -> Self {
542 Self { before, after }
543 }
544
545 #[must_use]
547 pub const fn before(&self) -> &T {
548 &self.before
549 }
550
551 #[must_use]
553 pub const fn after(&self) -> &T {
554 &self.after
555 }
556
557 #[must_use]
561 pub const fn after_mut(&mut self) -> &mut T {
562 &mut self.after
563 }
564
565 #[must_use]
567 pub fn into_after(self) -> T {
568 self.after
569 }
570}
571
572#[derive(Debug)]
590pub struct DraftField<'a, T> {
591 before: &'a T,
592 after: &'a mut T,
593}
594
595impl<'a, T> DraftField<'a, T> {
596 #[must_use]
598 pub const fn new(before: &'a T, after: &'a mut T) -> Self {
599 Self { before, after }
600 }
601
602 #[must_use]
604 pub const fn before(&self) -> &T {
605 self.before
606 }
607
608 #[must_use]
610 pub const fn after(&self) -> &T {
611 self.after
612 }
613
614 pub fn set(&mut self, value: T) {
616 *self.after = value;
617 }
618}
619
620impl<T: PartialEq> DraftField<'_, T> {
621 #[must_use]
623 pub fn changed(&self) -> bool {
624 self.before != self.after
625 }
626
627 #[must_use]
629 pub fn unchanged(&self) -> bool {
630 self.before == self.after
631 }
632
633 #[must_use]
635 pub fn changed_to(&self, value: &T) -> bool {
636 self.changed() && *self.after == *value
637 }
638
639 #[must_use]
641 pub fn changed_from(&self, value: &T) -> bool {
642 self.changed() && *self.before == *value
643 }
644}
645
646impl<T: PartialEq> DraftField<'_, Option<T>> {
647 #[must_use]
649 pub const fn was_set(&self) -> bool {
650 self.before.is_none() && self.after.is_some()
651 }
652
653 #[must_use]
655 pub const fn was_cleared(&self) -> bool {
656 self.before.is_some() && self.after.is_none()
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663
664 #[test]
667 fn patch_unchanged_is_default() {
668 let p: Patch<String> = Patch::default();
669 assert!(p.is_unchanged());
670 assert!(!p.is_set());
671 assert!(!p.is_clear());
672 }
673
674 #[test]
675 fn patch_set_holds_value() {
676 let p = Patch::Set("hello");
677 assert!(p.is_set());
678 assert!(!p.is_unchanged());
679 assert!(!p.is_clear());
680 assert_eq!(p.as_set(), Some(&"hello"));
681 }
682
683 #[test]
684 fn patch_clear_is_clear() {
685 let p: Patch<i32> = Patch::Clear;
686 assert!(p.is_clear());
687 assert!(!p.is_set());
688 assert!(!p.is_unchanged());
689 }
690
691 #[test]
692 fn patch_into_option_set() {
693 assert_eq!(Patch::Set(42).into_option(), Some(Some(42)));
694 }
695
696 #[test]
697 fn patch_into_option_clear() {
698 assert_eq!(Patch::<i32>::Clear.into_option(), Some(None));
699 }
700
701 #[test]
702 fn patch_into_option_unchanged() {
703 assert_eq!(Patch::<i32>::Unchanged.into_option(), None);
704 }
705
706 #[test]
709 fn field_diff_unchanged() {
710 let diff = FieldDiff::new(1, 1);
711 assert!(diff.unchanged());
712 assert!(!diff.changed());
713 }
714
715 #[test]
716 fn field_diff_changed() {
717 let diff = FieldDiff::new(1, 2);
718 assert!(diff.changed());
719 }
720
721 #[test]
722 fn field_diff_changed_to() {
723 let diff = FieldDiff::new(1, 2);
724 assert!(diff.changed_to(&2));
725 }
726
727 #[test]
728 fn field_diff_changed_from() {
729 let diff = FieldDiff::new(1, 2);
730 assert!(diff.changed_from(&1));
731 }
732
733 #[test]
734 fn field_diff_set_updates_after() {
735 let mut diff = FieldDiff::new(1, 1);
736 assert!(diff.unchanged());
737 diff.set(5);
738 assert!(diff.changed());
739 assert_eq!(diff.after(), &5);
740 assert_eq!(diff.before(), &1);
741 }
742
743 #[test]
744 fn field_diff_option_was_set() {
745 let diff = FieldDiff::new(None, Some(42));
746 assert!(diff.was_set());
747 }
748
749 #[test]
750 fn field_diff_option_was_cleared() {
751 let diff = FieldDiff::new(Some(42), None);
752 assert!(diff.was_cleared());
753 }
754
755 #[test]
758 fn mutation_op_as_str() {
759 assert_eq!(MutationOp::Create.as_str(), "create");
760 assert_eq!(MutationOp::Update.as_str(), "update");
761 assert_eq!(MutationOp::Delete.as_str(), "delete");
762 }
763
764 #[test]
765 fn mutation_op_display() {
766 assert_eq!(format!("{}", MutationOp::Create), "create");
767 }
768
769 #[test]
772 fn mutation_context_auto_populates() {
773 let ctx = MutationContext::new(MutationOp::Create);
774 assert!(ctx.actor.is_none());
775 assert!(ctx.request_id.is_some());
776 assert_eq!(ctx.request_id.as_ref().unwrap().len(), 36);
778 assert!(matches!(ctx.op, MutationOp::Create));
779 assert!(ctx.invalidate_keys.is_empty());
780 }
781
782 #[test]
783 fn mutation_context_invalidate_pushes_key() {
784 let mut ctx = MutationContext::new(MutationOp::Create);
785 assert!(ctx.invalidate_keys.is_empty());
786 ctx.invalidate("cache:key");
787 assert_eq!(ctx.invalidate_keys, vec!["cache:key".to_string()]);
788 }
789
790 #[test]
791 fn mutation_context_with_actor() {
792 let mut ctx = MutationContext::new(MutationOp::Update);
793 ctx.actor = Some("user-123".into());
794 assert_eq!(ctx.actor.as_deref(), Some("user-123"));
795 }
796
797 #[test]
798 fn mutation_context_carries_scoped_idempotency_key() {
799 let mut ctx = MutationContext::new(MutationOp::Create);
800 assert!(ctx.idempotency_key.is_none());
801
802 ctx.set_idempotency_key("v2:scoped-http-key");
803
804 assert_eq!(ctx.idempotency_key.as_deref(), Some("v2:scoped-http-key"));
805 }
806
807 #[test]
808 fn mutation_context_deserializes_without_idempotency_key() {
809 let ctx: MutationContext = serde_json::from_value(serde_json::json!({
810 "op": "Create",
811 "actor": null,
812 "request_id": "request-1",
813 "now": "2026-05-17T00:00:00Z",
814 "invalidate_keys": []
815 }))
816 .expect("old durable hook payloads should deserialize");
817
818 assert!(ctx.idempotency_key.is_none());
819 }
820
821 #[tokio::test]
824 async fn no_hooks_all_methods_are_noop() {
825 let hooks: NoHooks<(), (), ()> = NoHooks::default();
826 let mut ctx = MutationContext::new(MutationOp::Create);
827 let mut new_model = ();
828 let model = ();
829 let mut draft = UpdateDraft::new(());
830
831 assert!(hooks.before_create(&mut ctx, &mut new_model).await.is_ok());
832 assert!(hooks.before_update(&mut ctx, &mut draft).await.is_ok());
833 assert!(hooks.before_delete(&mut ctx, &model).await.is_ok());
834 assert!(hooks.after_create(&mut ctx, &model).await.is_ok());
835 assert!(hooks.after_update(&mut ctx, &model).await.is_ok());
836 }
837
838 #[tokio::test]
839 async fn no_hooks_commit_variants_are_noop() {
840 let hooks: NoHooks<(), (), ()> = NoHooks::default();
842 let mut ctx = MutationContext::new(MutationOp::Create);
843 let model = ();
844
845 assert!(
846 hooks.after_create_commit(&mut ctx, &model).await.is_ok(),
847 "after_create_commit must default to Ok(())"
848 );
849 assert!(
850 hooks.after_update_commit(&mut ctx, &model).await.is_ok(),
851 "after_update_commit must default to Ok(())"
852 );
853 assert!(
854 hooks.after_delete_commit(&mut ctx, &model).await.is_ok(),
855 "after_delete_commit must default to Ok(())"
856 );
857 }
858
859 #[tokio::test]
860 async fn custom_hooks_can_override_commit_variants() {
861 use std::sync::Arc;
862 use std::sync::atomic::{AtomicU32, Ordering};
863
864 static CALLS: AtomicU32 = AtomicU32::new(0);
865
866 #[derive(Clone, Default)]
867 struct CountingHooks;
868
869 impl MutationHooks for CountingHooks {
870 type Model = ();
871 type NewModel = ();
872 type UpdateModel = ();
873
874 async fn after_create_commit(
875 &self,
876 _ctx: &mut MutationContext,
877 _record: &Self::Model,
878 ) -> AutumnResult<()> {
879 CALLS.fetch_add(1, Ordering::SeqCst);
880 Ok(())
881 }
882
883 async fn after_update_commit(
884 &self,
885 _ctx: &mut MutationContext,
886 _record: &Self::Model,
887 ) -> AutumnResult<()> {
888 CALLS.fetch_add(1, Ordering::SeqCst);
889 Ok(())
890 }
891
892 async fn after_delete_commit(
893 &self,
894 _ctx: &mut MutationContext,
895 _record: &Self::Model,
896 ) -> AutumnResult<()> {
897 CALLS.fetch_add(1, Ordering::SeqCst);
898 Ok(())
899 }
900 }
901
902 CALLS.store(0, Ordering::SeqCst);
903 let hooks = CountingHooks;
904 let mut ctx = MutationContext::new(MutationOp::Create);
905 let model = ();
906
907 hooks.after_create_commit(&mut ctx, &model).await.unwrap();
908 hooks.after_update_commit(&mut ctx, &model).await.unwrap();
909 hooks.after_delete_commit(&mut ctx, &model).await.unwrap();
910
911 assert_eq!(CALLS.load(Ordering::SeqCst), 3);
912 let _ = Arc::new(CountingHooks); }
914
915 #[test]
918 fn patch_serde_set_roundtrip() {
919 let p = Patch::Set(42);
920 let json = serde_json::to_string(&p).unwrap();
921 assert_eq!(json, "42");
922 let back: Patch<i32> = serde_json::from_str(&json).unwrap();
923 assert_eq!(back, Patch::Set(42));
924 }
925
926 #[test]
927 fn patch_serde_clear_serializes_as_null() {
928 let p: Patch<i32> = Patch::Clear;
929 let json = serde_json::to_string(&p).unwrap();
930 assert_eq!(json, "null");
931 }
932
933 #[test]
934 fn patch_serde_null_deserializes_as_clear() {
935 let p: Patch<i32> = serde_json::from_str("null").unwrap();
936 assert_eq!(p, Patch::Clear);
937 }
938
939 #[test]
940 fn patch_serde_absent_field_is_unchanged() {
941 #[derive(Deserialize, PartialEq, Debug)]
942 struct Payload {
943 #[serde(default)]
944 name: Patch<String>,
945 #[serde(default)]
946 age: Patch<i32>,
947 }
948 let p: Payload = serde_json::from_str(r#"{"name": "Alice"}"#).unwrap();
949 assert_eq!(p.name, Patch::Set("Alice".to_string()));
950 assert_eq!(p.age, Patch::Unchanged);
951 }
952
953 #[test]
954 fn patch_serde_explicit_null_is_clear() {
955 #[derive(Deserialize, PartialEq, Debug)]
956 struct Payload {
957 #[serde(default)]
958 name: Patch<String>,
959 }
960 let p: Payload = serde_json::from_str(r#"{"name": null}"#).unwrap();
961 assert_eq!(p.name, Patch::Clear);
962 }
963
964 #[test]
967 fn update_draft_before_after() {
968 let draft = UpdateDraft::new_with_changes("old".to_string(), "new".to_string());
969 assert_eq!(draft.before(), "old");
970 assert_eq!(draft.after(), "new");
971 }
972
973 #[test]
974 fn update_draft_into_after() {
975 let draft = UpdateDraft::new_with_changes(1, 2);
976 assert_eq!(draft.into_after(), 2);
977 }
978
979 #[test]
980 fn update_draft_new_clones() {
981 let draft = UpdateDraft::new(42);
982 assert_eq!(draft.before(), &42);
983 assert_eq!(draft.after(), &42);
984 }
985
986 #[test]
987 fn update_draft_after_mut() {
988 let mut draft = UpdateDraft::new_with_changes(1, 2);
989 *draft.after_mut() = 3;
990 assert_eq!(draft.after(), &3);
991 }
992
993 #[test]
996 fn draft_field_before_after() {
997 let before = 1;
998 let mut after = 2;
999 let field = DraftField::new(&before, &mut after);
1000 assert_eq!(field.before(), &1);
1001 assert_eq!(field.after(), &2);
1002 }
1003
1004 #[test]
1005 fn draft_field_changed() {
1006 let before = 1;
1007 let mut after = 2;
1008 let field = DraftField::new(&before, &mut after);
1009 assert!(field.changed());
1010 assert!(!field.unchanged());
1011 }
1012
1013 #[test]
1014 fn draft_field_unchanged() {
1015 let before = 1;
1016 let mut after = 1;
1017 let field = DraftField::new(&before, &mut after);
1018 assert!(field.unchanged());
1019 assert!(!field.changed());
1020 }
1021
1022 #[test]
1023 fn draft_field_changed_to() {
1024 let before = "draft".to_string();
1025 let mut after = "published".to_string();
1026 let field = DraftField::new(&before, &mut after);
1027 assert!(field.changed_to(&"published".to_string()));
1028 assert!(!field.changed_to(&"draft".to_string()));
1029 }
1030
1031 #[test]
1032 fn draft_field_changed_from() {
1033 let before = "draft".to_string();
1034 let mut after = "published".to_string();
1035 let field = DraftField::new(&before, &mut after);
1036 assert!(field.changed_from(&"draft".to_string()));
1037 assert!(!field.changed_from(&"published".to_string()));
1038 }
1039
1040 #[test]
1041 fn draft_field_set_mutates_after() {
1042 let before = 10;
1043 let mut after = 10;
1044 {
1045 let mut field = DraftField::new(&before, &mut after);
1046 assert!(field.unchanged());
1047 field.set(20);
1048 assert!(field.changed());
1049 assert_eq!(field.after(), &20);
1050 }
1051 assert_eq!(after, 20);
1053 }
1054
1055 #[test]
1056 fn draft_field_option_was_set() {
1057 let before: Option<i32> = None;
1058 let mut after: Option<i32> = Some(42);
1059 let field = DraftField::new(&before, &mut after);
1060 assert!(field.was_set());
1061 assert!(!field.was_cleared());
1062 }
1063
1064 #[test]
1065 fn draft_field_option_was_cleared() {
1066 let before: Option<i32> = Some(42);
1067 let mut after: Option<i32> = None;
1068 let field = DraftField::new(&before, &mut after);
1069 assert!(field.was_cleared());
1070 assert!(!field.was_set());
1071 }
1072}