vstorage 0.6.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;
use crate::base::Property;
use crate::sync::analysis::{ItemWithData, ResolvedMapping};
use crate::sync::status::StatusVersions;

use super::operation::{ItemOp, MappingUidSource, PropertyOp, WriteInA, WriteInB, WriteMode};
use super::status::{MappingUid, Side};

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 side A.
///
/// Treat side A as the authoritative source and update side B to match it.
#[derive(Debug, Clone, Copy)]
pub struct KeepAResolver;

impl ConflictResolver for KeepAResolver {
    fn resolve_item(&self, conflict: ConflictInfo, mapping_uid: MappingUid) -> ItemOp {
        let ConflictInfo { a, b, old, .. } = conflict;
        let mode = match old {
            Some(old) => WriteMode::UpdateExisting { old },
            None => WriteMode::UpdateNew {
                target: b.state.version,
            },
        };
        ItemOp::WriteInB(WriteInB {
            source_a: a.into(),
            mode,
            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 {
        PropertyOp::Write {
            property,
            value: value_a,
            side: Side::B,
            mapping,
            mapping_uid: MappingUidSource::Immediate(mapping_uid),
            deletion_completion: None,
        }
    }
}

/// Conflict resolver that always keeps side B.
///
/// Treat side B as the authoritative source and update side A to match it.
#[derive(Debug, Clone, Copy)]
pub struct KeepBResolver;

impl ConflictResolver for KeepBResolver {
    fn resolve_item(&self, conflict: ConflictInfo, mapping_uid: MappingUid) -> ItemOp {
        let ConflictInfo { a, b, old, .. } = conflict;
        let mode = match old {
            Some(old) => WriteMode::UpdateExisting { old },
            None => WriteMode::UpdateNew {
                target: a.state.version,
            },
        };
        ItemOp::WriteInA(WriteInA {
            source_b: b.into(),
            mode,
            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 {
        PropertyOp::Write {
            property,
            value: value_b,
            side: Side::A,
            mapping,
            mapping_uid: MappingUidSource::Immediate(mapping_uid),
            deletion_completion: None,
        }
    }
}