use alloc::boxed::Box;
use alloc::string::String;
use core::fmt::{self, Debug, Display};
use core::future::Future;
use core::pin::Pin;
#[derive(Debug, Clone, PartialEq)]
pub struct Progress {
done: f64,
total: Option<f64>,
message: Option<String>,
}
impl Progress {
#[must_use]
pub const fn new(progress: f64) -> Self {
Self {
done: progress,
total: None,
message: None,
}
}
#[must_use]
pub const fn with_total(mut self, total: f64) -> Self {
self.total = Some(total);
self
}
#[must_use]
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
#[must_use]
pub const fn progress(&self) -> f64 {
self.done
}
#[must_use]
pub const fn total(&self) -> Option<f64> {
self.total
}
#[must_use]
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
}
pub trait ProgressSink: Send + 'static {
fn report(&mut self, progress: Progress) -> impl Future<Output = ()> + Send;
}
trait ProgressSinkImpl: Send {
fn report(&mut self, progress: Progress) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}
impl<S: ProgressSink> ProgressSinkImpl for S {
fn report(&mut self, progress: Progress) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(ProgressSink::report(self, progress))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ProgressError {
NotFinite(Progress),
NotIncreasing {
previous: f64,
reported: Progress,
},
}
impl Display for ProgressError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFinite(progress) => write!(
f,
"progress report is not finite: progress {}, total {:?}",
progress.done, progress.total
),
Self::NotIncreasing { previous, reported } => write!(
f,
"progress must increase with every report: {} does not exceed {previous}",
reported.done
),
}
}
}
impl core::error::Error for ProgressError {}
pub struct ToolContext {
sink: Option<Box<dyn ProgressSinkImpl>>,
last_progress: Option<f64>,
}
impl ToolContext {
#[must_use]
pub const fn new() -> Self {
Self {
sink: None,
last_progress: None,
}
}
#[must_use]
pub fn with_progress(sink: impl ProgressSink) -> Self {
Self {
sink: Some(Box::new(sink)),
last_progress: None,
}
}
#[must_use]
pub const fn is_listening(&self) -> bool {
self.sink.is_some()
}
pub async fn report_progress(&mut self, progress: Progress) -> Result<(), ProgressError> {
if !progress.done.is_finite() || progress.total.is_some_and(|total| !total.is_finite()) {
return Err(ProgressError::NotFinite(progress));
}
if let Some(previous) = self.last_progress
&& progress.done <= previous
{
return Err(ProgressError::NotIncreasing {
previous,
reported: progress,
});
}
self.last_progress = Some(progress.done);
if let Some(sink) = &mut self.sink {
sink.report(progress).await;
}
Ok(())
}
}
impl Default for ToolContext {
fn default() -> Self {
Self::new()
}
}
impl Debug for ToolContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ToolContext")
.field("listening", &self.sink.is_some())
.field("last_progress", &self.last_progress)
.finish()
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use alloc::vec::Vec;
use std::sync::mpsc;
struct Collect(mpsc::Sender<Progress>);
impl ProgressSink for Collect {
async fn report(&mut self, progress: Progress) {
self.0.send(progress).expect("test receiver alive");
}
}
fn listening() -> (ToolContext, mpsc::Receiver<Progress>) {
let (tx, rx) = mpsc::channel();
(ToolContext::with_progress(Collect(tx)), rx)
}
#[test]
fn reports_whether_the_caller_listens() {
assert!(!ToolContext::new().is_listening());
assert!(listening().0.is_listening());
}
#[tokio::test]
async fn delivers_increasing_reports_in_order() {
let (mut cx, seen) = listening();
cx.report_progress(Progress::new(1.0)).await.unwrap();
cx.report_progress(Progress::new(2.5).with_total(10.0).with_message("half"))
.await
.unwrap();
assert_eq!(
seen.try_iter().collect::<Vec<_>>(),
[
Progress::new(1.0),
Progress::new(2.5).with_total(10.0).with_message("half"),
]
);
}
#[tokio::test]
async fn rejects_non_increasing_and_non_finite_reports() {
let (mut cx, seen) = listening();
cx.report_progress(Progress::new(3.0)).await.unwrap();
assert_eq!(
cx.report_progress(Progress::new(3.0)).await,
Err(ProgressError::NotIncreasing {
previous: 3.0,
reported: Progress::new(3.0),
})
);
assert!(matches!(
cx.report_progress(Progress::new(f64::NAN)).await,
Err(ProgressError::NotFinite(_))
));
assert!(matches!(
cx.report_progress(Progress::new(4.0).with_total(f64::INFINITY))
.await,
Err(ProgressError::NotFinite(_))
));
cx.report_progress(Progress::new(3.5)).await.unwrap();
assert_eq!(
seen.try_iter().collect::<Vec<_>>(),
[Progress::new(3.0), Progress::new(3.5)]
);
}
#[tokio::test]
async fn context_without_a_listener_still_validates() {
let mut cx = ToolContext::new();
cx.report_progress(Progress::new(1.0)).await.unwrap();
assert!(cx.report_progress(Progress::new(0.5)).await.is_err());
}
}