git-bug 0.2.4

A rust library for interfacing with git-bug repositories
Documentation
// git-bug-rs - A rust library for interfacing with git-bug repositories
//
// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of git-bug-rs/git-gub.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/agpl.txt>.

//! A collection of multiple [`Operations`][`super::Operation`].
//! This ensures that invalid sequences of [`Operations`][`super::Operation`]
//! are not only invalid but completely unrepresentable.

use std::iter::{Once, once};

use serde::{Deserialize, Serialize};

use super::{Operation, operation_data::OperationData};
use crate::replica::entity::Entity;

/// A collection of multiple [`Operations`][`super::Operation`].
#[derive(Debug, Deserialize, Serialize)]
pub struct Operations<E: Entity> {
    #[serde(bound = "Operation<E>: serde::Serialize + serde::de::DeserializeOwned")]
    root_operation: Operation<E>,

    #[serde(bound = "Vec<Operation<E>>: serde::Serialize + serde::de::DeserializeOwned")]
    other_operations: Vec<Operation<E>>,
}

#[allow(missing_docs)]
pub mod create {
    use crate::replica::entity::{Entity, operation::Operation};

    #[derive(Debug, thiserror::Error)]
    pub enum Error<E: Entity> {
        #[error("Tried to collect an empty vector to an Operations structure")]
        EmptyOperations,

        #[error(
            "Tried to use an non root operation ({first}) as root for an Operations structure."
        )]
        OperationsFromNonRoot { first: Box<Operation<E>> },
    }
}

impl<E: Entity> Operations<E> {
    /// Create a new [`Operations`] from a simple vector of single Operations.
    ///
    /// # Errors
    /// - If the first operation in `ops` is not actually a root operation (see
    ///   [`OperationData::is_root`] for how we determine this.)
    /// - If the `ops` are empty.
    // We use only expects.
    #[allow(clippy::missing_panics_doc)]
    pub fn from_operations(mut ops: Vec<Operation<E>>) -> Result<Self, create::Error<E>> {
        if ops.is_empty() {
            return Err(create::Error::EmptyOperations);
        }

        if !ops
            .first()
            .expect("Exists, as the operations are not empty")
            .data
            .is_root()
        {
            return Err(create::Error::OperationsFromNonRoot {
                first: Box::new(ops.remove(0)),
            });
        }

        Ok(Self {
            root_operation: ops.remove(0),
            other_operations: ops,
        })
    }

    /// Return the root operation.
    pub fn root(&self) -> &Operation<E> {
        &self.root_operation
    }
}

/// Treat this collection of operations as a vector.
impl<T: Entity> Operations<T> {
    /// Delegate for [`Vec<T>::len`].
    #[must_use]
    // Operations will never be empty, thus no is_empty method.
    #[allow(clippy::len_without_is_empty)]
    pub fn len(&self) -> usize {
        self.other_operations.len() + 1
    }

    /// Delegate for `iter` on the underlying [`Vec`] .
    #[must_use]
    pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
        self.into_iter()
    }
}

impl<E: Entity> IntoIterator for Operations<E> {
    type IntoIter =
        std::iter::Chain<Once<Self::Item>, <Vec<Operation<E>> as IntoIterator>::IntoIter>;
    type Item = <Vec<Operation<E>> as IntoIterator>::Item;

    fn into_iter(self) -> Self::IntoIter {
        once(self.root_operation).chain(self.other_operations)
    }
}
impl<'a, E: Entity> IntoIterator for &'a Operations<E> {
    type IntoIter =
        std::iter::Chain<Once<Self::Item>, <&'a Vec<Operation<E>> as IntoIterator>::IntoIter>;
    type Item = <&'a Vec<Operation<E>> as IntoIterator>::Item;

    fn into_iter(self) -> Self::IntoIter {
        once(&self.root_operation).chain(self.other_operations.iter())
    }
}