vstorage 0.7.0

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

use std::sync::Arc;

use crate::sync::{mode::Mode, status::StatusVersions};

use super::{
    analysis::{
        ExistingItem, ExistingItemPresence, ItemAnalysis, ItemSource, NewItem, PropertyChange,
        ResolvedMapping, SideState, StatusAction,
    },
    operation::{
        DeleteItem, ItemOp, MappingUidSource, PropertyOp, PropertyOpKind, StatusOnly, StatusWrite,
        StorageWrite, WriteItem, resource_name,
    },
    ordering::{DeletionBarrier, DeletionCompletionHandle},
    status::{MappingUid, Side},
};

/// One-way synchronisation mode (A -> B).
///
/// Never conflicts occur as A always wins.
pub struct OneWaySync;

impl Mode for OneWaySync {
    fn decide_item_action(
        &self,
        analysis: ItemAnalysis,
        mapping: &Arc<ResolvedMapping>,
        mapping_uid_source: MappingUidSource,
        on_complete: Option<&DeletionBarrier>,
    ) -> Option<ItemOp> {
        let on_complete_handle = on_complete.map(DeletionBarrier::completion_handle);

        match analysis {
            ItemAnalysis::New(new_item) => Some(Self::plan_new_item(
                new_item,
                mapping,
                mapping_uid_source,
                on_complete_handle,
            )),
            ItemAnalysis::Existing(existing) => {
                Self::plan_existing_item(existing, mapping, on_complete_handle)
            }
        }
    }

    fn decide_property_action(
        &self,
        change: PropertyChange,
        mapping: &Arc<ResolvedMapping>,
        mapping_uid: MappingUid,
        on_complete: Option<&DeletionBarrier>,
    ) -> Option<PropertyOp> {
        let on_complete = on_complete.map(DeletionBarrier::completion_handle);
        let mapping = mapping.clone();
        let mapping_uid = MappingUidSource::Immediate(mapping_uid);

        let (property, kind) = match change {
            PropertyChange::OnNeither { property, .. } => (property, PropertyOpKind::ClearStatus),
            PropertyChange::OnlyA {
                property, value_a, ..
            } => (
                property,
                PropertyOpKind::Write {
                    value: value_a,
                    side: Side::B,
                },
            ),
            PropertyChange::OnlyB { property, .. } => {
                (property, PropertyOpKind::Delete { side: Side::B })
            }
            PropertyChange::OnBoth {
                property,
                value_a,
                value_b,
                previous,
            } => {
                if value_a == value_b {
                    return match previous {
                        Some(prev) if prev == value_a => None, // No change needed
                        _ => Some(PropertyOp {
                            property,
                            mapping,
                            mapping_uid,
                            on_complete,
                            kind: PropertyOpKind::UpdateStatus { value: value_a },
                        }),
                    };
                }
                (
                    property,
                    PropertyOpKind::Write {
                        value: value_a,
                        side: Side::B,
                    },
                )
            }
        };

        Some(PropertyOp {
            property,
            mapping,
            mapping_uid,
            on_complete,
            kind,
        })
    }
}

impl OneWaySync {
    /// Plan operations for a new item (no previous status entry).
    fn plan_new_item(
        item: NewItem,
        mapping: &Arc<ResolvedMapping>,
        mapping_uid_source: MappingUidSource,
        on_complete: Option<DeletionCompletionHandle>,
    ) -> ItemOp {
        match item {
            NewItem::OnlyA { a } => {
                let name = resource_name(&a.state.version.href);
                ItemOp::Write(WriteItem {
                    source: ItemSource::from(a),
                    target_side: Side::B,
                    storage_write: StorageWrite::Create {
                        collection: mapping.b().href().clone(),
                        resource_name: Some(name),
                    },
                    status_write: StatusWrite::Insert,
                    mapping_uid: mapping_uid_source,
                    on_complete,
                })
            }
            NewItem::OnlyB { b } => ItemOp::Delete(DeleteItem {
                uid: b.state.uid.clone(),
                target: b.state.version,
                side: Side::B,
                mapping_uid: mapping_uid_source,
                on_complete,
            }),
            NewItem::OnBoth { a, b } => {
                if a.state.hash == b.state.hash {
                    ItemOp::StatusOnly(StatusOnly {
                        action: StatusAction::Insert {
                            uid: a.state.uid.clone(),
                            hash: a.state.hash.clone(),
                            versions: StatusVersions {
                                a: a.state.version,
                                b: b.state.version,
                            },
                        },
                        mapping_uid: mapping_uid_source,
                        on_complete,
                    })
                } else {
                    ItemOp::Write(WriteItem {
                        source: ItemSource::from(a),
                        target_side: Side::B,
                        storage_write: StorageWrite::Update {
                            target: b.state.version,
                        },
                        status_write: StatusWrite::Insert,
                        mapping_uid: mapping_uid_source,
                        on_complete,
                    })
                }
            }
        }
    }

    /// Plan operations for an existing item (has previous status entry).
    /// One-way sync: A always wins.
    #[allow(clippy::too_many_lines)]
    fn plan_existing_item(
        item: ExistingItem,
        mapping: &Arc<ResolvedMapping>,
        on_complete: Option<DeletionCompletionHandle>,
    ) -> Option<ItemOp> {
        let mapping_uid = MappingUidSource::Immediate(item.mapping_uid());
        let status = item.status;

        match item.current {
            ExistingItemPresence::OnNeither => Some(ItemOp::StatusOnly(StatusOnly {
                action: StatusAction::Clear { uid: status.uid },
                mapping_uid,
                on_complete,
            })),
            ExistingItemPresence::OnlyA { a } => {
                let name = resource_name(&a.state().version.href);
                Some(ItemOp::Write(WriteItem {
                    source: a.into_item_source(),
                    target_side: Side::B,
                    storage_write: StorageWrite::Create {
                        collection: mapping.b().href().clone(),
                        resource_name: Some(name),
                    },
                    status_write: StatusWrite::Update { old: status.into() },
                    mapping_uid,
                    on_complete,
                }))
            }
            ExistingItemPresence::OnlyB { b } => Some(ItemOp::Delete(DeleteItem {
                uid: b.state().uid.clone(),
                target: b.into_item_ver(),
                side: Side::B,
                mapping_uid,
                on_complete,
            })),
            ExistingItemPresence::OnBoth { a, b } => match (a, b) {
                // A unchanged, B unchanged: no-op if B version matches status
                (a @ SideState::Unchanged { .. }, SideState::Unchanged { state: b }) => {
                    if b.version == status.b {
                        None
                    } else {
                        let old: StatusVersions = status.into();
                        let target = old.b.clone();
                        Some(ItemOp::Write(WriteItem {
                            source: a.into_item_source(),
                            target_side: Side::B,
                            storage_write: StorageWrite::Update { target },
                            status_write: StatusWrite::Update { old },
                            mapping_uid,
                            on_complete,
                        }))
                    }
                }

                // A changed, same hash as B: status-only update
                (
                    SideState::Changed { state: a, .. },
                    SideState::Changed { state: b, .. } | SideState::Unchanged { state: b },
                ) if a.hash == b.hash => Some(ItemOp::StatusOnly(StatusOnly {
                    action: StatusAction::Update {
                        hash: a.hash.clone(),
                        old: status.into(),
                        new: StatusVersions {
                            a: a.version,
                            b: b.version,
                        },
                    },
                    mapping_uid,
                    on_complete,
                })),

                // B changed: overwrite B with A.
                (
                    a @ (SideState::Unchanged { .. } | SideState::Changed { .. }),
                    SideState::Changed { state: b, .. },
                ) => Some(ItemOp::Write(WriteItem {
                    source: a.into_item_source(),
                    target_side: Side::B,
                    storage_write: StorageWrite::Update { target: b.version },
                    status_write: StatusWrite::Update { old: status.into() },
                    mapping_uid,
                    on_complete,
                })),

                (a @ SideState::Changed { .. }, SideState::Unchanged { .. }) => {
                    let old: StatusVersions = status.into();
                    let target = old.b.clone();
                    Some(ItemOp::Write(WriteItem {
                        source: a.into_item_source(),
                        target_side: Side::B,
                        storage_write: StorageWrite::Update { target },
                        status_write: StatusWrite::Update { old },
                        mapping_uid,
                        on_complete,
                    }))
                }
            },
        }
    }
}