kanban-persistence 0.7.0

Persistence traits and shared types for the kanban project management tool
Documentation
use thiserror::Error;

#[derive(Error, Debug)]
pub enum PersistenceError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("serialization error: {0}")]
    Serialization(String),

    #[error("database error: {0}")]
    Database(String),

    #[error("unsupported storage locator {locator:?}; supported: {}", supported.join(", "))]
    UnsupportedLocator {
        locator: String,
        supported: Vec<String>,
    },

    #[error("file conflict: {path} was modified by another instance")]
    ConflictDetected {
        path: String,
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    #[error("unsupported operation: {0}")]
    Unsupported(String),

    #[error(
        "file format v{file_version} is newer than this binary's max v{binary_max}; \
         please upgrade kanban"
    )]
    UnsupportedFutureVersion { file_version: u32, binary_max: u32 },
}

pub type PersistenceResult<T> = Result<T, PersistenceError>;

impl From<PersistenceError> for kanban_domain::KanbanError {
    fn from(e: PersistenceError) -> Self {
        match e {
            PersistenceError::Io(io) => kanban_domain::KanbanError::Io(io),
            PersistenceError::Serialization(s) => kanban_domain::KanbanError::Serialization(s),
            PersistenceError::Database(s) => kanban_domain::KanbanError::Database(s),
            PersistenceError::UnsupportedLocator { locator, supported } => {
                kanban_domain::KanbanError::Internal(format!(
                    "No backend registered for {:?}. Available backends: {}",
                    locator,
                    if supported.is_empty() {
                        "none".to_string()
                    } else {
                        supported.join(", ")
                    }
                ))
            }
            PersistenceError::ConflictDetected { path, source } => {
                kanban_domain::KanbanError::ConflictDetected { path, source }
            }
            PersistenceError::Unsupported(s) => kanban_domain::KanbanError::Internal(s),
            PersistenceError::UnsupportedFutureVersion {
                file_version,
                binary_max,
            } => kanban_domain::KanbanError::UnsupportedFutureVersion {
                file_version,
                binary_max,
            },
        }
    }
}

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

    #[test]
    fn test_unsupported_future_version_display_mentions_both_versions() {
        let err = PersistenceError::UnsupportedFutureVersion {
            file_version: 99,
            binary_max: 6,
        };
        let msg = err.to_string();
        assert!(msg.contains("99"), "msg: {msg}");
        assert!(msg.contains('6'), "msg: {msg}");
    }

    #[test]
    fn test_persistence_unsupported_future_version_maps_to_kanban_error() {
        let pe = PersistenceError::UnsupportedFutureVersion {
            file_version: 99,
            binary_max: 6,
        };
        let ke: KanbanError = pe.into();
        assert!(
            matches!(
                ke,
                KanbanError::UnsupportedFutureVersion {
                    file_version: 99,
                    binary_max: 6
                }
            ),
            "expected UnsupportedFutureVersion variant"
        );
    }
}