forgekit-core 0.5.0

Deterministic code intelligence SDK - Core library
Documentation
use std::sync::Arc;

#[derive(Debug, Clone)]
pub struct ProgressEvent {
    pub operation: String,
    pub phase: ProgressPhase,
    pub current: u64,
    pub total: Option<u64>,
    pub message: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProgressPhase {
    Started,
    Progress,
    Completed,
    Failed(String),
}

pub trait ProgressSink: Send + Sync {
    fn emit(&self, event: ProgressEvent);
}

pub struct NoopProgress;

impl ProgressSink for NoopProgress {
    fn emit(&self, _event: ProgressEvent) {}
}

pub struct ChannelProgress {
    tx: tokio::sync::mpsc::UnboundedSender<ProgressEvent>,
}

impl ChannelProgress {
    pub fn new() -> (Self, tokio::sync::mpsc::UnboundedReceiver<ProgressEvent>) {
        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
        (Self { tx }, rx)
    }
}

impl ProgressSink for ChannelProgress {
    fn emit(&self, event: ProgressEvent) {
        let _ = self.tx.send(event);
    }
}

pub struct ProgressEmitter {
    sink: Option<Arc<dyn ProgressSink>>,
}

impl ProgressEmitter {
    pub fn new(sink: Option<Arc<dyn ProgressSink>>) -> Self {
        Self { sink }
    }

    pub fn started(&self, operation: &str, message: &str) {
        self.emit(ProgressEvent {
            operation: operation.to_string(),
            phase: ProgressPhase::Started,
            current: 0,
            total: None,
            message: message.to_string(),
        });
    }

    pub fn progress(&self, operation: &str, current: u64, total: Option<u64>, message: &str) {
        self.emit(ProgressEvent {
            operation: operation.to_string(),
            phase: ProgressPhase::Progress,
            current,
            total,
            message: message.to_string(),
        });
    }

    pub fn completed(&self, operation: &str, message: &str) {
        self.emit(ProgressEvent {
            operation: operation.to_string(),
            phase: ProgressPhase::Completed,
            current: 0,
            total: None,
            message: message.to_string(),
        });
    }

    pub fn failed(&self, operation: &str, error: &str) {
        self.emit(ProgressEvent {
            operation: operation.to_string(),
            phase: ProgressPhase::Failed(error.to_string()),
            current: 0,
            total: None,
            message: error.to_string(),
        });
    }

    fn emit(&self, event: ProgressEvent) {
        if let Some(ref sink) = self.sink {
            sink.emit(event);
        }
    }
}

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

    #[test]
    fn test_noop_does_not_panic() {
        let noop = NoopProgress;
        noop.emit(ProgressEvent {
            operation: "test".to_string(),
            phase: ProgressPhase::Started,
            current: 0,
            total: None,
            message: "test".to_string(),
        });
    }

    #[test]
    fn test_channel_receives_events() {
        let (progress, mut rx) = ChannelProgress::new();
        progress.emit(ProgressEvent {
            operation: "index".to_string(),
            phase: ProgressPhase::Started,
            current: 0,
            total: Some(100),
            message: "starting".to_string(),
        });

        let event = rx.try_recv().unwrap();
        assert_eq!(event.operation, "index");
        assert_eq!(event.phase, ProgressPhase::Started);
        assert_eq!(event.total, Some(100));
    }

    #[test]
    fn test_emitter_with_no_sink() {
        let emitter = ProgressEmitter::new(None);
        emitter.started("test", "starting");
        emitter.progress("test", 50, Some(100), "halfway");
        emitter.completed("test", "done");
    }

    #[test]
    fn test_emitter_with_channel() {
        let (progress, mut rx) = ChannelProgress::new();
        let emitter = ProgressEmitter::new(Some(Arc::new(progress)));

        emitter.started("build", "compiling");
        emitter.progress("build", 5, Some(10), "file 5");
        emitter.completed("build", "success");

        let e1 = rx.try_recv().unwrap();
        assert_eq!(e1.phase, ProgressPhase::Started);
        let e2 = rx.try_recv().unwrap();
        assert_eq!(e2.current, 5);
        let e3 = rx.try_recv().unwrap();
        assert_eq!(e3.phase, ProgressPhase::Completed);
    }

    #[test]
    fn test_emitter_failed() {
        let (progress, mut rx) = ChannelProgress::new();
        let emitter = ProgressEmitter::new(Some(Arc::new(progress)));

        emitter.failed("build", "compile error");

        let event = rx.try_recv().unwrap();
        assert!(matches!(event.phase, ProgressPhase::Failed(ref e) if e == "compile error"));
    }

    #[test]
    fn test_channel_dropped_receiver_does_not_panic() {
        let (progress, rx) = ChannelProgress::new();
        drop(rx);
        progress.emit(ProgressEvent {
            operation: "test".to_string(),
            phase: ProgressPhase::Started,
            current: 0,
            total: None,
            message: "should not panic".to_string(),
        });
    }
}