1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct RuntimeTaskDefinition<Kind, PresentationCode> {
14 pub kind: Kind,
16 pub wire_value: &'static str,
18 pub display_name: &'static str,
20 pub presentation_code: PresentationCode,
22}
23
24pub trait RegisteredRuntimeTaskKind: Copy {
30 fn as_str(self) -> &'static str;
32
33 fn display_name(self) -> &'static str;
35
36 fn from_wire_value(value: &str) -> Option<Self>;
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum RuntimeTaskName<K> {
47 Known(K),
49 Legacy(String),
51}
52
53impl<K> RuntimeTaskName<K>
54where
55 K: RegisteredRuntimeTaskKind,
56{
57 pub const fn from_kind(kind: K) -> Self {
59 Self::Known(kind)
60 }
61
62 pub fn parse(value: impl Into<String>) -> Self {
64 let value = value.into();
65 K::from_wire_value(&value)
66 .map(Self::Known)
67 .unwrap_or(Self::Legacy(value))
68 }
69
70 pub fn as_str(&self) -> &str {
72 match self {
73 Self::Known(kind) => kind.as_str(),
74 Self::Legacy(value) => value.as_str(),
75 }
76 }
77
78 pub fn display_name(&self) -> String {
80 match self {
81 Self::Known(kind) => kind.display_name().to_string(),
82 Self::Legacy(value) => value.replace('-', " "),
83 }
84 }
85
86 pub fn known_kind(&self) -> Option<K> {
88 match self {
89 Self::Known(kind) => Some(*kind),
90 Self::Legacy(_) => None,
91 }
92 }
93
94 pub fn known(&self) -> Option<K> {
98 self.known_kind()
99 }
100}
101
102impl<K> From<K> for RuntimeTaskName<K>
103where
104 K: RegisteredRuntimeTaskKind,
105{
106 fn from(value: K) -> Self {
107 Self::Known(value)
108 }
109}
110
111impl<K> From<String> for RuntimeTaskName<K>
112where
113 K: RegisteredRuntimeTaskKind,
114{
115 fn from(value: String) -> Self {
116 Self::parse(value)
117 }
118}
119
120impl<K> From<&str> for RuntimeTaskName<K>
121where
122 K: RegisteredRuntimeTaskKind,
123{
124 fn from(value: &str) -> Self {
125 Self::parse(value)
126 }
127}
128
129impl<K> std::fmt::Display for RuntimeTaskName<K>
130where
131 K: RegisteredRuntimeTaskKind,
132{
133 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 formatter.write_str(self.as_str())
135 }
136}
137
138impl<K> Serialize for RuntimeTaskName<K>
139where
140 K: RegisteredRuntimeTaskKind,
141{
142 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
143 where
144 S: serde::Serializer,
145 {
146 serializer.serialize_str(self.as_str())
147 }
148}
149
150impl<'de, K> Deserialize<'de> for RuntimeTaskName<K>
151where
152 K: RegisteredRuntimeTaskKind,
153{
154 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
155 where
156 D: serde::Deserializer<'de>,
157 {
158 String::deserialize(deserializer).map(Self::parse)
159 }
160}
161
162#[macro_export]
175macro_rules! runtime_task_registry {
176 (
177 $(#[$meta:meta])*
178 $vis:vis mod $module:ident {
179 kind: $kind:ty;
180 presentation: $presentation:ty;
181 tasks {
182 $(
183 $kind_value:path => {
184 wire: $wire:literal,
185 display: $display:literal,
186 presentation: $presentation_value:path $(,)?
187 }
188 ),+ $(,)?
189 }
190 }
191 ) => {
192 $(#[$meta])*
193 $vis mod $module {
194 pub const DEFINITIONS: &[
196 $crate::RuntimeTaskDefinition<$kind, $presentation>
197 ] = &[
198 $(
199 $crate::RuntimeTaskDefinition {
200 kind: $kind_value,
201 wire_value: $wire,
202 display_name: $display,
203 presentation_code: $presentation_value,
204 },
205 )+
206 ];
207
208 pub const fn as_str(kind: $kind) -> &'static str {
210 match kind {
211 $(
212 $kind_value => $wire,
213 )+
214 }
215 }
216
217 pub const fn display_name(kind: $kind) -> &'static str {
219 match kind {
220 $(
221 $kind_value => $display,
222 )+
223 }
224 }
225
226 pub const fn presentation_code(kind: $kind) -> $presentation {
228 match kind {
229 $(
230 $kind_value => $presentation_value,
231 )+
232 }
233 }
234
235 pub fn from_wire_value(value: &str) -> Option<$kind> {
237 match value {
238 $(
239 $wire => Some($kind_value),
240 )+
241 _ => None,
242 }
243 }
244
245 #[cfg(test)]
246 mod runtime_task_metadata_tests {
247 use super::*;
248
249 #[test]
250 fn registered_runtime_task_metadata_is_consistent() {
251 for (index, definition) in DEFINITIONS.iter().enumerate() {
252 assert!(
253 !definition.wire_value.is_empty(),
254 "runtime task wire value at index {index} must not be empty"
255 );
256 assert!(
257 !definition.display_name.is_empty(),
258 "runtime task display name at index {index} must not be empty"
259 );
260 assert_eq!(as_str(definition.kind), definition.wire_value);
261 assert_eq!(display_name(definition.kind), definition.display_name);
262 assert_eq!(presentation_code(definition.kind), definition.presentation_code);
263 assert_eq!(from_wire_value(definition.wire_value), Some(definition.kind));
264 }
265
266 for (left_index, left) in DEFINITIONS.iter().enumerate() {
267 for right in DEFINITIONS.iter().skip(left_index + 1) {
268 assert_ne!(
269 left.wire_value,
270 right.wire_value,
271 "runtime task wire value '{}' is registered more than once",
272 left.wire_value
273 );
274 }
275 }
276 }
277 }
278 }
279 };
280}
281
282#[cfg(test)]
283mod tests {
284 use super::{RegisteredRuntimeTaskKind, RuntimeTaskName};
285
286 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
287 enum TestRuntimeTaskKind {
288 Cleanup,
289 Health,
290 }
291
292 impl RegisteredRuntimeTaskKind for TestRuntimeTaskKind {
293 fn as_str(self) -> &'static str {
294 match self {
295 Self::Cleanup => "cleanup",
296 Self::Health => "system-health",
297 }
298 }
299
300 fn display_name(self) -> &'static str {
301 match self {
302 Self::Cleanup => "Cleanup",
303 Self::Health => "System health",
304 }
305 }
306
307 fn from_wire_value(value: &str) -> Option<Self> {
308 match value {
309 "cleanup" => Some(Self::Cleanup),
310 "system-health" => Some(Self::Health),
311 _ => None,
312 }
313 }
314 }
315
316 #[test]
317 fn runtime_task_name_round_trips_known_values() {
318 let name = RuntimeTaskName::<TestRuntimeTaskKind>::from("system-health");
319
320 assert_eq!(name.known_kind(), Some(TestRuntimeTaskKind::Health));
321 assert_eq!(name.as_str(), "system-health");
322 assert_eq!(name.display_name(), "System health");
323 assert_eq!(name.to_string(), "system-health");
324 assert_eq!(
325 serde_json::to_string(&name).expect("known task name should serialize"),
326 r#""system-health""#
327 );
328 }
329
330 #[test]
331 fn runtime_task_name_preserves_legacy_values() {
332 let name = RuntimeTaskName::<TestRuntimeTaskKind>::from("old-maintenance-job");
333
334 assert_eq!(name.known_kind(), None);
335 assert_eq!(name.as_str(), "old-maintenance-job");
336 assert_eq!(name.display_name(), "old maintenance job");
337 assert_eq!(
338 serde_json::to_string(&name).expect("legacy task name should serialize"),
339 r#""old-maintenance-job""#
340 );
341 }
342
343 #[test]
344 fn runtime_task_name_deserializes_known_and_legacy_values() {
345 let known: RuntimeTaskName<TestRuntimeTaskKind> =
346 serde_json::from_str(r#""cleanup""#).expect("known task name should deserialize");
347 let legacy: RuntimeTaskName<TestRuntimeTaskKind> =
348 serde_json::from_str(r#""removed-task""#).expect("legacy task name should deserialize");
349
350 assert_eq!(known.known_kind(), Some(TestRuntimeTaskKind::Cleanup));
351 assert_eq!(legacy.known_kind(), None);
352 assert_eq!(legacy.as_str(), "removed-task");
353 }
354}