Skip to main content

qubit_progress/model/
progress_event.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::time::Duration;
11
12use serde::{
13    Deserialize,
14    Serialize,
15};
16
17use super::{
18    ProgressCounter,
19    ProgressEventBuilder,
20    ProgressMetric,
21    ProgressMetricSnapshot,
22    ProgressPhase,
23    ProgressSchema,
24    ProgressStage,
25};
26
27/// Immutable progress event delivered to reporters.
28///
29/// Each event carries its [`ProgressSchema`], making serialized events
30/// self-describing for logs, databases, and agent-readable JSON streams.
31///
32/// # Examples
33///
34/// ```
35/// use std::time::Duration;
36///
37/// use qubit_progress::{
38///     ProgressEvent,
39///     ProgressMetric,
40///     ProgressPhase,
41///     ProgressSchema,
42/// };
43///
44/// let schema = ProgressSchema::new(vec![
45///     ProgressMetric::new("entries", "Entries"),
46///     ProgressMetric::new("bytes", "Bytes"),
47/// ]);
48/// let event = ProgressEvent::builder(schema)
49///     .running()
50///     .counter("entries", |counter| counter.total(5).completed(2))
51///     .counter("bytes", |counter| counter.total(500).completed(200))
52///     .elapsed(Duration::from_millis(500))
53///     .build();
54///
55/// assert_eq!(event.phase(), ProgressPhase::Running);
56/// assert_eq!(event.counter("entries").map(|c| c.completed_count()), Some(2));
57/// ```
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct ProgressEvent {
60    /// Metric schema that describes every counter in this event.
61    schema: ProgressSchema,
62    /// Lifecycle phase of the reported operation.
63    phase: ProgressPhase,
64    /// Optional current stage.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    stage: Option<ProgressStage>,
67    /// Metric counters for this event.
68    counters: Vec<ProgressCounter>,
69    /// Monotonic elapsed duration.
70    #[serde(with = "qubit_serde::serde::duration_with_unit")]
71    elapsed: Duration,
72}
73
74impl ProgressEvent {
75    /// Creates a progress event builder for a schema.
76    ///
77    /// # Parameters
78    ///
79    /// * `schema` - Metric schema carried by the built event.
80    ///
81    /// # Returns
82    ///
83    /// A builder initialized as running progress with no counters and zero
84    /// elapsed time.
85    #[inline]
86    pub fn builder(schema: ProgressSchema) -> ProgressEventBuilder {
87        ProgressEventBuilder::new(schema)
88    }
89
90    /// Creates a progress event from a builder.
91    ///
92    /// # Parameters
93    ///
94    /// * `builder` - Builder containing configured event fields.
95    ///
96    /// # Returns
97    ///
98    /// A progress event built from `builder`.
99    #[inline]
100    pub fn new(builder: ProgressEventBuilder) -> Self {
101        Self {
102            schema: builder.schema,
103            phase: builder.phase,
104            stage: builder.stage,
105            counters: builder.counters,
106            elapsed: builder.elapsed,
107        }
108    }
109
110    /// Creates a progress event for the supplied lifecycle phase.
111    ///
112    /// # Parameters
113    ///
114    /// * `schema` - Metric schema carried by the event.
115    /// * `phase` - Lifecycle phase for the event.
116    /// * `counters` - Metric counters carried by the event.
117    /// * `elapsed` - Elapsed duration carried by the event.
118    ///
119    /// # Returns
120    ///
121    /// A progress event with `schema`, `phase`, `counters`, and `elapsed`.
122    #[inline]
123    pub fn from_phase(
124        schema: ProgressSchema,
125        phase: ProgressPhase,
126        counters: Vec<ProgressCounter>,
127        elapsed: Duration,
128    ) -> Self {
129        Self::builder(schema)
130            .phase(phase)
131            .counters(counters)
132            .elapsed(elapsed)
133            .build()
134    }
135
136    /// Creates a started progress event.
137    ///
138    /// # Parameters
139    ///
140    /// * `schema` - Metric schema carried by the event.
141    /// * `counters` - Initial progress counters.
142    /// * `elapsed` - Elapsed duration at start, usually zero.
143    ///
144    /// # Returns
145    ///
146    /// A progress event with [`ProgressPhase::Started`].
147    #[inline]
148    pub fn started(schema: ProgressSchema, counters: Vec<ProgressCounter>, elapsed: Duration) -> Self {
149        Self::from_phase(schema, ProgressPhase::Started, counters, elapsed)
150    }
151
152    /// Creates a running progress event.
153    ///
154    /// # Parameters
155    ///
156    /// * `schema` - Metric schema carried by the event.
157    /// * `counters` - Current progress counters.
158    /// * `elapsed` - Elapsed duration since operation start.
159    ///
160    /// # Returns
161    ///
162    /// A progress event with [`ProgressPhase::Running`].
163    #[inline]
164    pub fn running(schema: ProgressSchema, counters: Vec<ProgressCounter>, elapsed: Duration) -> Self {
165        Self::from_phase(schema, ProgressPhase::Running, counters, elapsed)
166    }
167
168    /// Creates a finished progress event.
169    ///
170    /// # Parameters
171    ///
172    /// * `schema` - Metric schema carried by the event.
173    /// * `counters` - Final progress counters.
174    /// * `elapsed` - Total elapsed duration.
175    ///
176    /// # Returns
177    ///
178    /// A progress event with [`ProgressPhase::Finished`].
179    #[inline]
180    pub fn finished(schema: ProgressSchema, counters: Vec<ProgressCounter>, elapsed: Duration) -> Self {
181        Self::from_phase(schema, ProgressPhase::Finished, counters, elapsed)
182    }
183
184    /// Creates a failed progress event.
185    ///
186    /// # Parameters
187    ///
188    /// * `schema` - Metric schema carried by the event.
189    /// * `counters` - Final or current progress counters.
190    /// * `elapsed` - Elapsed duration at failure.
191    ///
192    /// # Returns
193    ///
194    /// A progress event with [`ProgressPhase::Failed`].
195    #[inline]
196    pub fn failed(schema: ProgressSchema, counters: Vec<ProgressCounter>, elapsed: Duration) -> Self {
197        Self::from_phase(schema, ProgressPhase::Failed, counters, elapsed)
198    }
199
200    /// Creates a canceled progress event.
201    ///
202    /// # Parameters
203    ///
204    /// * `schema` - Metric schema carried by the event.
205    /// * `counters` - Final or current progress counters.
206    /// * `elapsed` - Elapsed duration at cancellation.
207    ///
208    /// # Returns
209    ///
210    /// A progress event with [`ProgressPhase::Canceled`].
211    #[inline]
212    pub fn canceled(schema: ProgressSchema, counters: Vec<ProgressCounter>, elapsed: Duration) -> Self {
213        Self::from_phase(schema, ProgressPhase::Canceled, counters, elapsed)
214    }
215
216    /// Returns a copy configured with the current stage.
217    ///
218    /// # Parameters
219    ///
220    /// * `stage` - Current operation stage.
221    ///
222    /// # Returns
223    ///
224    /// This event with `stage` recorded.
225    #[inline]
226    #[must_use]
227    pub fn with_stage(mut self, stage: ProgressStage) -> Self {
228        self.stage = Some(stage);
229        self
230    }
231
232    /// Returns the event schema.
233    ///
234    /// # Returns
235    ///
236    /// The metric schema carried by this event.
237    #[inline]
238    pub const fn schema(&self) -> &ProgressSchema {
239        &self.schema
240    }
241
242    /// Returns the event phase.
243    ///
244    /// # Returns
245    ///
246    /// The lifecycle phase carried by this event.
247    #[inline]
248    pub const fn phase(&self) -> ProgressPhase {
249        self.phase
250    }
251
252    /// Returns the current stage when known.
253    ///
254    /// # Returns
255    ///
256    /// `Some(stage)` when this event carries stage information, otherwise
257    /// `None`.
258    #[inline]
259    pub const fn stage(&self) -> Option<&ProgressStage> {
260        self.stage.as_ref()
261    }
262
263    /// Returns the progress counters.
264    ///
265    /// # Returns
266    ///
267    /// The counters carried by this event.
268    #[inline]
269    pub fn counters(&self) -> &[ProgressCounter] {
270        self.counters.as_slice()
271    }
272
273    /// Finds a counter by metric id.
274    ///
275    /// # Parameters
276    ///
277    /// * `metric_id` - Metric identifier to search for.
278    ///
279    /// # Returns
280    ///
281    /// `Some(counter)` when the event contains a matching counter, otherwise
282    /// `None`.
283    #[inline]
284    pub fn counter(&self, metric_id: &str) -> Option<&ProgressCounter> {
285        self.counters.iter().find(|counter| counter.metric_id() == metric_id)
286    }
287
288    /// Creates metric snapshots for all counters in this event.
289    ///
290    /// Each snapshot flattens one counter with the event phase, stage, elapsed
291    /// duration, and complete metric metadata. If a counter references a metric
292    /// id that is not present in the schema, the snapshot uses the metric id as
293    /// both the fallback id and display name.
294    ///
295    /// # Returns
296    ///
297    /// One snapshot per counter carried by this event.
298    pub fn metric_snapshots(&self) -> Vec<ProgressMetricSnapshot> {
299        self.counters
300            .iter()
301            .map(|counter| {
302                let metric = self
303                    .schema
304                    .metric(counter.metric_id())
305                    .cloned()
306                    .unwrap_or_else(|| ProgressMetric::new(counter.metric_id(), counter.metric_id()));
307                ProgressMetricSnapshot::new(metric, self.phase, self.stage.clone(), counter, self.elapsed)
308            })
309            .collect()
310    }
311
312    /// Returns the elapsed duration.
313    ///
314    /// # Returns
315    ///
316    /// The monotonic elapsed duration carried by this event.
317    #[inline]
318    pub const fn elapsed(&self) -> Duration {
319        self.elapsed
320    }
321}