vstorage 0.6.0

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

//! Item analysis types.

use crate::{
    base::{FetchedItem, Item, ItemHash, ItemVersion},
    sync::status::{ItemPairStatus, ItemState, MappingUid, StatusVersions},
};

/// Item state with data present.
#[derive(PartialEq, Clone)]
pub struct ItemWithData {
    /// Metadata and state for this item.
    pub state: ItemState,
    /// Data for this version of the item.
    pub data: Item,
}

/// Item with optional data (may need fetching).
#[derive(Clone, PartialEq, Debug)]
pub struct ItemSource {
    /// Metadata and state for this item.
    pub state: ItemState,
    /// Data for this version of the item, if available.
    pub data: Option<Item>,
}

impl From<ItemWithData> for ItemSource {
    fn from(item: ItemWithData) -> Self {
        ItemSource {
            state: item.state,
            data: Some(item.data),
        }
    }
}

impl ItemSource {
    /// Convert to `ItemVersion` for the item state by cloning.
    #[must_use]
    pub fn to_item_ver(&self) -> ItemVersion {
        self.state.version.clone()
    }
}

impl std::fmt::Debug for ItemWithData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ItemWithData")
            .field("state", &self.state)
            .field("data", &"<omitted>")
            .finish()
    }
}

/// Complete analysis of an item across both sides and status.
///
/// Encodes whether an item is new (never synced) or existing (previously synced).
pub enum ItemAnalysis {
    /// Item is new (no previous status entry).
    New(NewItem),
    /// Item exists in status (previously synchronized).
    Existing(ExistingItem),
}

/// New item that has never been synchronized before.
///
/// Uses [`ItemWithData`] because data MUST be fetched; there's no status to
/// compare etag, so we can't know if it's unchanged.
pub enum NewItem {
    /// New item exists only on side A.
    OnlyA { a: ItemWithData },
    /// New item exists only on side B.
    OnlyB { b: ItemWithData },
    /// New item exists on both sides (e.g., created independently).
    OnBoth { a: ItemWithData, b: ItemWithData },
}

/// Existing item which was previously synchronized.
pub struct ExistingItem {
    /// The previous sync status (always present for existing items).
    pub status: ItemPairStatus,
    /// Current presence on each side.
    pub current: ExistingItemPresence,
}

impl ExistingItem {
    /// Create from current side states and status.
    #[must_use]
    pub fn new(
        current_a: Option<SideState>,
        current_b: Option<SideState>,
        status: ItemPairStatus,
    ) -> ExistingItem {
        let current = match (current_a, current_b) {
            (None, None) => ExistingItemPresence::OnNeither,
            (Some(a), None) => ExistingItemPresence::OnlyA { a },
            (None, Some(b)) => ExistingItemPresence::OnlyB { b },
            (Some(a), Some(b)) => ExistingItemPresence::OnBoth { a, b },
        };
        ExistingItem { status, current }
    }

    /// Get the mapping UID from status.
    #[must_use]
    pub fn mapping_uid(&self) -> MappingUid {
        self.status.mapping_uid
    }
}

/// Current presence of an existing item on each side.
///
/// Uses [`SideState`] because data may be skipped if etag matched status.
pub enum ExistingItemPresence {
    /// Item deleted from both sides.
    OnNeither,
    /// Item exists only on side A (deleted from B).
    OnlyA { a: SideState },
    /// Item exists only on side B (deleted from A).
    OnlyB { b: SideState },
    /// Item exists on both sides.
    OnBoth { a: SideState, b: SideState },
}

/// Current state of an item on one side (for existing items only).
#[derive(Clone, Debug)]
pub enum SideState {
    /// Item changed from status (data was fetched).
    Changed { state: ItemState, data: Item },
    /// Item unchanged from status (data not fetched).
    Unchanged { state: ItemState },
}

impl SideState {
    /// Create changed status from a fetched item (changed or new).
    #[must_use]
    pub fn from_fetched(FetchedItem { href, item, etag }: FetchedItem) -> Self {
        Self::Changed {
            state: ItemState {
                version: ItemVersion::new(href, etag),
                uid: item.ident(),
                hash: item.hash(),
            },
            data: item,
        }
    }

    /// Create for an unchanged item (data not fetched).
    #[must_use]
    pub fn unchanged(state: ItemState) -> Self {
        Self::Unchanged { state }
    }

    /// Get the item state.
    #[must_use]
    pub fn state(&self) -> &ItemState {
        match self {
            Self::Changed { state, .. } | Self::Unchanged { state } => state,
        }
    }

    /// Convert to `ItemVersion` without copying.
    #[must_use]
    pub fn into_item_ver(self) -> ItemVersion {
        match self {
            SideState::Changed { state, .. } | SideState::Unchanged { state } => state.version,
        }
    }
}

/// Actions that only affect the status database.
#[derive(PartialEq, Debug, Clone)]
pub enum StatusAction {
    /// Item is new and identical on both sides.
    Insert {
        uid: String,
        hash: ItemHash,
        versions: StatusVersions,
    },
    /// Item has changed on both sides but remains in sync.
    Update {
        hash: ItemHash,
        old: StatusVersions,
        new: StatusVersions,
    },
    /// Item is gone from both sides but still present in status db.
    Clear { uid: String },
}

impl std::fmt::Display for StatusAction {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            StatusAction::Insert { uid, .. } => write!(f, "save to status (uid: {uid})"),
            StatusAction::Update { old, .. } => {
                write!(f, "update in status (a.href: {})", old.a.href)
            }
            StatusAction::Clear { uid } => write!(f, "clear from status (uid: {uid})"),
        }
    }
}