thor-notify 0.2.1

Notification schema and inbox management for Thor
Documentation
use crate::schema::Notification;
use std::path::PathBuf;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum InboxError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),
    #[error("Notification not found: {0}")]
    NotFound(String),
}

pub type Result<T> = std::result::Result<T, InboxError>;

/// Get the inbox directory path
pub fn inbox_dir() -> PathBuf {
    dirs::data_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("thor")
        .join("inbox")
}

/// Ensure inbox directory exists
pub async fn ensure_inbox_dir() -> Result<PathBuf> {
    let dir = inbox_dir();
    tokio::fs::create_dir_all(&dir).await?;
    Ok(dir)
}

/// Save a notification to the inbox
pub async fn save_notification(notification: &Notification) -> Result<PathBuf> {
    let dir = ensure_inbox_dir().await?;
    let filename = format!(
        "{}-{}-{}.json",
        notification.timestamp.format("%Y%m%d%H%M%S"),
        notification.branch.replace('/', "-"),
        notification.id
    );
    let path = dir.join(filename);

    let json = serde_json::to_string_pretty(notification)?;
    tokio::fs::write(&path, json).await?;

    Ok(path)
}

/// List all notifications in the inbox
pub async fn list_notifications() -> Result<Vec<Notification>> {
    let dir = inbox_dir();
    if !dir.exists() {
        return Ok(Vec::new());
    }

    let mut notifications = Vec::new();

    let mut entries = tokio::fs::read_dir(dir).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();

        if path.extension().map(|e| e == "json").unwrap_or(false) {
            let content = tokio::fs::read_to_string(&path).await?;
            if let Ok(notification) = serde_json::from_str::<Notification>(&content) {
                notifications.push(notification);
            }
        }
    }

    // Sort by timestamp, newest first
    notifications.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));

    Ok(notifications)
}

/// Get a notification by ID
pub async fn get_notification(id: &str) -> Result<Notification> {
    let notifications = list_notifications().await?;
    notifications
        .into_iter()
        .find(|n| n.id.to_string() == id || n.id.to_string().starts_with(id))
        .ok_or_else(|| InboxError::NotFound(id.to_string()))
}

/// Delete a notification by ID
pub async fn delete_notification(id: &str) -> Result<()> {
    let dir = inbox_dir();

    let mut entries = tokio::fs::read_dir(&dir).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();

        if path.extension().map(|e| e == "json").unwrap_or(false) {
            if let Ok(content) = tokio::fs::read_to_string(&path).await {
                if let Ok(n) = serde_json::from_str::<Notification>(&content) {
                    if n.id.to_string() == id || n.id.to_string().starts_with(id) {
                        tokio::fs::remove_file(path).await?;
                        return Ok(());
                    }
                }
            }
        }
    }

    Err(InboxError::NotFound(id.to_string()))
}