Skip to main content

typub_log/
progress.rs

1//! Progress reporting trait for decoupling storage from UI.
2//!
3//! Implements [[ADR-0004]] Phase 2: ProgressReporter trait.
4
5/// Trait for reporting progress during long-running operations.
6///
7/// This trait decouples the storage layer from the UI layer, allowing
8/// operations like S3 uploads to report progress without depending on
9/// `typub-ui`.
10///
11/// # Implementations
12///
13/// - `NullReporter` - Discards all progress reports (default)
14/// - `FnReporter` - Wraps a closure for simple progress reporting
15/// - `typub-ui` provides `IndicatifReporter` with progress bars (Phase 4)
16///
17/// # Example
18///
19/// ```rust
20/// use typub_log::{ProgressReporter, NullReporter};
21///
22/// fn upload_with_progress(reporter: &dyn ProgressReporter) {
23///     reporter.set_message("Uploading assets...");
24///     for i in 0..10 {
25///         // ... upload logic ...
26///         reporter.set_progress(i + 1, 10);
27///     }
28///     reporter.finish_success("Upload complete");
29/// }
30///
31/// // Use null reporter when no UI is available
32/// upload_with_progress(&NullReporter);
33/// ```
34pub trait ProgressReporter: Send + Sync {
35    /// Set the current progress message.
36    fn set_message(&self, message: &str);
37
38    /// Set progress as (current, total).
39    ///
40    /// For indeterminate progress, call with (0, 0).
41    fn set_progress(&self, current: u64, total: u64);
42
43    /// Mark the operation as successfully completed.
44    fn finish_success(&self, message: &str);
45
46    /// Mark the operation as failed.
47    fn finish_error(&self, message: &str);
48
49    /// Increment progress by a given amount.
50    fn inc(&self, delta: u64) {
51        // Default implementation does nothing
52        let _ = delta;
53    }
54}
55
56// ============ NullReporter ============
57
58/// A progress reporter that discards all reports.
59///
60/// Use this when no progress UI is needed (e.g., in library mode or tests).
61#[derive(Debug, Clone, Copy, Default)]
62pub struct NullReporter;
63
64impl ProgressReporter for NullReporter {
65    fn set_message(&self, _message: &str) {}
66    fn set_progress(&self, _current: u64, _total: u64) {}
67    fn finish_success(&self, _message: &str) {}
68    fn finish_error(&self, _message: &str) {}
69}
70
71// ============ () as NullReporter ============
72
73/// Unit type implements ProgressReporter as a null reporter.
74///
75/// This allows passing `&()` when no progress reporting is needed.
76impl ProgressReporter for () {
77    fn set_message(&self, _message: &str) {}
78    fn set_progress(&self, _current: u64, _total: u64) {}
79    fn finish_success(&self, _message: &str) {}
80    fn finish_error(&self, _message: &str) {}
81}
82
83// ============ FnReporter ============
84
85/// A progress reporter that wraps a closure.
86///
87/// Useful for simple progress reporting without implementing the full trait.
88///
89/// # Example
90///
91/// ```rust
92/// use typub_log::{FnReporter, ProgressReporter};
93///
94/// let reporter = FnReporter::new(|msg| println!("Progress: {}", msg));
95/// reporter.set_message("Working...");
96/// ```
97pub struct FnReporter<F>
98where
99    F: Fn(&str) + Send + Sync,
100{
101    callback: F,
102}
103
104impl<F> FnReporter<F>
105where
106    F: Fn(&str) + Send + Sync,
107{
108    /// Create a new FnReporter with the given callback.
109    pub fn new(callback: F) -> Self {
110        Self { callback }
111    }
112}
113
114impl<F> ProgressReporter for FnReporter<F>
115where
116    F: Fn(&str) + Send + Sync,
117{
118    fn set_message(&self, message: &str) {
119        (self.callback)(message);
120    }
121
122    fn set_progress(&self, current: u64, total: u64) {
123        let msg = format!("{}/{}", current, total);
124        (self.callback)(&msg);
125    }
126
127    fn finish_success(&self, message: &str) {
128        (self.callback)(message);
129    }
130
131    fn finish_error(&self, message: &str) {
132        (self.callback)(message);
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    #![allow(clippy::expect_used)]
139
140    use super::*;
141    use std::sync::Arc;
142    use std::sync::atomic::{AtomicUsize, Ordering};
143
144    #[test]
145    fn test_null_reporter_is_silent() {
146        let reporter = NullReporter;
147        reporter.set_message("test");
148        reporter.set_progress(1, 10);
149        reporter.finish_success("done");
150        reporter.finish_error("error");
151        // No panic = success
152    }
153
154    #[test]
155    fn test_unit_as_reporter() {
156        let reporter: &dyn ProgressReporter = &();
157        reporter.set_message("test");
158        reporter.set_progress(1, 10);
159        reporter.finish_success("done");
160        // No panic = success
161    }
162
163    #[test]
164    fn test_fn_reporter_calls_callback() {
165        let count = Arc::new(AtomicUsize::new(0));
166        let count_clone = count.clone();
167
168        let reporter = FnReporter::new(move |_msg| {
169            count_clone.fetch_add(1, Ordering::SeqCst);
170        });
171
172        reporter.set_message("one");
173        reporter.set_progress(1, 10);
174        reporter.finish_success("done");
175
176        assert_eq!(count.load(Ordering::SeqCst), 3);
177    }
178}