vstorage 0.7.0

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

//! Types for specifying rules for a synchronisation.
use std::sync::Arc;

use crate::{
    CollectionId, Href,
    base::Storage,
    sync::{TwoWaySync, mode::Mode},
};

/// Collection declared either via its `href` or `collection_id`.
///
/// A user-configured collection, which may or may not exist.
#[derive(Debug, Clone)]
pub enum CollectionDescription {
    /// Refers to a collection with a matching collection id.
    ///
    /// If it does not exist, a collection will be created with the expectation that discovery
    /// would find it with this collection id.
    Id {
        /// The id for the declared collection.
        id: CollectionId,
    },
    /// Refers to a collection with a matching `href`.
    ///
    /// If it does not exist, a collection with this exact `href` shall be created.
    Href {
        /// The href for the declared collection.
        href: Href,
    },
}

impl CollectionDescription {
    pub(crate) fn alias(&self) -> String {
        match self {
            CollectionDescription::Id { id } => id.to_string(),
            CollectionDescription::Href { href } => format!("href:{href}"),
        }
    }
}

/// A pair of collections to be synchronised between a pair of storages.
///
/// As declared by a user, which may be lacking information on one side.
#[derive(Debug, Clone)]
pub enum SyncedCollection {
    /// Copy between two collections with the same definition on both sides.
    ///
    /// Usage of [`CollectionDescription::Href`] between different storage implementations is
    /// discouraged.
    Direct {
        /// Description which applies to the collection on both sides.
        description: CollectionDescription,
    },
    /// Copy between two collections with explicit definitions on both sides.
    Mapped {
        /// A descriptive name used for logging and display.
        alias: String,
        /// The description for the collection on side `a`.
        a: CollectionDescription,
        /// The description for the collection on side `b`.
        b: CollectionDescription,
    },
}

impl SyncedCollection {
    /// Create a direct collection mapping.
    ///
    /// Creates the simplest kind of mapping: it maps two collections with the same
    /// [`CollectionId`].
    #[must_use]
    pub fn direct(id: CollectionId) -> Self {
        SyncedCollection::Direct {
            description: CollectionDescription::Id { id },
        }
    }
}

/// A pair of storage that are to be synchronised.
///
/// Merely wraps around the declaration of what shall be synchronised. It can be
/// constructed offline and is the entry point to generate a stream of operations via
/// [`Plan::new`](super::plan::Plan::new) and then execute them.
///
/// New pairs can be created via [`StoragePair::new`].
#[derive(Clone)]
pub struct StoragePair {
    pub(super) storage_a: Arc<dyn Storage>,
    pub(super) storage_b: Arc<dyn Storage>,
    pub(super) mappings: Arc<Vec<SyncedCollection>>,
    pub(super) all_from_a: bool,
    pub(super) all_from_b: bool,
    pub(super) on_empty: OnEmpty,
    pub(super) on_delete: OnDelete,
    pub(super) mode: Arc<dyn Mode>,
}

impl StoragePair {
    /// Create a new instance.
    ///
    /// By default, no collections are to be synchronised. See other associated functions for
    /// details con configuring additional collections.
    #[must_use]
    pub fn new(storage_a: Arc<dyn Storage>, storage_b: Arc<dyn Storage>) -> StoragePair {
        StoragePair {
            storage_a,
            storage_b,
            mappings: Arc::default(),
            all_from_a: false,
            all_from_b: false,
            on_empty: OnEmpty::Skip,
            on_delete: OnDelete::Sync,
            mode: Arc::new(TwoWaySync),
        }
    }

    /// Include the specified mapping when synchronising.
    #[must_use]
    pub fn with_mapping(mut self, mapping: SyncedCollection) -> Self {
        // Cloning should never happen in typical usage.
        let mut mappings = Arc::unwrap_or_clone(self.mappings);
        mappings.push(mapping);
        self.mappings = Arc::from(mappings);
        self
    }

    /// Include all collections from storage A when synchronising.
    ///
    /// By default, only explicitly included collections are synchronised.
    #[must_use]
    pub fn with_all_from_a(mut self) -> Self {
        self.all_from_a = true;
        self
    }

    /// Include all collections from storage B when synchronising.
    ///
    /// By default, only explicitly included collections are synchronised.
    #[must_use]
    pub fn with_all_from_b(mut self) -> Self {
        self.all_from_b = true;
        self
    }

    /// Returns a reference to storage a.
    #[must_use]
    pub fn storage_a(&self) -> &Arc<dyn Storage> {
        &self.storage_a
    }

    /// Returns a reference to storage a.
    #[must_use]
    pub fn storage_b(&self) -> &Arc<dyn Storage> {
        &self.storage_b
    }

    /// Action to take when a collection is completely emptied.
    #[must_use]
    pub fn on_empty(mut self, action: OnEmpty) -> Self {
        self.on_empty = action;
        self
    }

    /// Action to take when a collection is deleted.
    #[must_use]
    pub fn on_delete(mut self, action: OnDelete) -> Self {
        self.on_delete = action;
        self
    }

    /// Set the synchronization mode.
    ///
    /// By default, [`TwoWaySync`] is used.
    #[must_use]
    pub fn with_mode(mut self, mode: Arc<dyn Mode>) -> Self {
        self.mode = mode;
        self
    }
}

/// Action to take when a collection has been emptied on one side.
#[derive(Debug, PartialEq, Default, Clone, Copy)]
pub enum OnEmpty {
    /// Skip synchronising this pair of collecitons.
    #[default]
    Skip,
    /// Synchronise changes (e.g.: emptying the other side too).
    Sync,
}

/// Action to take when a collection has been deleted on one side.
#[derive(Debug, PartialEq, Default, Clone, Copy)]
pub enum OnDelete {
    /// Skip synchronising this collection.
    Skip,
    /// Synchronising the deletion.
    ///
    /// Note that only empty collections are deleted, so the collection on the opposite side will
    /// not be deleted if it is non-empty.
    #[default]
    Sync,
}