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}