daedalus_core/
ids.rs

1//! Strongly-typed IDs for nodes, ports, edges, channels, runs, and ticks.
2//! Serde encodes them as `"prefix:n"` strings for stability in planner/runtime
3//! diagnostics and golden outputs.
4
5use std::fmt;
6use std::num::NonZeroU64;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10
11use crate::errors::{CoreError, CoreErrorCode};
12
13macro_rules! define_id {
14    ($name:ident, $prefix:literal) => {
15        #[doc = concat!("Strongly-typed ID for `", stringify!($prefix), "` resources.")]
16        #[doc = ""]
17        #[doc = "```"]
18        #[doc = concat!("use daedalus_core::ids::", stringify!($name), ";")]
19        #[doc = concat!("let id = ", stringify!($name), "::try_from(1).unwrap();")]
20        #[doc = concat!("assert_eq!(id.to_string(), \"", $prefix, ":1\");")]
21        #[doc = "```"]
22        #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
23        pub struct $name(NonZeroU64);
24
25        impl $name {
26            pub const PREFIX: &'static str = $prefix;
27
28            pub fn new(raw: NonZeroU64) -> Self {
29                Self(raw)
30            }
31
32            pub fn get(self) -> NonZeroU64 {
33                self.0
34            }
35
36            pub fn into_inner(self) -> NonZeroU64 {
37                self.0
38            }
39        }
40
41        impl fmt::Display for $name {
42            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43                write!(f, "{}:{}", Self::PREFIX, self.0)
44            }
45        }
46
47        impl From<NonZeroU64> for $name {
48            fn from(value: NonZeroU64) -> Self {
49                Self::new(value)
50            }
51        }
52
53        impl TryFrom<u64> for $name {
54            type Error = CoreError;
55
56            fn try_from(value: u64) -> Result<Self, Self::Error> {
57                let Some(nz) = NonZeroU64::new(value) else {
58                    return Err(CoreError::new(
59                        CoreErrorCode::InvalidId,
60                        format!("{} must be non-zero", Self::PREFIX),
61                    ));
62                };
63                Ok(Self::new(nz))
64            }
65        }
66
67        impl FromStr for $name {
68            type Err = CoreError;
69
70            fn from_str(s: &str) -> Result<Self, Self::Err> {
71                let (prefix, rest) = s.split_once(':').ok_or_else(|| {
72                    CoreError::new(CoreErrorCode::InvalidId, format!("missing prefix in {}", s))
73                })?;
74                if prefix != Self::PREFIX {
75                    return Err(CoreError::new(
76                        CoreErrorCode::InvalidId,
77                        format!("expected prefix {} but found {}", Self::PREFIX, prefix),
78                    ));
79                }
80                let raw: u64 = rest.parse().map_err(|_| {
81                    CoreError::new(
82                        CoreErrorCode::InvalidId,
83                        format!("invalid numeric id {}", s),
84                    )
85                })?;
86                Self::try_from(raw)
87            }
88        }
89
90        impl Serialize for $name {
91            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
92            where
93                S: serde::Serializer,
94            {
95                serializer.serialize_str(&self.to_string())
96            }
97        }
98
99        impl<'de> Deserialize<'de> for $name {
100            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
101            where
102                D: serde::Deserializer<'de>,
103            {
104                let s = String::deserialize(deserializer)?;
105                s.parse().map_err(serde::de::Error::custom)
106            }
107        }
108    };
109}
110
111define_id!(NodeId, "node");
112define_id!(PortId, "port");
113define_id!(EdgeId, "edge");
114define_id!(ChannelId, "chan");
115define_id!(RunId, "run");
116define_id!(TickId, "tick");
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use serde_json;
122
123    #[test]
124    fn display_and_parse_round_trip() {
125        let id = NodeId::try_from(42).expect("id");
126        let rendered = id.to_string();
127        assert_eq!(rendered, "node:42");
128        let parsed = rendered.parse::<NodeId>().expect("parse");
129        assert_eq!(parsed, id);
130    }
131
132    #[test]
133    fn rejects_zero() {
134        let err = NodeId::try_from(0).unwrap_err();
135        assert_eq!(err.code(), CoreErrorCode::InvalidId);
136    }
137
138    #[test]
139    fn serde_string_is_stable() {
140        let id = EdgeId::try_from(7).expect("id");
141        let json = serde_json::to_string(&id).expect("serialize");
142        assert_eq!(json, "\"edge:7\"");
143        let back: EdgeId = serde_json::from_str(&json).expect("deserialize");
144        assert_eq!(back, id);
145    }
146}