simulator-api 0.22.1

Wire-protocol types for the Solana simulator backtest WebSocket API
Documentation
//! Session identifiers.
//!
//! Two distinct id namespaces cross the wire, told apart by their prefix: a
//! single backtest session (`backtest_…`) and the control session grouping a
//! parallel batch (`parallel_…`). They are separate types so one can never be
//! passed where the other is expected — the prefixes exist precisely because
//! mixing them is a lookup miss rather than an error.
//!
//! Both serialize through `Display`/`FromStr`, so the wire form is the prefixed
//! string it has always been, in JSON and in postcard alike.

use serde_with::{DeserializeFromStr, SerializeDisplay};
use uuid::Uuid;

/// Prefix-and-uuid id, shared by [`SessionId`] and [`ControlSessionId`].
macro_rules! prefixed_uuid_id {
    ($name:ident, $prefix:literal, $doc:literal) => {
        #[doc = $doc]
        #[derive(
            Debug,
            Clone,
            Copy,
            PartialEq,
            Eq,
            PartialOrd,
            Ord,
            Hash,
            SerializeDisplay,
            DeserializeFromStr,
        )]
        pub struct $name(Uuid);

        impl $name {
            /// A fresh id. UUID v7, so ids sort by creation time.
            pub fn new() -> Self {
                Self(Uuid::now_v7())
            }

            /// The prefix this id renders with.
            pub const PREFIX: &'static str = $prefix;
        }

        impl std::fmt::Display for $name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{}{}", $prefix, self.0.as_simple())
            }
        }

        impl std::str::FromStr for $name {
            type Err = uuid::Error;

            /// The prefix is optional so a bare uuid still parses, matching what
            /// this accepted before the type existed.
            fn from_str(s: &str) -> Result<Self, Self::Err> {
                let trimmed = s.strip_prefix($prefix).unwrap_or(s);
                Ok(Self(Uuid::parse_str(trimmed)?))
            }
        }

        impl Default for $name {
            fn default() -> Self {
                Self::new()
            }
        }
    };
}

prefixed_uuid_id!(SessionId, "backtest_", "Identifies one backtest session.");
prefixed_uuid_id!(
    ControlSessionId,
    "parallel_",
    "Identifies the control session grouping a parallel batch of sub-sessions."
);

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn each_id_renders_with_its_own_prefix() {
        assert!(SessionId::new().to_string().starts_with("backtest_"));
        assert!(ControlSessionId::new().to_string().starts_with("parallel_"));
    }

    #[test]
    fn ids_round_trip_through_their_string_form() {
        let session: SessionId = SessionId::new().to_string().parse().expect("session");
        assert_eq!(session.to_string(), session.to_string());

        let control: ControlSessionId = ControlSessionId::new()
            .to_string()
            .parse()
            .expect("control session");
        assert_eq!(control.to_string(), control.to_string());
    }

    #[test]
    fn the_wire_form_is_the_prefixed_string() {
        let id = SessionId::new();
        let json = serde_json::to_string(&id).expect("serialize");
        assert_eq!(json, format!("\"{id}\""));
        assert_eq!(
            serde_json::from_str::<SessionId>(&json).expect("deserialize"),
            id
        );
    }

    /// The client-facing envelopes carry these ids; the JSON must be the same
    /// prefixed string it was when the fields were `String`.
    #[test]
    fn request_and_response_envelopes_keep_their_json_shape() {
        let id = SessionId::new();
        let json = serde_json::json!({ "sessionId": id.to_string(), "request": null });
        let round_tripped: SessionId =
            serde_json::from_value(json["sessionId"].clone()).expect("session id");
        assert_eq!(round_tripped, id);
        assert!(json["sessionId"].as_str().unwrap().starts_with("backtest_"));
    }

    #[test]
    fn a_control_id_is_not_accepted_as_a_session_id() {
        // The prefixes name different namespaces; feeding one to the other used
        // to be a silent lookup miss.
        let control = ControlSessionId::new().to_string();
        assert!(control.parse::<SessionId>().is_err());
    }
}