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}