vstorage 0.6.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::{DeleteB, ItemOp, MappingUidSource, PropertyOp, StatusOnly, WriteInB, WriteMode},
    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 deletion_completion = on_complete.map(DeletionBarrier::completion_handle);

        match change {
            PropertyChange::OnNeither { property, .. } => Some(PropertyOp::ClearStatus {
                property,
                mapping: mapping.clone(),
                mapping_uid: MappingUidSource::Immediate(mapping_uid),
                deletion_completion,
            }),

            PropertyChange::OnlyA {
                property, value_a, ..
            } => Some(PropertyOp::Write {
                property,
                value: value_a,
                side: Side::B,
                mapping: mapping.clone(),
                mapping_uid: MappingUidSource::Immediate(mapping_uid),
                deletion_completion,
            }),

            PropertyChange::OnlyB { property, .. } => Some(PropertyOp::Delete {
                property,
                side: Side::B,
                mapping: mapping.clone(),
                mapping_uid: MappingUidSource::Immediate(mapping_uid),
                deletion_completion,
            }),

            PropertyChange::OnBoth {
                property,
                value_a,
                value_b,
                previous,
            } => {
                if value_a == value_b {
                    match previous {
                        Some(prev) if prev == value_a => None, // No change needed
                        _ => Some(PropertyOp::UpdateStatus {
                            property,
                            value: value_a,
                            mapping: mapping.clone(),
                            mapping_uid: MappingUidSource::Immediate(mapping_uid),
                            deletion_completion,
                        }),
                    }
                } else {
                    Some(PropertyOp::Write {
                        property,
                        value: value_a,
                        side: Side::B,
                        mapping: mapping.clone(),
                        mapping_uid: MappingUidSource::Immediate(mapping_uid),
                        deletion_completion,
                    })
                }
            }
        }
    }
}

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 } => ItemOp::WriteInB(WriteInB {
                source_a: ItemSource::from(a),
                mode: WriteMode::CreateNew {
                    collection: mapping.b().href().clone(),
                },
                mapping_uid: mapping_uid_source,
                on_complete,
            }),
            NewItem::OnlyB { b } => ItemOp::DeleteB(DeleteB {
                uid: b.state.uid.clone(),
                target_b: b.state.version,
                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::WriteInB(WriteInB {
                        source_a: ItemSource::from(a),
                        mode: WriteMode::UpdateNew {
                            target: b.state.version,
                        },
                        mapping_uid: mapping_uid_source,
                        on_complete,
                    })
                }
            }
        }
    }

    /// Plan operations for an existing item (has previous status entry).
    /// One-way sync: A always wins.
    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 (state, data) = match a {
                    SideState::Changed { state, data } => (state, Some(data)),
                    SideState::Unchanged { state } => (state, None),
                };
                Some(ItemOp::WriteInB(WriteInB {
                    source_a: ItemSource { state, data },
                    mode: WriteMode::CreateRefresh {
                        collection: mapping.b().href().clone(),
                        old: status.into(),
                    },
                    mapping_uid,
                    on_complete,
                }))
            }
            ExistingItemPresence::OnlyB { b } => Some(ItemOp::DeleteB(DeleteB {
                uid: b.state().uid.clone(),
                target_b: b.into_item_ver(),
                mapping_uid,
                on_complete,
            })),
            ExistingItemPresence::OnBoth { a, b } => match (a, b) {
                // A unchanged, B unchanged: no-op if B version matches status
                (SideState::Unchanged { state }, SideState::Unchanged { state: b }) => {
                    let ver_b = b.version;
                    if ver_b == status.b {
                        None
                    } else {
                        Some(ItemOp::WriteInB(WriteInB {
                            source_a: ItemSource { state, data: None },
                            mode: WriteMode::UpdateExisting { old: status.into() },
                            mapping_uid,
                            on_complete,
                        }))
                    }
                }

                // A unchanged, B changed: overwrite B with A.
                (SideState::Unchanged { state }, SideState::Changed { state: b, .. }) => {
                    Some(ItemOp::WriteInB(WriteInB {
                        source_a: ItemSource { state, data: None },
                        mode: WriteMode::UpdateForce {
                            target: b.version,
                            old: status.into(),
                        },
                        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,
                })),

                (SideState::Changed { state, data }, SideState::Unchanged { .. }) => {
                    Some(ItemOp::WriteInB(WriteInB {
                        source_a: ItemSource {
                            state,
                            data: Some(data),
                        },
                        mode: WriteMode::UpdateExisting { old: status.into() },
                        mapping_uid,
                        on_complete,
                    }))
                }

                (SideState::Changed { state, data }, SideState::Changed { state: b, .. }) => {
                    Some(ItemOp::WriteInB(WriteInB {
                        source_a: ItemSource {
                            state,
                            data: Some(data),
                        },
                        mode: WriteMode::UpdateForce {
                            target: b.version,
                            old: status.into(),
                        },
                        mapping_uid,
                        on_complete,
                    }))
                }
            },
        }
    }
}