eips 0.2.4

Efficient intention-preserving sequence CRDT
Documentation
/*
 * Copyright (C) 2025-2026 taylor.fish <contact@taylor.fish>
 *
 * This file is part of Eips.
 *
 * Eips is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Eips is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Eips. If not, see <https://www.gnu.org/licenses/>.
 *
 * The Eips Lesser Network Exception and Eips Peer-to-Peer Exception
 * also apply to Eips. These exceptions are additional permissions
 * under section 7 of the GNU Affero General Public License, version 3.
 * You should have received a copy of these exceptions; if not, see
 * <https://codeberg.org/taylordotfish/eips-exceptions>.
 */

//! Error types.

#[cfg(doc)]
use crate::options::EipsOptions;
use core::fmt::{self, Debug, Display};

/// An error encountered while [applying a remote change][apply].
///
/// [apply]: crate::Eips::apply_change
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub enum ChangeError<Id> {
    /// The remote change's parent ID was invalid.
    BadParentId(
        /// The invalid parent ID.
        Id,
    ),

    /// The remote change has no parent but its direction is
    /// [`Before`](crate::change::Direction::Before).
    BadDirection(
        /// The ID of the remote change.
        Id,
    ),

    /// The remote change corresponds to an existing item, but there was a
    /// conflict between the old and new data.
    MergeConflict(
        /// The ID of the remote change.
        Id,
    ),

    /// The remote change's ID is already in use, but with a different parent
    /// ID.
    DuplicateId(
        /// The ID of the remote change.
        Id,
    ),

    /// The remote change contains move information, but move operations are
    /// not supported.
    ///
    /// Remember that the value of [`EipsOptions::SupportsMove`] must be the
    /// same for all clients in a distributed system. Clients that support move
    /// operations cannot talk to clients that don't.
    UnsupportedMove(
        /// The ID of the remote change.
        Id,
    ),

    /// The remote change's old location ID was invalid.
    BadOldLocation(
        /// The invalid old location ID.
        Id,
    ),

    /// The remote change has no move information but refers to the destination
    /// of a move operation.
    UnexpectedMove(
        /// The ID of the remote change.
        Id,
    ),

    /// The remote change's old location ID incorrectly corresponds to an
    /// the destination of a move operation.
    OldLocationIsMove(
        /// The invalid old location ID.
        Id,
    ),

    /// The remote change represents an item to move but is incorrectly marked
    /// as hidden.
    HiddenMove(
        /// The ID of the remote change.
        Id,
    ),

    /// The remote change had an invalid move timestamp.
    ///
    /// If this happens, it is due to one of the following:
    ///
    /// * The change was not applied in causal order.
    /// * The change was generated by an incorrectly behaving client or was
    ///   otherwise corrupted.
    BadMoveTimestamp {
        /// The ID of the remote change.
        id: Id,
        /// The invalid timestamp.
        timestamp: crate::MoveTimestamp,
    },

    /// The change's move timestamp was greater than [`usize::MAX`].
    ///
    /// Since the addition of [`Self::BadMoveTimestamp`], this variant is no
    /// longer used. It is not possible for causally applied changes from
    /// correctly behaving clients to result in a timestamp greater than
    /// [`usize::MAX`], because that would imply that [`usize::MAX`] move
    /// operations have already been performed, which would use more than
    /// [`usize::MAX`] bytes of memory. Therefore, one of the two conditions
    /// described in [`Self::BadMoveTimestamp`] must be true, so that variant
    /// is always returned instead.
    #[deprecated(
        since = "0.2.2",
        note = "replaced by `BadMoveTimestamp`, which is more general"
    )]
    TimestampOverflow {
        /// The ID of the remote change.
        id: Id,
        /// The invalid timestamp.
        timestamp: crate::MoveTimestamp,
    },
}

impl<Id> ChangeError<Id> {
    pub(crate) fn as_basic(&self) -> Basic<&Self> {
        Basic(self)
    }

    fn fmt_basic(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::BadParentId(_) => write!(f, "bad parent id"),
            Self::BadDirection(_) => {
                write!(f, "change has no parent but its direction is 'before'")
            }
            Self::MergeConflict(_) => {
                write!(f, "conflict between change and existing data")
            }
            Self::DuplicateId(_) => {
                write!(f, "id is already in use with a different parent")
            }
            Self::UnsupportedMove(_) => {
                write!(f, "change has move info but moves are unsupported")
            }
            Self::BadOldLocation(_) => write!(f, "bad old location"),
            Self::UnexpectedMove(_) => {
                write!(f, "change has no move info but is a move destination")
            }
            Self::OldLocationIsMove(_) => {
                write!(f, "old location is a move destination")
            }
            Self::HiddenMove(_) => {
                write!(f, "change is a move destination but is hidden")
            }
            #[allow(deprecated)]
            Self::BadMoveTimestamp {
                timestamp,
                ..
            }
            | Self::TimestampOverflow {
                timestamp,
                ..
            } => {
                write!(f, "invalid move timestamp: {timestamp}")
            }
        }
    }
}

impl<Id: Display> Display for ChangeError<Id> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let basic = Basic(self);
        match self {
            Self::BadParentId(id) => write!(f, "{basic}: {id}"),
            Self::BadDirection(id) => write!(f, "{basic} (id {id})"),
            Self::MergeConflict(id) => write!(f, "{basic} (id {id})"),
            Self::DuplicateId(id) => write!(f, "{basic}: {id}"),
            Self::UnsupportedMove(id) => write!(f, "{basic} (id {id})"),
            Self::BadOldLocation(id) => write!(f, "{basic}: {id}"),
            Self::UnexpectedMove(id) => write!(f, "{basic} (id {id})"),
            Self::OldLocationIsMove(id) => write!(f, "{basic}: {id}"),
            Self::HiddenMove(id) => write!(f, "{basic} (id {id})"),
            #[allow(deprecated)]
            Self::BadMoveTimestamp {
                id,
                ..
            }
            | Self::TimestampOverflow {
                id,
                ..
            } => {
                write!(f, "{basic} (id {id})")
            }
        }
    }
}

#[cfg(feature = "std")]
#[cfg_attr(feature = "doc_cfg", doc(cfg(feature = "std")))]
impl<Id: Debug + Display> std::error::Error for ChangeError<Id> {}

/// An error encountered due to an invalid or out-of-bounds index.
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct IndexError {
    /// The invalid index.
    pub index: usize,
}

impl Display for IndexError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(fmt, "bad index: {}", self.index)
    }
}

#[cfg(feature = "std")]
#[cfg_attr(feature = "doc_cfg", doc(cfg(feature = "std")))]
impl std::error::Error for IndexError {}

/// An error encountered due to an invalid or missing ID.
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct IdError<Id> {
    /// The invalid ID.
    pub id: Id,
}

impl<Id: Display> Display for IdError<Id> {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(fmt, "bad id: {}", self.id)
    }
}

#[cfg(feature = "std")]
#[cfg_attr(feature = "doc_cfg", doc(cfg(feature = "std")))]
impl<Id: Debug + Display> std::error::Error for IdError<Id> {}

pub(crate) struct Basic<T>(T);

impl<Id> Display for Basic<&ChangeError<Id>> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt_basic(f)
    }
}

impl<Id> Debug for Basic<&ChangeError<Id>> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "ChangeError(\"{self}\")")
    }
}