vstorage 0.7.0

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

//! Resolve conflicts during synchronisation.

use std::sync::Arc;

use crate::{
    Href,
    property::Property,
    sync::{
        analysis::{ItemWithData, ResolvedMapping},
        operation::{
            ItemOp, MappingUidSource, PropertyOp, PropertyOpKind, StatusWrite, StorageWrite,
            WriteItem,
        },
        status::{MappingUid, Side, StatusVersions},
    },
};

pub use super::plan::resolve_conflicts;

/// Conflict information for items that have diverged.
#[derive(PartialEq, Debug, Clone)]
pub struct ConflictInfo {
    pub a: ItemWithData,
    pub b: ItemWithData,
    pub old: Option<StatusVersions>,
    pub collection_a: Href,
    pub collection_b: Href,
}

/// Trait for resolving conflicts in operation streams.
pub trait ConflictResolver: Send + Sync {
    /// Resolve an item conflict, returning the resolved operation.
    ///
    /// The returned operation must be an [`ItemOp`] variant that performs
    /// the appropriate sync action  based on the resolution strategy.
    fn resolve_item(&self, conflict: ConflictInfo, mapping_uid: MappingUid) -> ItemOp;

    /// Resolve a property conflict.
    ///
    /// The returned operation must be a `PropertyOp` variant that writes
    /// or deletes the property based on the resolution strategy.
    fn resolve_property(
        &self,
        property: Property,
        value_a: String,
        value_b: String,
        mapping: Arc<ResolvedMapping>,
        mapping_uid: MappingUid,
    ) -> PropertyOp;
}

/// Conflict resolver that always keeps the specified side.
///
/// Treats the given side as the authoritative source and updates the other side to match it.
#[derive(Debug, Clone, Copy)]
pub struct KeepSideResolver(pub Side);

impl ConflictResolver for KeepSideResolver {
    fn resolve_item(&self, conflict: ConflictInfo, mapping_uid: MappingUid) -> ItemOp {
        let (source, target_version, target_side) = match self.0 {
            Side::A => (conflict.a, conflict.b.state.version, Side::B),
            Side::B => (conflict.b, conflict.a.state.version, Side::A),
        };
        let (storage_write, status_write) = match conflict.old {
            Some(old) => (
                StorageWrite::Update {
                    target: old.for_side(target_side).clone(),
                },
                StatusWrite::Update { old },
            ),
            None => (
                StorageWrite::Update {
                    target: target_version,
                },
                StatusWrite::Insert,
            ),
        };
        ItemOp::Write(WriteItem {
            source: source.into(),
            target_side,
            storage_write,
            status_write,
            mapping_uid: MappingUidSource::Immediate(mapping_uid),
            on_complete: None,
        })
    }

    fn resolve_property(
        &self,
        property: Property,
        value_a: String,
        value_b: String,
        mapping: Arc<ResolvedMapping>,
        mapping_uid: MappingUid,
    ) -> PropertyOp {
        let (value, side) = match self.0 {
            Side::A => (value_a, Side::B),
            Side::B => (value_b, Side::A),
        };
        PropertyOp {
            property,
            mapping,
            mapping_uid: MappingUidSource::Immediate(mapping_uid),
            on_complete: None,
            kind: PropertyOpKind::Write { value, side },
        }
    }
}