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}