ralph_workflow/logging/
run_id.rs1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct RunId(String);
16
17impl RunId {
18 #[must_use]
22 pub fn new() -> Self {
23 let now = Utc::now();
24 let base = now.format("%Y-%m-%d_%H-%M-%S%.3fZ").to_string();
25 Self(base)
26 }
27
28 #[must_use]
39 pub fn for_test(id: &str) -> Self {
40 Self(id.to_string())
41 }
42
43 #[must_use]
48 pub fn from_checkpoint(id: &str) -> Self {
49 Self(id.to_string())
50 }
51
52 #[must_use]
54 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57
58 #[must_use]
69 pub fn with_collision_counter(&self, counter: u32) -> Self {
70 Self(format!("{}-{:02}", self.0, counter))
71 }
72}
73
74impl fmt::Display for RunId {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 write!(f, "{}", self.0)
77 }
78}
79
80impl Default for RunId {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn test_run_id_format() {
92 let run_id = RunId::new();
93 let s = run_id.as_str();
94
95 assert!(s.len() >= 24, "Run ID should be at least 24 chars");
98 assert!(s.ends_with('Z'), "Run ID should end with Z");
99 assert!(
100 s.contains('_'),
101 "Run ID should contain underscore separator"
102 );
103 assert!(
104 s.contains('-'),
105 "Run ID should contain date/time separators"
106 );
107 assert!(
108 s.contains('.'),
109 "Run ID should contain millisecond separator"
110 );
111 }
112
113 #[test]
114 fn test_run_id_from_checkpoint() {
115 let original = "2026-02-06_14-03-27.123Z";
116 let run_id = RunId::from_checkpoint(original);
117 assert_eq!(run_id.as_str(), original);
118 }
119
120 #[test]
121 fn test_run_id_with_collision_counter() {
122 let base = RunId::new();
123 let collided = base.with_collision_counter(1);
124
125 assert!(
126 collided.as_str().ends_with("-01"),
127 "Collision counter should be appended"
128 );
129 assert!(collided.as_str().starts_with(base.as_str()));
130 }
131
132 #[test]
133 fn test_run_id_display() {
134 let run_id = RunId::new();
135 let displayed = format!("{run_id}");
136 assert_eq!(displayed, run_id.as_str());
137 }
138
139 #[test]
140 fn test_run_id_sortable() {
141 let earlier = RunId::for_test("2024-01-01_00-00-01.000Z");
145 let later = RunId::for_test("2024-01-01_00-00-02.000Z");
146 assert!(
147 earlier.as_str() < later.as_str(),
148 "RunId format should be lexicographically sortable in chronological order"
149 );
150 }
151}