automorph 0.2.0

Derive macros for bidirectional Automerge-Rust struct synchronization
Documentation
//! Error types for Automorph.
//!
//! This module provides error types for synchronization failures.

use std::fmt;

/// Error type for Automorph operations.
///
/// This error is returned when synchronization fails, typically due to:
/// - Type mismatches between the Rust struct and Automerge document
/// - Missing required fields
/// - Invalid data in the Automerge document
///
/// # Example
///
/// ```rust
/// use automorph::{Automorph, Error};
/// use automerge::{AutoCommit, ROOT};
///
/// let doc = AutoCommit::new();
/// // Trying to read a String from an empty document fails
/// let result = String::load(&doc, &ROOT, "missing");
/// assert!(result.is_err());
/// ```
#[derive(Debug)]
pub struct Error {
    /// The kind of error that occurred.
    pub kind: ErrorKind,
    /// Additional context about where the error occurred.
    pub context: Option<String>,
}

/// The specific kind of error that occurred.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
    /// Expected a specific Automerge type but found a different one.
    TypeMismatch {
        /// What type was expected.
        expected: &'static str,
        /// What type was found (if known).
        found: Option<String>,
    },
    /// A required field was missing from the Automerge document.
    MissingField {
        /// The name of the missing field.
        field: String,
    },
    /// The value in the Automerge document is missing or null.
    MissingValue,
    /// The value in the Automerge document is invalid for the target type.
    InvalidValue {
        /// Description of why the value is invalid.
        reason: String,
    },
    /// An error occurred in the underlying Automerge library.
    Automerge {
        /// The error message from Automerge.
        message: String,
    },
    /// An unknown field was found in the document when deny_unknown_fields is enabled.
    UnknownField {
        /// The name of the unknown field.
        field: String,
    },
}

impl Error {
    /// Creates a new type mismatch error.
    #[must_use]
    pub fn type_mismatch(expected: &'static str, found: Option<String>) -> Self {
        Self {
            kind: ErrorKind::TypeMismatch { expected, found },
            context: None,
        }
    }

    /// Creates a new missing field error.
    #[must_use]
    pub fn missing_field(field: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::MissingField {
                field: field.into(),
            },
            context: None,
        }
    }

    /// Creates a new missing value error.
    #[must_use]
    pub fn missing_value() -> Self {
        Self {
            kind: ErrorKind::MissingValue,
            context: None,
        }
    }

    /// Creates a new invalid value error.
    #[must_use]
    pub fn invalid_value(reason: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::InvalidValue {
                reason: reason.into(),
            },
            context: None,
        }
    }

    /// Creates a type mismatch error for expected struct type.
    #[must_use]
    pub fn expected_type(expected: &'static str, type_name: &str) -> Self {
        Self {
            kind: ErrorKind::TypeMismatch {
                expected,
                found: Some(format!("while loading {}", type_name)),
            },
            context: None,
        }
    }

    /// Creates an unknown field error.
    #[must_use]
    pub fn unknown_field(field: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::UnknownField {
                field: field.into(),
            },
            context: None,
        }
    }

    /// Adds context to this error.
    #[must_use]
    pub fn with_context(mut self, context: impl Into<String>) -> Self {
        self.context = Some(context.into());
        self
    }

    /// Prepends a field name to the existing context path.
    ///
    /// This is useful for building up paths like "person.address.city" when
    /// errors bubble up through nested structures.
    ///
    /// # Example
    ///
    /// ```rust
    /// use automorph::Error;
    ///
    /// let err = Error::missing_value()
    ///     .with_field("city")
    ///     .with_field("address")
    ///     .with_field("person");
    /// // Error message will show: "at person.address.city"
    /// ```
    #[must_use]
    pub fn with_field(mut self, field: impl Into<String>) -> Self {
        let field = field.into();
        self.context = Some(match self.context {
            Some(existing) => {
                // If existing starts with '[', don't add a dot between
                // e.g., field = "items", existing = "[2]" -> "items[2]"
                if existing.starts_with('[') {
                    format!("{}{}", field, existing)
                } else {
                    format!("{}.{}", field, existing)
                }
            }
            None => field,
        });
        self
    }

    /// Prepends an array index to the existing context path.
    ///
    /// # Example
    ///
    /// ```rust
    /// use automorph::Error;
    ///
    /// let err = Error::missing_value()
    ///     .with_index(2)
    ///     .with_field("items");
    /// // Error message will show: "at items[2]"
    /// ```
    #[must_use]
    pub fn with_index(mut self, index: usize) -> Self {
        let index_str = format!("[{}]", index);
        self.context = Some(match self.context {
            Some(existing) => {
                // If existing starts with '[', just concatenate (e.g., [5][2])
                // Otherwise, add a dot between (e.g., [5].name)
                if existing.starts_with('[') {
                    format!("{}{}", index_str, existing)
                } else {
                    format!("{}.{}", index_str, existing)
                }
            }
            None => index_str,
        });
        self
    }

    /// Returns true if this error is a missing value error.
    #[must_use]
    pub fn is_missing_value(&self) -> bool {
        matches!(self.kind, ErrorKind::MissingValue)
    }

    /// Returns true if this error is a missing field error.
    #[must_use]
    pub fn is_missing_field(&self) -> bool {
        matches!(self.kind, ErrorKind::MissingField { .. })
    }

    /// Returns true if this error is a type mismatch error.
    #[must_use]
    pub fn is_type_mismatch(&self) -> bool {
        matches!(self.kind, ErrorKind::TypeMismatch { .. })
    }

    /// Returns true if this error is an unknown field error.
    #[must_use]
    pub fn is_unknown_field(&self) -> bool {
        matches!(self.kind, ErrorKind::UnknownField { .. })
    }

    /// Returns true if this error is an invalid value error.
    #[must_use]
    pub fn is_invalid_value(&self) -> bool {
        matches!(self.kind, ErrorKind::InvalidValue { .. })
    }

    /// Returns true if this error is an Automerge error.
    #[must_use]
    pub fn is_automerge(&self) -> bool {
        matches!(self.kind, ErrorKind::Automerge { .. })
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.kind {
            ErrorKind::TypeMismatch { expected, found } => {
                write!(f, "type mismatch: expected {expected}")?;
                if let Some(found) = found {
                    write!(f, ", found {found}")?;
                }
            }
            ErrorKind::MissingField { field } => {
                write!(f, "missing required field: {field}")?;
            }
            ErrorKind::MissingValue => {
                write!(f, "value not found")?;
            }
            ErrorKind::InvalidValue { reason } => {
                write!(f, "invalid value: {reason}")?;
            }
            ErrorKind::Automerge { message } => {
                write!(f, "automerge error: {message}")?;
            }
            ErrorKind::UnknownField { field } => {
                write!(f, "unknown field: {field}")?;
            }
        }
        if let Some(ctx) = &self.context {
            write!(f, " (at {ctx})")?;
        }
        Ok(())
    }
}

impl std::error::Error for Error {}

impl From<automerge::AutomergeError> for Error {
    fn from(err: automerge::AutomergeError) -> Self {
        Self {
            kind: ErrorKind::Automerge {
                message: err.to_string(),
            },
            context: None,
        }
    }
}

/// Result type alias for Automorph operations.
pub type Result<T> = std::result::Result<T, Error>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_display() {
        let err = Error::type_mismatch("String", Some("Int".to_string()));
        assert!(err.to_string().contains("type mismatch"));
        assert!(err.to_string().contains("String"));
        assert!(err.to_string().contains("Int"));
    }

    #[test]
    fn test_error_with_context() {
        let err = Error::missing_field("name").with_context("person.name");
        assert!(err.to_string().contains("person.name"));
    }

    #[test]
    fn test_missing_value_error() {
        let err = Error::missing_value();
        assert!(err.to_string().contains("not found"));
    }

    #[test]
    fn test_invalid_value_error() {
        let err = Error::invalid_value("out of range");
        assert!(err.to_string().contains("out of range"));
    }

    #[test]
    fn test_error_with_field_path() {
        let err = Error::missing_value()
            .with_field("city")
            .with_field("address")
            .with_field("person");
        let msg = err.to_string();
        assert!(msg.contains("person.address.city"), "got: {}", msg);
    }

    #[test]
    fn test_error_with_index() {
        let err = Error::missing_value().with_index(2).with_field("items");
        let msg = err.to_string();
        assert!(msg.contains("items[2]"), "got: {}", msg);
    }

    #[test]
    fn test_error_with_nested_index() {
        let err = Error::type_mismatch("String", None)
            .with_field("name")
            .with_index(5)
            .with_field("users");
        let msg = err.to_string();
        assert!(msg.contains("users[5].name"), "got: {}", msg);
    }

    #[test]
    fn test_error_predicates() {
        assert!(Error::missing_value().is_missing_value());
        assert!(Error::missing_field("x").is_missing_field());
        assert!(Error::type_mismatch("Int", None).is_type_mismatch());
        assert!(Error::unknown_field("x").is_unknown_field());
        assert!(Error::invalid_value("bad").is_invalid_value());

        // Test Automerge error predicate
        let automerge_err = Error {
            kind: ErrorKind::Automerge {
                message: "test error".to_string(),
            },
            context: None,
        };
        assert!(automerge_err.is_automerge());
    }
}