vstorage 0.7.0

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

//! Property operation planning.

use std::{
    collections::{HashMap, HashSet},
    sync::Arc,
};

use crate::{
    ErrorKind,
    property::Property,
    sync::{
        analysis::{PropertyChange, ResolvedMapping},
        declare::StoragePair,
        mode::Mode,
        operation::PropertyOp,
        ordering::DeletionBarrier,
        plan::PlanError,
        status::{MappingUid, StatusDatabase},
    },
};

/// Detect the state change for a property.
///
/// Pure state comparison without making decisions about what operations to perform.
pub(crate) fn detect_property_change(
    property: Property,
    value_a: Option<String>,
    value_b: Option<String>,
    previous: Option<String>,
) -> Option<PropertyChange> {
    match (value_a, value_b, previous) {
        (None, None, None) => None,
        (None, None, Some(_)) => Some(PropertyChange::OnNeither { property }),
        (None, Some(value_b), previous) => Some(PropertyChange::OnlyB {
            property,
            value_b,
            previous,
        }),
        (Some(value_a), None, previous) => Some(PropertyChange::OnlyA {
            property,
            value_a,
            previous,
        }),
        (Some(value_a), Some(value_b), previous) => Some(PropertyChange::OnBoth {
            property,
            value_a,
            value_b,
            previous,
        }),
    }
}

/// Load properties and create property operations using mode-based approach.
pub(crate) async fn load_and_create_property_operations(
    mapping: &Arc<ResolvedMapping>,
    mapping_uid: Option<MappingUid>,
    pair: &StoragePair,
    status: Option<&StatusDatabase>,
    deletion_completion: Option<&DeletionBarrier>,
    mode: &dyn Mode,
) -> Result<Vec<PropertyOp>, PlanError> {
    let (props_a, props_b) = match tokio::try_join!(
        pair.storage_a().list_properties(mapping.a().href()),
        pair.storage_b().list_properties(mapping.b().href()),
    ) {
        Ok((a, b)) => (a, b),
        Err(error) => {
            // If storage doesn't support properties or doesn't exist, return empty
            return if let ErrorKind::DoesNotExist | ErrorKind::Unsupported = error.kind {
                Ok(Vec::new())
            } else {
                Err(error.into())
            };
        }
    };

    let props_status = match (status, mapping_uid) {
        (Some(s), Some(u)) => s.list_properties_for_collection(u)?,
        _ => Vec::with_capacity(0),
    };

    // FIXME: properties require mapping_uid. Should require MappingUidSource.
    let Some(mapping_uid) = mapping_uid else {
        return Ok(Vec::new());
    };

    // Convert to owned collections for easier manipulation
    // FIXME: a lot of pointless work.
    let mut values_a: HashMap<Property, String> =
        props_a.into_iter().map(|p| (p.property, p.value)).collect();
    let mut values_b: HashMap<Property, String> =
        props_b.into_iter().map(|p| (p.property, p.value)).collect();
    let mut previous_values: HashMap<String, String> = props_status
        .into_iter()
        .map(|p| (p.property, p.value))
        .collect();

    // Collect all unique properties
    let all_props: HashSet<Property> = values_a.keys().chain(values_b.keys()).copied().collect();

    let operations: Vec<PropertyOp> = all_props
        .into_iter()
        .filter_map(|property: Property| {
            let from_a = values_a.remove(&property);
            let from_b = values_b.remove(&property);
            let from_status = previous_values.remove(property.name());

            detect_property_change(property, from_a, from_b, from_status)
        })
        .filter_map(|change| {
            mode.decide_property_action(change, mapping, mapping_uid, deletion_completion)
        })
        .collect();

    Ok(operations)
}