Skip to main content

aster_forge_tasks/
runtime_metadata.rs

1//! Runtime task metadata registration helpers.
2//!
3//! Product crates still own their runtime task enum and presentation codes. This module only
4//! provides compact definition types plus helpers that generate consistent lookup and wire-format
5//! handling from one registration list. This keeps product-specific task catalogs in the product
6//! crate while Forge owns the boring mechanics: stable string encoding, legacy string fallback, and
7//! display-name derivation for unknown historical task names.
8
9use serde::{Deserialize, Serialize};
10
11/// Product-owned runtime task metadata registered with Forge helpers.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct RuntimeTaskDefinition<Kind, PresentationCode> {
14    /// Product-owned task enum variant.
15    pub kind: Kind,
16    /// Stable wire value stored in task payloads.
17    pub wire_value: &'static str,
18    /// Operator-facing display name.
19    pub display_name: &'static str,
20    /// Product-owned presentation code for UI/i18n mapping.
21    pub presentation_code: PresentationCode,
22}
23
24/// Lookup contract implemented by product-owned runtime task enums.
25///
26/// Implementations should normally delegate to the module generated by
27/// [`crate::runtime_task_registry!`]. Forge uses this trait to provide product-neutral
28/// serialization and display helpers without owning the task enum itself.
29pub trait RegisteredRuntimeTaskKind: Copy {
30    /// Returns the stable wire value stored in task payloads.
31    fn as_str(self) -> &'static str;
32
33    /// Returns the operator-facing display name for this runtime task.
34    fn display_name(self) -> &'static str;
35
36    /// Decodes a runtime task kind from a stable wire value.
37    fn from_wire_value(value: &str) -> Option<Self>;
38}
39
40/// Runtime task name stored in product payloads.
41///
42/// Known names decode to the product-owned task enum. Unknown values are preserved as legacy
43/// strings so old database rows, removed tasks, or future task kinds can still be displayed and
44/// round-tripped instead of failing deserialization.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum RuntimeTaskName<K> {
47    /// A task kind registered by the product crate.
48    Known(K),
49    /// A historical or otherwise unknown wire value.
50    Legacy(String),
51}
52
53impl<K> RuntimeTaskName<K>
54where
55    K: RegisteredRuntimeTaskKind,
56{
57    /// Builds a known task name from a product-owned task kind.
58    pub const fn from_kind(kind: K) -> Self {
59        Self::Known(kind)
60    }
61
62    /// Parses a task name from a stored wire value, preserving unknown values as legacy strings.
63    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    /// Returns the stable wire value.
71    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    /// Returns a display name for operator-facing UI.
79    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    /// Returns the decoded product task kind when this name is known.
87    pub fn known_kind(&self) -> Option<K> {
88        match self {
89            Self::Known(kind) => Some(*kind),
90            Self::Legacy(_) => None,
91        }
92    }
93
94    /// Returns the decoded product task kind when this name is known.
95    ///
96    /// This is an ergonomic alias for [`RuntimeTaskName::known_kind`].
97    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/// Generates product-local runtime task metadata lookup functions.
163///
164/// The generated module exposes:
165///
166/// - `DEFINITIONS`
167/// - `as_str(kind)`
168/// - `display_name(kind)`
169/// - `presentation_code(kind)`
170/// - `from_wire_value(value)`
171///
172/// This keeps runtime task wire values, display names, and presentation codes registered in one
173/// product-local place while preserving product-owned enums and UI codes.
174#[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            /// Registered runtime task definitions in product order.
195            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            /// Returns the stable wire value for a registered runtime task kind.
209            pub const fn as_str(kind: $kind) -> &'static str {
210                match kind {
211                    $(
212                        $kind_value => $wire,
213                    )+
214                }
215            }
216
217            /// Returns the operator-facing display name for a registered runtime task kind.
218            pub const fn display_name(kind: $kind) -> &'static str {
219                match kind {
220                    $(
221                        $kind_value => $display,
222                    )+
223                }
224            }
225
226            /// Returns the product-owned presentation code for a registered runtime task kind.
227            pub const fn presentation_code(kind: $kind) -> $presentation {
228                match kind {
229                    $(
230                        $kind_value => $presentation_value,
231                    )+
232                }
233            }
234
235            /// Decodes a runtime task kind from its stable wire value.
236            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}