vstorage 0.7.0

Common API for various icalendar/vcard storages.
Documentation
// Copyright 2023-2025 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: EUPL-1.2

//! Synchronisation operations.
//!
//! The main type is [`Operation`], which represents a single atomic synchronisation action.

use std::sync::Arc;

use crate::{
    Href,
    base::ItemVersion,
    property::Property,
    sync::{
        analysis::{ItemSource, ResolvedMapping, StatusAction},
        conflict::ConflictInfo,
        ordering::{
            CompletionDroppedError, CompletionHandle, DeletionCompletionHandle, DeletionWaitHandle,
            WaitHandle,
        },
        status::{MappingUid, Side, StatusVersions},
    },
};

/// Operation to be executed on a pair.
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Operation {
    /// Flush stale mappings from status database.
    FlushStaleMappings { stale_uids: Vec<MappingUid> },

    /// Collection-level operation
    Collection(CollectionOp),

    /// Item-level operation
    Item(ItemOp),

    /// Property-level operation
    Property(PropertyOp),
}

impl Operation {
    /// Returns true if this operation represents a conflict requiring resolution.
    #[must_use]
    pub fn is_conflict(&self) -> bool {
        matches!(
            self,
            Operation::Item(ItemOp::Conflict { .. })
                | Operation::Property(PropertyOp {
                    kind: PropertyOpKind::Conflict { .. },
                    ..
                })
        )
    }
}

/// Collection-level synchronisation operation.
#[derive(Debug, Clone)]
pub enum CollectionOp {
    /// Save new collection mapping to status database.
    SaveToStatus {
        mapping: Arc<ResolvedMapping>,
        completion: CompletionHandle,
    },

    /// Create collection on one side.
    CreateInOne {
        mapping: Arc<ResolvedMapping>,
        side: Side,
        completion: CompletionHandle,
    },

    /// Create collection on both sides.
    CreateInBoth {
        mapping: Arc<ResolvedMapping>,
        completion: CompletionHandle,
    },

    /// Delete collection (after items removed).
    ///
    /// Guaranteed to be queued after all item operations for this collection.
    /// Execution waits for all item/property operations to complete via the deletion barrier.
    Delete {
        mapping: Arc<ResolvedMapping>,
        mapping_uid: MappingUid,
        side: Side,
        /// Wait handle for deletion barrier - ensures all items/properties complete before deletion.
        wait_for_items: DeletionWaitHandle,
    },

    /// Store sync token for a collection side.
    StoreSyncToken {
        mapping_uid: MappingUidSource,
        side: Side,
        token: String,
        wait_for_items: DeletionWaitHandle,
    },
}

/// Source of mapping UID for item operations.
///
/// The mapping UID may be immediately available (for existing collections)
/// or deferred via a wait handle (for collections being created).
#[derive(Debug, Clone)]
pub enum MappingUidSource {
    /// Mapping UID is immediately available.
    Immediate(MappingUid),
    /// Mapping UID will be provided via wait handle when collection operation completes.
    Deferred(WaitHandle),
}

impl MappingUidSource {
    /// Returns the immediate mapping UID if available, otherwise None.
    #[must_use]
    pub fn immediate(&self) -> Option<MappingUid> {
        match self {
            MappingUidSource::Immediate(uid) => Some(*uid),
            MappingUidSource::Deferred(_) => None,
        }
    }

    /// Returns the wait handle if mapping UID is deferred, otherwise None.
    #[must_use]
    pub fn wait_handle(&self) -> Option<&WaitHandle> {
        match self {
            MappingUidSource::Immediate(_) => None,
            MappingUidSource::Deferred(handle) => Some(handle),
        }
    }

    /// Resolves the mapping UID, waiting if necessary.
    ///
    /// If immediate, returns the UID. If deferred, waits on the handle.
    ///
    /// # Errors
    ///
    /// Returns an error if the completion handle was dropped before signaling.
    pub async fn resolve(self) -> Result<MappingUid, CompletionDroppedError> {
        match self {
            MappingUidSource::Immediate(uid) => Ok(uid),
            MappingUidSource::Deferred(mut handle) => handle.wait().await,
        }
    }
}

/// Storage operation to perform on the target side.
#[derive(Debug, Clone)]
pub enum StorageWrite {
    /// Create a new item in the given collection.
    Create {
        collection: Href,
        resource_name: Option<Href>,
    },
    /// Update an existing item at the given href/etag.
    Update { target: ItemVersion },
}

/// Extract the resource name (last path component) from an href.
pub(super) fn resource_name(href: &str) -> String {
    href.rsplit('/').next().unwrap_or(href).to_string()
}

/// Status database operation to perform after the storage write.
#[derive(Debug, Clone)]
pub enum StatusWrite {
    /// Insert a new entry into the status database.
    Insert,
    /// Update an existing status entry.
    Update { old: StatusVersions },
}

/// Write item to the target side from the source on the opposite side.
#[derive(Debug, Clone)]
pub struct WriteItem {
    /// Data from the source side (the side being read from).
    pub source: ItemSource,
    /// Which side to write to.
    pub target_side: Side,
    pub storage_write: StorageWrite,
    pub status_write: StatusWrite,
    pub mapping_uid: MappingUidSource,
    /// Optional handle to signal collection deletion barrier when this operation completes.
    pub on_complete: Option<DeletionCompletionHandle>,
}

impl From<WriteItem> for ItemOp {
    fn from(inner: WriteItem) -> Self {
        ItemOp::Write(inner)
    }
}

/// Delete item from one side.
#[derive(Debug, Clone)]
pub struct DeleteItem {
    pub target: ItemVersion,
    /// Which side to delete from.
    pub side: Side,
    pub uid: String,
    pub mapping_uid: MappingUidSource,
    /// Optional handle to signal collection deletion barrier when this operation completes.
    pub on_complete: Option<DeletionCompletionHandle>,
}

impl From<DeleteItem> for ItemOp {
    fn from(inner: DeleteItem) -> Self {
        ItemOp::Delete(inner)
    }
}

/// Only update status database (no storage operations).
#[derive(Debug, Clone)]
pub struct StatusOnly {
    pub action: StatusAction,
    pub mapping_uid: MappingUidSource,
    /// Optional handle to signal collection deletion barrier when this operation completes.
    pub on_complete: Option<DeletionCompletionHandle>,
}

impl From<StatusOnly> for ItemOp {
    fn from(inner: StatusOnly) -> Self {
        ItemOp::StatusOnly(inner)
    }
}

/// Item-level synchronisation operation.
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum ItemOp {
    /// Write item to one side from source on the other.
    ///
    /// If collection creation is pending, the mapping UID will be received via a wait handle.
    Write(WriteItem),

    /// Delete item from one side.
    Delete(DeleteItem),

    /// Only update status database (no storage operations).
    StatusOnly(StatusOnly),

    /// Conflict requiring resolution.
    Conflict {
        info: ConflictInfo,
        mapping: Arc<ResolvedMapping>,
        mapping_uid: MappingUidSource,
        /// Signal collection deletion barrier when this operation completes.
        /// Only meaningful if some form of auto-resolution is in use.
        on_complete: Option<DeletionCompletionHandle>,
    },
}

impl ItemOp {
    /// Returns the wait handle that must be awaited before executing this operation, if any.
    #[must_use]
    pub fn wait_handle(&self) -> Option<&WaitHandle> {
        match self {
            ItemOp::Write(inner) => inner.mapping_uid.wait_handle(),
            ItemOp::StatusOnly(inner) => inner.mapping_uid.wait_handle(),
            _ => None,
        }
    }

    /// Returns the item UID for this operation, if applicable.
    #[must_use]
    pub fn uid(&self) -> Option<&str> {
        match self {
            ItemOp::Write(inner) => Some(&inner.source.state.uid),
            ItemOp::Delete(inner) => Some(&inner.uid),
            ItemOp::StatusOnly(inner) => match &inner.action {
                StatusAction::Insert { uid, .. } | StatusAction::Clear { uid } => Some(uid),
                StatusAction::Update { .. } => None,
            },
            ItemOp::Conflict { info, .. } => Some(&info.a.state.uid),
        }
    }

    /// Returns the mapping UID source for this operation.
    #[must_use]
    pub fn mapping_uid(&self) -> MappingUidSource {
        match self {
            ItemOp::Write(inner) => inner.mapping_uid.clone(),
            ItemOp::Delete(inner) => inner.mapping_uid.clone(),
            ItemOp::StatusOnly(inner) => inner.mapping_uid.clone(),
            ItemOp::Conflict { mapping_uid, .. } => mapping_uid.clone(),
        }
    }
}

/// Property-level synchronisation operation.
#[derive(Debug, Clone)]
pub struct PropertyOp {
    pub property: Property,
    pub mapping: Arc<ResolvedMapping>,
    pub mapping_uid: MappingUidSource,
    /// Optional handle to signal collection deletion barrier when this operation completes.
    pub on_complete: Option<DeletionCompletionHandle>,
    pub kind: PropertyOpKind,
}

/// The specific kind of property operation.
#[derive(Debug, Clone)]
pub enum PropertyOpKind {
    /// Write property to one side.
    Write { value: String, side: Side },

    /// Delete property from one side.
    Delete { side: Side },

    /// Clear property from status database.
    ClearStatus,

    /// Update property in status database.
    UpdateStatus { value: String },

    /// Property has conflicting values.
    Conflict { value_a: String, value_b: String },
}

impl std::fmt::Display for CollectionOp {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CollectionOp::SaveToStatus { mapping, .. } => {
                write!(f, "save collection '{}' to status", mapping.alias())
            }
            CollectionOp::CreateInOne { mapping, side, .. } => {
                let alias = mapping.alias();
                write!(f, "create collection '{alias}' in storage {side}",)
            }
            CollectionOp::CreateInBoth { mapping, .. } => {
                let alias = mapping.alias();
                write!(f, "create collection '{alias}' in both storages",)
            }
            CollectionOp::Delete { mapping, side, .. } => {
                let alias = mapping.alias();
                write!(f, "delete collection '{alias}' from storage {side}",)
            }
            CollectionOp::StoreSyncToken {
                mapping_uid, side, ..
            } => {
                write!(
                    f,
                    "store sync token for collection (uid: {mapping_uid:?}) side {side}"
                )
            }
        }
    }
}

impl std::fmt::Display for ItemOp {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ItemOp::Write(inner) => {
                let side = inner.target_side;
                match &inner.storage_write {
                    StorageWrite::Create { .. } => write!(f, "write to {side} (create new)"),
                    StorageWrite::Update { .. } => write!(f, "write to {side} (update existing)"),
                }
            }
            ItemOp::Delete(inner) => {
                write!(f, "delete from {}", inner.side)
            }
            ItemOp::StatusOnly(_) => {
                write!(f, "status only")
            }
            ItemOp::Conflict { .. } => {
                write!(f, "conflict")
            }
        }
    }
}

impl std::fmt::Display for PropertyOp {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.kind {
            PropertyOpKind::Write { value, side } => {
                write!(f, "write to {side} (value: {value:?})")
            }
            PropertyOpKind::Delete { side } => {
                write!(f, "delete from {side}")
            }
            PropertyOpKind::ClearStatus => {
                write!(f, "clear from status")
            }
            PropertyOpKind::UpdateStatus { value } => {
                write!(f, "update in status (value: {value:?})")
            }
            PropertyOpKind::Conflict { value_a, value_b } => {
                write!(f, "conflict (a: {value_a:?}, b: {value_b:?})")
            }
        }
    }
}