use serde::de::Deserializer;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MutationOp {
Create,
Update,
Delete,
}
impl MutationOp {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Create => "create",
Self::Update => "update",
Self::Delete => "delete",
}
}
}
impl std::fmt::Display for MutationOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct MutationContext {
pub op: MutationOp,
pub actor: Option<String>,
pub request_id: Option<String>,
pub now: chrono::DateTime<chrono::Utc>,
}
impl MutationContext {
#[must_use]
pub fn new(op: MutationOp) -> Self {
Self {
op,
actor: None,
request_id: Some(uuid::Uuid::new_v4().to_string()),
now: chrono::Utc::now(),
}
}
}
use crate::AutumnResult;
use std::future::Future;
pub trait MutationHooks: Send + Sync + 'static {
type Model: Send + Sync;
type NewModel: Send + Sync;
type UpdateModel: Send + Sync;
fn before_create(
&self,
_ctx: &mut MutationContext,
_new: &mut Self::NewModel,
) -> impl Future<Output = AutumnResult<()>> + Send {
async { Ok(()) }
}
fn before_update(
&self,
_ctx: &mut MutationContext,
_draft: &mut UpdateDraft<Self::Model>,
) -> impl Future<Output = AutumnResult<()>> + Send
where
Self::Model: Clone,
{
async { Ok(()) }
}
fn before_delete(
&self,
_ctx: &mut MutationContext,
_record: &Self::Model,
) -> impl Future<Output = AutumnResult<()>> + Send {
async { Ok(()) }
}
}
pub struct NoHooks<M, N, U> {
_phantom: std::marker::PhantomData<(M, N, U)>,
}
impl<M, N, U> Default for NoHooks<M, N, U> {
fn default() -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
}
impl<M, N, U> MutationHooks for NoHooks<M, N, U>
where
M: Send + Sync + Clone + 'static,
N: Send + Sync + 'static,
U: Send + Sync + 'static,
{
type Model = M;
type NewModel = N;
type UpdateModel = U;
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Patch<T> {
#[default]
Unchanged,
Set(T),
Clear,
}
impl<T> Patch<T> {
#[must_use]
pub const fn is_unchanged(&self) -> bool {
matches!(self, Self::Unchanged)
}
#[must_use]
pub const fn is_set(&self) -> bool {
matches!(self, Self::Set(_))
}
#[must_use]
pub const fn is_clear(&self) -> bool {
matches!(self, Self::Clear)
}
#[must_use]
pub const fn as_set(&self) -> Option<&T> {
match self {
Self::Set(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn into_option(self) -> Option<Option<T>> {
match self {
Self::Set(v) => Some(Some(v)),
Self::Clear => Some(None),
Self::Unchanged => None,
}
}
}
impl<T: Serialize> Serialize for Patch<T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Unchanged | Self::Clear => serializer.serialize_none(),
Self::Set(v) => v.serialize(serializer),
}
}
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for Patch<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let opt: Option<T> = Option::deserialize(deserializer)?;
Ok(opt.map_or_else(|| Self::Clear, Self::Set))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldDiff<T> {
before: T,
after: T,
}
impl<T: PartialEq> FieldDiff<T> {
#[must_use]
pub const fn new(before: T, after: T) -> Self {
Self { before, after }
}
#[must_use]
pub const fn before(&self) -> &T {
&self.before
}
#[must_use]
pub const fn after(&self) -> &T {
&self.after
}
#[must_use]
pub fn changed(&self) -> bool {
self.before != self.after
}
#[must_use]
pub fn unchanged(&self) -> bool {
self.before == self.after
}
#[must_use]
pub fn changed_to(&self, value: &T) -> bool {
self.changed() && self.after == *value
}
#[must_use]
pub fn changed_from(&self, value: &T) -> bool {
self.changed() && self.before == *value
}
pub fn set(&mut self, value: T) {
self.after = value;
}
}
impl<T: PartialEq> FieldDiff<Option<T>> {
#[must_use]
pub const fn was_set(&self) -> bool {
self.before.is_none() && self.after.is_some()
}
#[must_use]
pub const fn was_cleared(&self) -> bool {
self.before.is_some() && self.after.is_none()
}
}
#[derive(Debug, Clone)]
pub struct UpdateDraft<T: Clone> {
pub before: T,
pub after: T,
}
impl<T: Clone> UpdateDraft<T> {
#[must_use]
pub fn new(before: T) -> Self {
let after = before.clone();
Self { before, after }
}
#[must_use]
pub const fn new_with_changes(before: T, after: T) -> Self {
Self { before, after }
}
#[must_use]
pub const fn before(&self) -> &T {
&self.before
}
#[must_use]
pub const fn after(&self) -> &T {
&self.after
}
#[must_use]
pub const fn after_mut(&mut self) -> &mut T {
&mut self.after
}
#[must_use]
pub fn into_after(self) -> T {
self.after
}
}
#[derive(Debug)]
pub struct DraftField<'a, T> {
before: &'a T,
after: &'a mut T,
}
impl<'a, T> DraftField<'a, T> {
#[must_use]
pub const fn new(before: &'a T, after: &'a mut T) -> Self {
Self { before, after }
}
#[must_use]
pub const fn before(&self) -> &T {
self.before
}
#[must_use]
pub const fn after(&self) -> &T {
self.after
}
pub fn set(&mut self, value: T) {
*self.after = value;
}
}
impl<T: PartialEq> DraftField<'_, T> {
#[must_use]
pub fn changed(&self) -> bool {
self.before != self.after
}
#[must_use]
pub fn unchanged(&self) -> bool {
self.before == self.after
}
#[must_use]
pub fn changed_to(&self, value: &T) -> bool {
self.changed() && *self.after == *value
}
#[must_use]
pub fn changed_from(&self, value: &T) -> bool {
self.changed() && *self.before == *value
}
}
impl<T: PartialEq> DraftField<'_, Option<T>> {
#[must_use]
pub const fn was_set(&self) -> bool {
self.before.is_none() && self.after.is_some()
}
#[must_use]
pub const fn was_cleared(&self) -> bool {
self.before.is_some() && self.after.is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn patch_unchanged_is_default() {
let p: Patch<String> = Patch::default();
assert!(p.is_unchanged());
assert!(!p.is_set());
assert!(!p.is_clear());
}
#[test]
fn patch_set_holds_value() {
let p = Patch::Set("hello");
assert!(p.is_set());
assert!(!p.is_unchanged());
assert!(!p.is_clear());
assert_eq!(p.as_set(), Some(&"hello"));
}
#[test]
fn patch_clear_is_clear() {
let p: Patch<i32> = Patch::Clear;
assert!(p.is_clear());
assert!(!p.is_set());
assert!(!p.is_unchanged());
}
#[test]
fn patch_into_option_set() {
assert_eq!(Patch::Set(42).into_option(), Some(Some(42)));
}
#[test]
fn patch_into_option_clear() {
assert_eq!(Patch::<i32>::Clear.into_option(), Some(None));
}
#[test]
fn patch_into_option_unchanged() {
assert_eq!(Patch::<i32>::Unchanged.into_option(), None);
}
#[test]
fn field_diff_unchanged() {
let diff = FieldDiff::new(1, 1);
assert!(diff.unchanged());
assert!(!diff.changed());
}
#[test]
fn field_diff_changed() {
let diff = FieldDiff::new(1, 2);
assert!(diff.changed());
}
#[test]
fn field_diff_changed_to() {
let diff = FieldDiff::new(1, 2);
assert!(diff.changed_to(&2));
}
#[test]
fn field_diff_changed_from() {
let diff = FieldDiff::new(1, 2);
assert!(diff.changed_from(&1));
}
#[test]
fn field_diff_set_updates_after() {
let mut diff = FieldDiff::new(1, 1);
assert!(diff.unchanged());
diff.set(5);
assert!(diff.changed());
assert_eq!(diff.after(), &5);
assert_eq!(diff.before(), &1);
}
#[test]
fn field_diff_option_was_set() {
let diff = FieldDiff::new(None, Some(42));
assert!(diff.was_set());
}
#[test]
fn field_diff_option_was_cleared() {
let diff = FieldDiff::new(Some(42), None);
assert!(diff.was_cleared());
}
#[test]
fn mutation_op_as_str() {
assert_eq!(MutationOp::Create.as_str(), "create");
assert_eq!(MutationOp::Update.as_str(), "update");
assert_eq!(MutationOp::Delete.as_str(), "delete");
}
#[test]
fn mutation_op_display() {
assert_eq!(format!("{}", MutationOp::Create), "create");
}
#[test]
fn mutation_context_auto_populates() {
let ctx = MutationContext::new(MutationOp::Create);
assert!(ctx.actor.is_none());
assert!(ctx.request_id.is_some());
assert_eq!(ctx.request_id.as_ref().unwrap().len(), 36);
assert!(matches!(ctx.op, MutationOp::Create));
}
#[test]
fn mutation_context_with_actor() {
let mut ctx = MutationContext::new(MutationOp::Update);
ctx.actor = Some("user-123".into());
assert_eq!(ctx.actor.as_deref(), Some("user-123"));
}
#[tokio::test]
async fn no_hooks_all_methods_are_noop() {
let hooks: NoHooks<(), (), ()> = NoHooks::default();
let mut ctx = MutationContext::new(MutationOp::Create);
let mut new_model = ();
let model = ();
let mut draft = UpdateDraft::new(());
assert!(hooks.before_create(&mut ctx, &mut new_model).await.is_ok());
assert!(hooks.before_update(&mut ctx, &mut draft).await.is_ok());
assert!(hooks.before_delete(&mut ctx, &model).await.is_ok());
}
#[test]
fn patch_serde_set_roundtrip() {
let p = Patch::Set(42);
let json = serde_json::to_string(&p).unwrap();
assert_eq!(json, "42");
let back: Patch<i32> = serde_json::from_str(&json).unwrap();
assert_eq!(back, Patch::Set(42));
}
#[test]
fn patch_serde_clear_serializes_as_null() {
let p: Patch<i32> = Patch::Clear;
let json = serde_json::to_string(&p).unwrap();
assert_eq!(json, "null");
}
#[test]
fn patch_serde_null_deserializes_as_clear() {
let p: Patch<i32> = serde_json::from_str("null").unwrap();
assert_eq!(p, Patch::Clear);
}
#[test]
fn patch_serde_absent_field_is_unchanged() {
#[derive(Deserialize, PartialEq, Debug)]
struct Payload {
#[serde(default)]
name: Patch<String>,
#[serde(default)]
age: Patch<i32>,
}
let p: Payload = serde_json::from_str(r#"{"name": "Alice"}"#).unwrap();
assert_eq!(p.name, Patch::Set("Alice".to_string()));
assert_eq!(p.age, Patch::Unchanged);
}
#[test]
fn patch_serde_explicit_null_is_clear() {
#[derive(Deserialize, PartialEq, Debug)]
struct Payload {
#[serde(default)]
name: Patch<String>,
}
let p: Payload = serde_json::from_str(r#"{"name": null}"#).unwrap();
assert_eq!(p.name, Patch::Clear);
}
#[test]
fn update_draft_before_after() {
let draft = UpdateDraft::new_with_changes("old".to_string(), "new".to_string());
assert_eq!(draft.before(), "old");
assert_eq!(draft.after(), "new");
}
#[test]
fn update_draft_into_after() {
let draft = UpdateDraft::new_with_changes(1, 2);
assert_eq!(draft.into_after(), 2);
}
#[test]
fn update_draft_new_clones() {
let draft = UpdateDraft::new(42);
assert_eq!(draft.before(), &42);
assert_eq!(draft.after(), &42);
}
#[test]
fn update_draft_after_mut() {
let mut draft = UpdateDraft::new_with_changes(1, 2);
*draft.after_mut() = 3;
assert_eq!(draft.after(), &3);
}
#[test]
fn draft_field_before_after() {
let before = 1;
let mut after = 2;
let field = DraftField::new(&before, &mut after);
assert_eq!(field.before(), &1);
assert_eq!(field.after(), &2);
}
#[test]
fn draft_field_changed() {
let before = 1;
let mut after = 2;
let field = DraftField::new(&before, &mut after);
assert!(field.changed());
assert!(!field.unchanged());
}
#[test]
fn draft_field_unchanged() {
let before = 1;
let mut after = 1;
let field = DraftField::new(&before, &mut after);
assert!(field.unchanged());
assert!(!field.changed());
}
#[test]
fn draft_field_changed_to() {
let before = "draft".to_string();
let mut after = "published".to_string();
let field = DraftField::new(&before, &mut after);
assert!(field.changed_to(&"published".to_string()));
assert!(!field.changed_to(&"draft".to_string()));
}
#[test]
fn draft_field_changed_from() {
let before = "draft".to_string();
let mut after = "published".to_string();
let field = DraftField::new(&before, &mut after);
assert!(field.changed_from(&"draft".to_string()));
assert!(!field.changed_from(&"published".to_string()));
}
#[test]
fn draft_field_set_mutates_after() {
let before = 10;
let mut after = 10;
{
let mut field = DraftField::new(&before, &mut after);
assert!(field.unchanged());
field.set(20);
assert!(field.changed());
assert_eq!(field.after(), &20);
}
assert_eq!(after, 20);
}
#[test]
fn draft_field_option_was_set() {
let before: Option<i32> = None;
let mut after: Option<i32> = Some(42);
let field = DraftField::new(&before, &mut after);
assert!(field.was_set());
assert!(!field.was_cleared());
}
#[test]
fn draft_field_option_was_cleared() {
let before: Option<i32> = Some(42);
let mut after: Option<i32> = None;
let field = DraftField::new(&before, &mut after);
assert!(field.was_cleared());
assert!(!field.was_set());
}
}