Skip to main content

qubit_progress/model/
progress_schema.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::collections::HashSet;
11
12use serde::{
13    Deserialize,
14    Serialize,
15};
16
17use super::{
18    ProgressCounter,
19    ProgressMetric,
20};
21
22/// Metric dictionary for one logical progress operation.
23///
24/// A schema defines which metric ids are valid for the operation. Events carry
25/// a schema so every serialized progress event is self-describing and reporters
26/// can resolve metric ids to display names without external state.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct ProgressSchema {
29    /// Metric definitions available in this progress stream.
30    metrics: Vec<ProgressMetric>,
31}
32
33impl ProgressSchema {
34    /// Creates a schema from metric definitions.
35    ///
36    /// # Parameters
37    ///
38    /// * `metrics` - Metric definitions available to events using this schema.
39    ///
40    /// # Returns
41    ///
42    /// A schema containing the supplied metric definitions.
43    #[inline]
44    pub fn new(metrics: Vec<ProgressMetric>) -> Self {
45        Self { metrics }
46    }
47
48    /// Creates a schema containing one metric.
49    ///
50    /// # Parameters
51    ///
52    /// * `id` - Stable metric identifier.
53    /// * `name` - Human-readable metric name.
54    ///
55    /// # Returns
56    ///
57    /// A schema with a single metric definition.
58    #[inline]
59    pub fn single(id: &str, name: &str) -> Self {
60        Self::new(vec![ProgressMetric::new(id, name)])
61    }
62
63    /// Returns all metric definitions.
64    ///
65    /// # Returns
66    ///
67    /// A slice of metrics declared by this schema.
68    #[inline]
69    pub fn metrics(&self) -> &[ProgressMetric] {
70        self.metrics.as_slice()
71    }
72
73    /// Finds a metric by id.
74    ///
75    /// # Parameters
76    ///
77    /// * `id` - Metric identifier to search for.
78    ///
79    /// # Returns
80    ///
81    /// `Some(metric)` when the schema contains the id, otherwise `None`.
82    #[inline]
83    pub fn metric(&self, id: &str) -> Option<&ProgressMetric> {
84        self.metrics.iter().find(|metric| metric.id() == id)
85    }
86
87    /// Finds a metric display name by id.
88    ///
89    /// # Parameters
90    ///
91    /// * `id` - Metric identifier to search for.
92    ///
93    /// # Returns
94    ///
95    /// `Some(name)` when the schema contains the id, otherwise `None`.
96    #[inline]
97    pub fn metric_name(&self, id: &str) -> Option<&str> {
98        self.metric(id).map(ProgressMetric::name)
99    }
100
101    /// Checks whether the schema contains a metric id.
102    ///
103    /// # Parameters
104    ///
105    /// * `id` - Metric identifier to test.
106    ///
107    /// # Returns
108    ///
109    /// `true` when this schema contains `id`.
110    #[inline]
111    pub fn contains_metric(&self, id: &str) -> bool {
112        self.metric(id).is_some()
113    }
114
115    /// Validates one counter against this schema.
116    ///
117    /// # Parameters
118    ///
119    /// * `counter` - Counter to validate.
120    ///
121    /// # Returns
122    ///
123    /// `true` when `counter.metric_id()` exists in this schema.
124    #[inline]
125    pub fn validate_counter(&self, counter: &ProgressCounter) -> bool {
126        self.contains_metric(counter.metric_id())
127    }
128
129    /// Validates counters against this schema.
130    ///
131    /// Validation is intentionally light-weight: every counter metric id must
132    /// exist in the schema and each metric id may appear at most once in the
133    /// event. Numeric relationships are not validated because many operations
134    /// discover totals dynamically or apply retry and compensation logic.
135    ///
136    /// # Parameters
137    ///
138    /// * `counters` - Counters to validate.
139    ///
140    /// # Returns
141    ///
142    /// `true` when all counters reference declared metrics and no metric id is
143    /// duplicated.
144    pub fn validate_counters(&self, counters: &[ProgressCounter]) -> bool {
145        let mut seen = HashSet::with_capacity(counters.len());
146        counters
147            .iter()
148            .all(|counter| self.validate_counter(counter) && seen.insert(counter.metric_id()))
149    }
150}
151
152impl Default for ProgressSchema {
153    /// Creates an empty schema.
154    ///
155    /// # Returns
156    ///
157    /// A schema without declared metrics.
158    #[inline]
159    fn default() -> Self {
160        Self::new(Vec::new())
161    }
162}