gobby-wiki 0.2.0

Gobby wiki CLI shell
use std::fmt;
use std::path::PathBuf;

use gobby_core::setup::SetupError;

use crate::{indexer, search};

#[derive(Debug)]
pub enum WikiError {
    NotImplemented {
        command: &'static str,
        detail: &'static str,
    },
    InvalidScope {
        detail: String,
    },
    Config {
        detail: String,
    },
    Io {
        action: &'static str,
        path: Option<PathBuf>,
        source: std::io::Error,
    },
    Json {
        action: &'static str,
        path: Option<PathBuf>,
        source: serde_json::Error,
    },
    Yaml {
        action: &'static str,
        path: Option<PathBuf>,
        source: serde_yaml::Error,
    },
    Registry {
        detail: String,
    },
    Daemon {
        endpoint: &'static str,
        message: String,
    },
    InvalidInput {
        field: &'static str,
        message: String,
    },
    NotFound {
        resource: &'static str,
        id: String,
    },
    Index {
        source: indexer::IndexError,
    },
    Search {
        source: search::SearchError,
    },
    Setup {
        source: SetupError,
    },
}

impl WikiError {
    pub fn code(&self) -> &'static str {
        match self {
            Self::NotImplemented { .. } => "not_implemented",
            Self::InvalidScope { .. } => "invalid_scope",
            Self::Config { .. } => "config_error",
            Self::Io { .. } => "io_error",
            Self::Json { .. } => "json_error",
            Self::Yaml { .. } => "yaml_error",
            Self::Registry { .. } => "registry_error",
            Self::Daemon { .. } => "daemon_error",
            Self::InvalidInput { .. } => "invalid_input",
            Self::NotFound { .. } => "not_found",
            Self::Index { .. } => "index_error",
            Self::Search { .. } => "search_error",
            Self::Setup { .. } => "setup_error",
        }
    }
}

impl fmt::Display for WikiError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NotImplemented { command, detail } => {
                write!(f, "{command}: {detail} ({})", self.code())
            }
            Self::InvalidScope { detail } | Self::Config { detail } | Self::Registry { detail } => {
                write!(f, "{detail} ({})", self.code())
            }
            Self::Io {
                action,
                path,
                source,
            } => format_action_error(f, action, path.as_ref(), source, self.code()),
            Self::Json {
                action,
                path,
                source,
            } => format_action_error(f, action, path.as_ref(), source, self.code()),
            Self::Yaml {
                action,
                path,
                source,
            } => format_action_error(f, action, path.as_ref(), source, self.code()),
            Self::Daemon { endpoint, message } => {
                write!(f, "{endpoint}: {message} ({})", self.code())
            }
            Self::InvalidInput { field, message } => {
                write!(f, "{field}: {message} ({})", self.code())
            }
            Self::NotFound { resource, id } => {
                write!(f, "{resource} `{id}` was not found ({})", self.code())
            }
            Self::Index { source } => write!(f, "index: {source} ({})", self.code()),
            Self::Search { source } => write!(f, "query: {source} ({})", self.code()),
            Self::Setup { source } => write!(f, "gwiki setup failed: {source} ({})", self.code()),
        }
    }
}

fn format_action_error(
    f: &mut fmt::Formatter<'_>,
    action: &str,
    path: Option<&PathBuf>,
    source: &dyn std::error::Error,
    code: &str,
) -> fmt::Result {
    match path {
        Some(path) => write!(
            f,
            "{action} failed for {}: {source} ({code})",
            path.display()
        ),
        None => write!(f, "{action} failed: {source} ({code})"),
    }
}

impl std::error::Error for WikiError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io { source, .. } => Some(source),
            Self::Json { source, .. } => Some(source),
            Self::Yaml { source, .. } => Some(source),
            Self::Index { source } => Some(source),
            Self::Search { source } => Some(source),
            Self::Setup { source } => Some(source),
            _ => None,
        }
    }
}

impl From<indexer::IndexError> for WikiError {
    fn from(error: indexer::IndexError) -> Self {
        Self::Index { source: error }
    }
}

impl From<search::SearchError> for WikiError {
    fn from(error: search::SearchError) -> Self {
        Self::Search { source: error }
    }
}

impl From<SetupError> for WikiError {
    fn from(error: SetupError) -> Self {
        Self::Setup { source: error }
    }
}

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

    #[test]
    fn typed_error_sources_are_preserved() {
        let source =
            serde_json::from_str::<serde_json::Value>("{").expect_err("invalid JSON should fail");
        let error = WikiError::Json {
            action: "parse fixture",
            path: None,
            source,
        };

        assert!(error.source().is_some());
        assert!(error.to_string().contains("parse fixture failed"));
    }

    #[test]
    fn wrapped_error_codes_are_specific() {
        let index = WikiError::Index {
            source: indexer::IndexError::Walk("walk failed".to_string()),
        };
        let search = WikiError::Search {
            source: search::SearchError::Backend("backend failed".to_string()),
        };

        assert_eq!(index.code(), "index_error");
        assert_eq!(search.code(), "search_error");
    }

    #[test]
    fn setup_error_source_is_preserved() {
        let error = WikiError::from(SetupError::CreationFailed {
            object: "gwiki_documents".to_string(),
            message: "permission denied".to_string(),
        });

        assert_eq!(error.code(), "setup_error");
        assert!(error.source().is_some());
        assert!(error.to_string().contains("gwiki setup failed"));
    }
}