1use std::fmt;
13use std::str::FromStr;
14
15use serde::{Deserialize, Serialize};
16use ulid::Ulid;
17
18use crate::digest::{Digest, DigestParseError};
19
20#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
26#[serde(transparent)]
27pub struct StepId(Digest);
28
29impl StepId {
30 pub const fn new(digest: Digest) -> Self {
32 Self(digest)
33 }
34
35 pub const fn digest(&self) -> &Digest {
37 &self.0
38 }
39}
40
41impl fmt::Display for StepId {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "step:{}", self.0)
44 }
45}
46
47impl fmt::Debug for StepId {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 write!(f, "StepId({}…)", self.0.short(12))
50 }
51}
52
53impl FromStr for StepId {
54 type Err = DigestParseError;
55
56 fn from_str(s: &str) -> Result<Self, Self::Err> {
57 Ok(Self(Digest::from_str(
58 s.strip_prefix("step:").unwrap_or(s),
59 )?))
60 }
61}
62
63#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
69#[serde(transparent)]
70pub struct RunId(Ulid);
71
72impl RunId {
73 pub fn new() -> Self {
75 Self(Ulid::new())
76 }
77
78 pub const fn from_ulid(ulid: Ulid) -> Self {
80 Self(ulid)
81 }
82
83 pub const fn ulid(&self) -> Ulid {
85 self.0
86 }
87}
88
89impl Default for RunId {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl fmt::Display for RunId {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "run:{}", self.0)
98 }
99}
100
101impl fmt::Debug for RunId {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(f, "RunId({})", self.0)
104 }
105}
106
107impl FromStr for RunId {
108 type Err = ulid::DecodeError;
109
110 fn from_str(s: &str) -> Result<Self, Self::Err> {
111 Ok(Self(Ulid::from_string(
112 s.strip_prefix("run:").unwrap_or(s),
113 )?))
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::canonical::Canonical;
121
122 #[test]
123 fn step_id_is_content_addressed_and_stable() {
124 let spec = ("reasoner-a", vec!["tool-x"], "checker-y");
125 let a = StepId::new(spec.digest().unwrap());
126 let b = StepId::new(spec.digest().unwrap());
127 assert_eq!(a, b, "the same step specification must yield the same id");
128 }
129
130 #[test]
131 fn different_specs_give_different_step_ids() {
132 let a = StepId::new(("reasoner-a",).digest().unwrap());
133 let b = StepId::new(("reasoner-b",).digest().unwrap());
134 assert_ne!(a, b);
135 }
136
137 #[test]
138 fn run_ids_are_unique() {
139 let first = RunId::new();
140 let second = RunId::new();
141 assert_ne!(first, second);
142 }
143
144 #[test]
145 fn run_ids_sort_chronologically() {
146 use std::time::{Duration, SystemTime};
147
148 let t0 = SystemTime::now();
149 let earlier = RunId::from_ulid(Ulid::from_datetime(t0));
150 let later = RunId::from_ulid(Ulid::from_datetime(t0 + Duration::from_millis(1)));
151
152 assert!(
153 earlier < later,
154 "a later run must sort after an earlier one"
155 );
156 }
157
158 #[test]
159 fn run_ids_never_go_backwards_in_time() {
160 let first = RunId::new();
161 let second = RunId::new();
162 assert!(first.ulid().timestamp_ms() <= second.ulid().timestamp_ms());
163 }
164
165 #[test]
173 fn ids_round_trip_through_strings() {
174 let step = StepId::new(Digest::of(b"x"));
175 assert_eq!(StepId::from_str(&step.to_string()).unwrap(), step);
176
177 let run = RunId::new();
178 assert_eq!(RunId::from_str(&run.to_string()).unwrap(), run);
179 }
180
181 #[test]
182 fn ids_are_prefixed_so_they_cannot_be_confused_in_logs() {
183 assert!(
184 StepId::new(Digest::of(b"x"))
185 .to_string()
186 .starts_with("step:")
187 );
188 assert!(RunId::new().to_string().starts_with("run:"));
189 }
190}