Skip to main content

meerkat_mob/
ids.rs

1//! Newtype identifiers for mob entities.
2//!
3//! These types wrap concrete primitives for compile-time safety.
4
5use serde::{Deserialize, Serialize};
6use std::borrow::Borrow;
7use std::fmt;
8use std::str::FromStr;
9use uuid::Uuid;
10
11/// Unique identifier for a flow run.
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct RunId(Uuid);
15
16impl RunId {
17    pub fn new() -> Self {
18        Self(Uuid::new_v4())
19    }
20
21    pub fn as_uuid(&self) -> &Uuid {
22        &self.0
23    }
24}
25
26impl Default for RunId {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl fmt::Display for RunId {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        self.0.fmt(f)
35    }
36}
37
38impl FromStr for RunId {
39    type Err = uuid::Error;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        Ok(Self(Uuid::parse_str(s)?))
43    }
44}
45
46macro_rules! string_newtype {
47    ($(#[$meta:meta])* $name:ident) => {
48        $(#[$meta])*
49        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
50        #[serde(transparent)]
51        pub struct $name(String);
52
53        impl $name {
54            pub fn as_str(&self) -> &str {
55                &self.0
56            }
57        }
58
59        impl fmt::Display for $name {
60            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61                self.0.fmt(f)
62            }
63        }
64
65        impl From<String> for $name {
66            fn from(value: String) -> Self {
67                Self(value)
68            }
69        }
70
71        impl From<&str> for $name {
72            fn from(value: &str) -> Self {
73                Self(value.to_owned())
74            }
75        }
76
77        impl Borrow<str> for $name {
78            fn borrow(&self) -> &str {
79                &self.0
80            }
81        }
82
83        impl AsRef<str> for $name {
84            fn as_ref(&self) -> &str {
85                &self.0
86            }
87        }
88    };
89}
90
91string_newtype!(
92    /// Unique identifier for a mob instance.
93    MobId
94);
95
96string_newtype!(
97    /// Unique identifier for a flow definition.
98    FlowId
99);
100
101string_newtype!(
102    /// Unique identifier for a step in a flow definition.
103    StepId
104);
105
106string_newtype!(
107    /// Branch group identifier used by mutually-exclusive flow steps.
108    BranchId
109);
110
111string_newtype!(
112    /// Unique identifier for a task in the shared task board.
113    TaskId
114);
115
116string_newtype!(
117    /// Unique identifier for a meerkat (agent instance) within a mob.
118    MeerkatId
119);
120
121string_newtype!(
122    /// Profile name within a mob definition.
123    ProfileName
124);
125
126string_newtype!(
127    /// Runtime identifier for a flow execution frame. One per FrameSpec invocation.
128    FrameId
129);
130
131string_newtype!(
132    /// Runtime identifier for one instance of a repeat_until loop.
133    LoopInstanceId
134);
135
136string_newtype!(
137    /// Lexical identifier for a node within a FrameSpec.
138    FlowNodeId
139);
140
141string_newtype!(
142    /// Lexical identifier for a loop definition within a FrameSpec.
143    LoopId
144);
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_run_id_roundtrip_json() {
152        let run_id = RunId::new();
153        let encoded = serde_json::to_string(&run_id).unwrap();
154        let decoded: RunId = serde_json::from_str(&encoded).unwrap();
155        assert_eq!(decoded, run_id);
156    }
157
158    #[test]
159    fn test_run_id_roundtrip_parse_display() {
160        let run_id = RunId::new();
161        let rendered = run_id.to_string();
162        let reparsed = RunId::from_str(&rendered).unwrap();
163        assert_eq!(reparsed, run_id);
164    }
165
166    #[test]
167    fn test_flow_id_roundtrip_json() {
168        let id = FlowId::from("flow-a");
169        let encoded = serde_json::to_string(&id).unwrap();
170        let decoded: FlowId = serde_json::from_str(&encoded).unwrap();
171        assert_eq!(decoded, id);
172    }
173
174    #[test]
175    fn test_step_id_roundtrip_json() {
176        let id = StepId::from("step-a");
177        let encoded = serde_json::to_string(&id).unwrap();
178        let decoded: StepId = serde_json::from_str(&encoded).unwrap();
179        assert_eq!(decoded, id);
180    }
181
182    #[test]
183    fn test_branch_id_roundtrip_json() {
184        let id = BranchId::from("branch-a");
185        let encoded = serde_json::to_string(&id).unwrap();
186        let decoded: BranchId = serde_json::from_str(&encoded).unwrap();
187        assert_eq!(decoded, id);
188    }
189
190    #[test]
191    fn test_task_id_roundtrip_json() {
192        let id = TaskId::from("task-a");
193        let encoded = serde_json::to_string(&id).unwrap();
194        let decoded: TaskId = serde_json::from_str(&encoded).unwrap();
195        assert_eq!(decoded, id);
196    }
197
198    #[test]
199    fn test_frame_id_roundtrip_json() {
200        let id = FrameId::from("frame-a");
201        let encoded = serde_json::to_string(&id).unwrap();
202        let decoded: FrameId = serde_json::from_str(&encoded).unwrap();
203        assert_eq!(decoded, id);
204    }
205
206    #[test]
207    fn test_loop_instance_id_roundtrip_json() {
208        let id = LoopInstanceId::from("loop-instance-a");
209        let encoded = serde_json::to_string(&id).unwrap();
210        let decoded: LoopInstanceId = serde_json::from_str(&encoded).unwrap();
211        assert_eq!(decoded, id);
212    }
213
214    #[test]
215    fn test_flow_node_id_roundtrip_json() {
216        let id = FlowNodeId::from("node-a");
217        let encoded = serde_json::to_string(&id).unwrap();
218        let decoded: FlowNodeId = serde_json::from_str(&encoded).unwrap();
219        assert_eq!(decoded, id);
220    }
221
222    #[test]
223    fn test_loop_id_roundtrip_json() {
224        let id = LoopId::from("loop-a");
225        let encoded = serde_json::to_string(&id).unwrap();
226        let decoded: LoopId = serde_json::from_str(&encoded).unwrap();
227        assert_eq!(decoded, id);
228    }
229
230    #[test]
231    fn test_existing_ids_roundtrip() {
232        let mob = MobId::from("mob-a");
233        let meerkat = MeerkatId::from("meerkat-a");
234        let profile = ProfileName::from("lead");
235        assert_eq!(
236            serde_json::from_str::<MobId>(&serde_json::to_string(&mob).unwrap()).unwrap(),
237            mob
238        );
239        assert_eq!(
240            serde_json::from_str::<MeerkatId>(&serde_json::to_string(&meerkat).unwrap()).unwrap(),
241            meerkat
242        );
243        assert_eq!(
244            serde_json::from_str::<ProfileName>(&serde_json::to_string(&profile).unwrap()).unwrap(),
245            profile
246        );
247    }
248}