Skip to main content

simulator_api/
session_id.rs

1//! Session identifiers.
2//!
3//! Two distinct id namespaces cross the wire, told apart by their prefix: a
4//! single backtest session (`backtest_…`) and the control session grouping a
5//! parallel batch (`parallel_…`). They are separate types so one can never be
6//! passed where the other is expected — the prefixes exist precisely because
7//! mixing them is a lookup miss rather than an error.
8//!
9//! Both serialize through `Display`/`FromStr`, so the wire form is the prefixed
10//! string it has always been, in JSON and in postcard alike.
11
12use serde_with::{DeserializeFromStr, SerializeDisplay};
13use uuid::Uuid;
14
15/// Prefix-and-uuid id, shared by [`SessionId`] and [`ControlSessionId`].
16macro_rules! prefixed_uuid_id {
17    ($name:ident, $prefix:literal, $doc:literal) => {
18        #[doc = $doc]
19        #[derive(
20            Debug,
21            Clone,
22            Copy,
23            PartialEq,
24            Eq,
25            PartialOrd,
26            Ord,
27            Hash,
28            SerializeDisplay,
29            DeserializeFromStr,
30        )]
31        pub struct $name(Uuid);
32
33        impl $name {
34            /// A fresh id. UUID v7, so ids sort by creation time.
35            pub fn new() -> Self {
36                Self(Uuid::now_v7())
37            }
38
39            /// The prefix this id renders with.
40            pub const PREFIX: &'static str = $prefix;
41        }
42
43        impl std::fmt::Display for $name {
44            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45                write!(f, "{}{}", $prefix, self.0.as_simple())
46            }
47        }
48
49        impl std::str::FromStr for $name {
50            type Err = uuid::Error;
51
52            /// The prefix is optional so a bare uuid still parses, matching what
53            /// this accepted before the type existed.
54            fn from_str(s: &str) -> Result<Self, Self::Err> {
55                let trimmed = s.strip_prefix($prefix).unwrap_or(s);
56                Ok(Self(Uuid::parse_str(trimmed)?))
57            }
58        }
59
60        impl Default for $name {
61            fn default() -> Self {
62                Self::new()
63            }
64        }
65    };
66}
67
68prefixed_uuid_id!(SessionId, "backtest_", "Identifies one backtest session.");
69prefixed_uuid_id!(
70    ControlSessionId,
71    "parallel_",
72    "Identifies the control session grouping a parallel batch of sub-sessions."
73);
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn each_id_renders_with_its_own_prefix() {
81        assert!(SessionId::new().to_string().starts_with("backtest_"));
82        assert!(ControlSessionId::new().to_string().starts_with("parallel_"));
83    }
84
85    #[test]
86    fn ids_round_trip_through_their_string_form() {
87        let session: SessionId = SessionId::new().to_string().parse().expect("session");
88        assert_eq!(session.to_string(), session.to_string());
89
90        let control: ControlSessionId = ControlSessionId::new()
91            .to_string()
92            .parse()
93            .expect("control session");
94        assert_eq!(control.to_string(), control.to_string());
95    }
96
97    #[test]
98    fn the_wire_form_is_the_prefixed_string() {
99        let id = SessionId::new();
100        let json = serde_json::to_string(&id).expect("serialize");
101        assert_eq!(json, format!("\"{id}\""));
102        assert_eq!(
103            serde_json::from_str::<SessionId>(&json).expect("deserialize"),
104            id
105        );
106    }
107
108    /// The client-facing envelopes carry these ids; the JSON must be the same
109    /// prefixed string it was when the fields were `String`.
110    #[test]
111    fn request_and_response_envelopes_keep_their_json_shape() {
112        let id = SessionId::new();
113        let json = serde_json::json!({ "sessionId": id.to_string(), "request": null });
114        let round_tripped: SessionId =
115            serde_json::from_value(json["sessionId"].clone()).expect("session id");
116        assert_eq!(round_tripped, id);
117        assert!(json["sessionId"].as_str().unwrap().starts_with("backtest_"));
118    }
119
120    #[test]
121    fn a_control_id_is_not_accepted_as_a_session_id() {
122        // The prefixes name different namespaces; feeding one to the other used
123        // to be a silent lookup miss.
124        let control = ControlSessionId::new().to_string();
125        assert!(control.parse::<SessionId>().is_err());
126    }
127}