hyperactor/
id.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Universal identifier types for the actor system.
10//!
11//! [`Label`] is an RFC 1035 label: up to 63 lowercase alphanumeric characters
12//! plus hyphens, starting with a letter and ending with an alphanumeric.
13//!
14//! [`Uid`] is either a singleton (identified by label) or an instance
15//! (identified by a random `u64`).
16
17use std::cmp::Ordering;
18use std::fmt;
19use std::hash::Hash;
20use std::hash::Hasher;
21use std::str::FromStr;
22
23use rand::RngCore as _;
24use serde::Deserialize;
25use serde::Serialize;
26use smol_str::SmolStr;
27
28use crate::port::Port;
29
30/// Maximum length of an RFC 1035 label.
31const MAX_LABEL_LEN: usize = 63;
32
33/// An RFC 1035 label: 1–63 chars, lowercase ASCII alphanumeric plus `-`,
34/// starting with a letter, ending with an alphanumeric character.
35#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub struct Label(SmolStr);
37
38/// Errors that can occur when constructing a [`Label`].
39#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
40pub enum LabelError {
41    /// The input string is empty.
42    #[error("label must not be empty")]
43    Empty,
44    /// The input exceeds 63 characters.
45    #[error("label exceeds 63 characters")]
46    TooLong,
47    /// The first character is not an ASCII lowercase letter.
48    #[error("label must start with a lowercase letter")]
49    InvalidStart,
50    /// The last character is not alphanumeric.
51    #[error("label must end with a lowercase letter or digit")]
52    InvalidEnd,
53    /// The input contains a character that is not lowercase alphanumeric or `-`.
54    #[error("label contains invalid character '{0}'")]
55    InvalidChar(char),
56}
57
58impl Label {
59    /// Validate and construct a new [`Label`].
60    pub fn new(s: &str) -> Result<Self, LabelError> {
61        if s.is_empty() {
62            return Err(LabelError::Empty);
63        }
64        if s.len() > MAX_LABEL_LEN {
65            return Err(LabelError::TooLong);
66        }
67        let first = s.as_bytes()[0];
68        if !first.is_ascii_lowercase() {
69            return Err(LabelError::InvalidStart);
70        }
71        let last = s.as_bytes()[s.len() - 1];
72        if !last.is_ascii_lowercase() && !last.is_ascii_digit() {
73            return Err(LabelError::InvalidEnd);
74        }
75        for ch in s.chars() {
76            if !ch.is_ascii_lowercase() && !ch.is_ascii_digit() && ch != '-' {
77                return Err(LabelError::InvalidChar(ch));
78            }
79        }
80        Ok(Self(SmolStr::new(s)))
81    }
82
83    /// Sanitize arbitrary input into a valid [`Label`].
84    ///
85    /// Lowercases, strips illegal characters, strips leading non-alpha and
86    /// trailing non-alphanumeric characters, and truncates to 63 chars.
87    /// Returns `"nil"` if the result would be empty.
88    pub fn strip(s: &str) -> Self {
89        let lowered: String = s
90            .chars()
91            .filter_map(|ch| {
92                let ch = ch.to_ascii_lowercase();
93                if ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' {
94                    Some(ch)
95                } else {
96                    None
97                }
98            })
99            .collect();
100
101        // Strip leading non-alpha characters.
102        let trimmed = lowered.trim_start_matches(|c: char| !c.is_ascii_lowercase());
103        // Strip trailing non-alphanumeric characters.
104        let trimmed =
105            trimmed.trim_end_matches(|c: char| !c.is_ascii_lowercase() && !c.is_ascii_digit());
106
107        if trimmed.is_empty() {
108            return Self(SmolStr::new("nil"));
109        }
110
111        let truncated = if trimmed.len() > MAX_LABEL_LEN {
112            // Re-trim trailing after truncation.
113            let t = &trimmed[..MAX_LABEL_LEN];
114            t.trim_end_matches(|c: char| !c.is_ascii_lowercase() && !c.is_ascii_digit())
115        } else {
116            trimmed
117        };
118
119        if truncated.is_empty() {
120            Self(SmolStr::new("nil"))
121        } else {
122            Self(SmolStr::new(truncated))
123        }
124    }
125
126    /// Returns the label as a string slice.
127    pub fn as_str(&self) -> &str {
128        &self.0
129    }
130}
131
132impl fmt::Debug for Label {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "Label({:?})", self.0.as_str())
135    }
136}
137
138impl fmt::Display for Label {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        f.write_str(&self.0)
141    }
142}
143
144impl FromStr for Label {
145    type Err = LabelError;
146
147    fn from_str(s: &str) -> Result<Self, Self::Err> {
148        Self::new(s)
149    }
150}
151
152impl Serialize for Label {
153    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
154        self.0.as_str().serialize(serializer)
155    }
156}
157
158impl<'de> Deserialize<'de> for Label {
159    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
160        let s = String::deserialize(deserializer)?;
161        Label::new(&s).map_err(serde::de::Error::custom)
162    }
163}
164
165/// A unique identifier: either a labeled singleton or a random instance.
166#[derive(Clone)]
167pub enum Uid {
168    /// A singleton identified by label.
169    Singleton(Label),
170    /// An instance identified by a random u64.
171    Instance(u64),
172}
173
174/// Errors that can occur when parsing a [`Uid`] from a string.
175#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
176pub enum UidParseError {
177    /// Error parsing the label component.
178    #[error("invalid label: {0}")]
179    InvalidLabel(#[from] LabelError),
180    /// The hex uid portion is invalid.
181    #[error("invalid hex uid: {0}")]
182    InvalidHex(String),
183}
184
185impl Uid {
186    /// Create a fresh instance with a random uid.
187    pub fn instance() -> Self {
188        let uid = rand::thread_rng().next_u64();
189        Uid::Instance(uid)
190    }
191
192    /// Create a singleton with the given label.
193    pub fn singleton(label: Label) -> Self {
194        Uid::Singleton(label)
195    }
196}
197
198impl PartialEq for Uid {
199    fn eq(&self, other: &Self) -> bool {
200        match (self, other) {
201            (Uid::Singleton(a), Uid::Singleton(b)) => a == b,
202            (Uid::Instance(a), Uid::Instance(b)) => a == b,
203            _ => false,
204        }
205    }
206}
207
208impl Eq for Uid {}
209
210impl Hash for Uid {
211    fn hash<H: Hasher>(&self, state: &mut H) {
212        std::mem::discriminant(self).hash(state);
213        match self {
214            Uid::Singleton(label) => label.hash(state),
215            Uid::Instance(uid) => uid.hash(state),
216        }
217    }
218}
219
220impl PartialOrd for Uid {
221    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
222        Some(self.cmp(other))
223    }
224}
225
226impl Ord for Uid {
227    fn cmp(&self, other: &Self) -> Ordering {
228        match (self, other) {
229            (Uid::Singleton(a), Uid::Singleton(b)) => a.cmp(b),
230            (Uid::Singleton(_), Uid::Instance(_)) => Ordering::Less,
231            (Uid::Instance(_), Uid::Singleton(_)) => Ordering::Greater,
232            (Uid::Instance(a), Uid::Instance(b)) => a.cmp(b),
233        }
234    }
235}
236
237/// Displays as `_label` (singleton) or `hex16` (instance), where hex16 is
238/// a zero-padded 16-character lowercase hex string.
239impl fmt::Debug for Uid {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        match self {
242            Uid::Singleton(label) => write!(f, "Uid(_{})", label),
243            Uid::Instance(uid) => write!(f, "Uid({:016x})", uid),
244        }
245    }
246}
247
248impl fmt::Display for Uid {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            Uid::Singleton(label) => write!(f, "_{label}"),
252            Uid::Instance(uid) => write!(f, "{uid:016x}"),
253        }
254    }
255}
256
257/// Parses `_label` as singleton, bare hex as instance.
258impl FromStr for Uid {
259    type Err = UidParseError;
260
261    fn from_str(s: &str) -> Result<Self, Self::Err> {
262        if let Some(rest) = s.strip_prefix('_') {
263            let label = Label::new(rest)?;
264            return Ok(Uid::Singleton(label));
265        }
266        let uid = parse_hex_uid(s)?;
267        Ok(Uid::Instance(uid))
268    }
269}
270
271fn parse_hex_uid(s: &str) -> Result<u64, UidParseError> {
272    if s.is_empty() || s.len() > 16 {
273        return Err(UidParseError::InvalidHex(s.to_string()));
274    }
275    for ch in s.chars() {
276        if !ch.is_ascii_hexdigit() {
277            return Err(UidParseError::InvalidHex(s.to_string()));
278        }
279    }
280    u64::from_str_radix(s, 16).map_err(|_| UidParseError::InvalidHex(s.to_string()))
281}
282
283impl Serialize for Uid {
284    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
285        serializer.serialize_str(&self.to_string())
286    }
287}
288
289impl<'de> Deserialize<'de> for Uid {
290    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
291        let s = String::deserialize(deserializer)?;
292        Uid::from_str(&s).map_err(serde::de::Error::custom)
293    }
294}
295
296/// Errors that can occur when parsing a [`ProcId`] or [`ActorId`] from a string.
297#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
298pub enum IdParseError {
299    /// Error parsing a [`ProcId`].
300    #[error("invalid proc id: {0}")]
301    InvalidProcId(#[from] UidParseError),
302    /// Error parsing an [`ActorId`] (missing `.` separator).
303    #[error("invalid actor id: expected format `<actor_uid>.<proc_uid>`")]
304    InvalidActorIdFormat,
305    /// Error parsing the actor uid component of an [`ActorId`].
306    #[error("invalid actor uid: {0}")]
307    InvalidActorUid(UidParseError),
308    /// Error parsing the proc uid component of an [`ActorId`].
309    #[error("invalid proc uid in actor id: {0}")]
310    InvalidActorProcUid(UidParseError),
311    /// The `<actor_id>:<port>` separator is missing.
312    #[error("invalid port id: expected format `<actor_id>:<port>`")]
313    InvalidPortIdFormat,
314    /// The port component is invalid.
315    #[error("invalid port: {0}")]
316    InvalidPort(String),
317}
318
319/// Identifies a process in the actor system.
320///
321/// Identity (Eq, Hash, Ord) is determined solely by `uid`; `label` is
322/// informational and excluded from comparisons.
323#[derive(Clone, Serialize, Deserialize)]
324pub struct ProcId {
325    uid: Uid,
326    label: Option<Label>,
327}
328
329impl ProcId {
330    /// Create a new [`ProcId`].
331    pub fn new(uid: Uid, label: Option<Label>) -> Self {
332        Self { uid, label }
333    }
334
335    /// Returns the uid.
336    pub fn uid(&self) -> &Uid {
337        &self.uid
338    }
339
340    /// Returns the label.
341    pub fn label(&self) -> Option<&Label> {
342        self.label.as_ref()
343    }
344}
345
346impl PartialEq for ProcId {
347    fn eq(&self, other: &Self) -> bool {
348        self.uid == other.uid
349    }
350}
351
352impl Eq for ProcId {}
353
354impl Hash for ProcId {
355    fn hash<H: Hasher>(&self, state: &mut H) {
356        self.uid.hash(state);
357    }
358}
359
360impl PartialOrd for ProcId {
361    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
362        Some(self.cmp(other))
363    }
364}
365
366impl Ord for ProcId {
367    fn cmp(&self, other: &Self) -> Ordering {
368        self.uid.cmp(&other.uid)
369    }
370}
371
372impl fmt::Display for ProcId {
373    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374        write!(f, "{}", self.uid)
375    }
376}
377
378impl fmt::Debug for ProcId {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        match &self.label {
381            Some(label) => write!(f, "<'{}' {}>", label, self.uid),
382            None => write!(f, "<{}>", self.uid),
383        }
384    }
385}
386
387impl FromStr for ProcId {
388    type Err = IdParseError;
389
390    fn from_str(s: &str) -> Result<Self, Self::Err> {
391        let uid: Uid = s.parse()?;
392        Ok(Self { uid, label: None })
393    }
394}
395
396/// Identifies an actor within a process.
397///
398/// Identity (Eq, Hash, Ord) is determined by `(proc_id, uid)`; `label` is
399/// informational and excluded from comparisons.
400#[derive(Clone, Serialize, Deserialize)]
401pub struct ActorId {
402    uid: Uid,
403    proc_id: ProcId,
404    label: Option<Label>,
405}
406
407impl ActorId {
408    /// Create a new [`ActorId`].
409    pub fn new(uid: Uid, proc_id: ProcId, label: Option<Label>) -> Self {
410        Self {
411            uid,
412            proc_id,
413            label,
414        }
415    }
416
417    /// Returns the uid.
418    pub fn uid(&self) -> &Uid {
419        &self.uid
420    }
421
422    /// Returns the proc id.
423    pub fn proc_id(&self) -> &ProcId {
424        &self.proc_id
425    }
426
427    /// Returns the label.
428    pub fn label(&self) -> Option<&Label> {
429        self.label.as_ref()
430    }
431}
432
433impl PartialEq for ActorId {
434    fn eq(&self, other: &Self) -> bool {
435        self.proc_id == other.proc_id && self.uid == other.uid
436    }
437}
438
439impl Eq for ActorId {}
440
441impl Hash for ActorId {
442    fn hash<H: Hasher>(&self, state: &mut H) {
443        self.proc_id.hash(state);
444        self.uid.hash(state);
445    }
446}
447
448impl PartialOrd for ActorId {
449    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
450        Some(self.cmp(other))
451    }
452}
453
454impl Ord for ActorId {
455    fn cmp(&self, other: &Self) -> Ordering {
456        self.proc_id
457            .cmp(&other.proc_id)
458            .then_with(|| self.uid.cmp(&other.uid))
459    }
460}
461
462impl fmt::Display for ActorId {
463    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
464        write!(f, "{}.{}", self.uid, self.proc_id.uid)
465    }
466}
467
468impl fmt::Debug for ActorId {
469    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470        match (&self.label, &self.proc_id.label) {
471            (Some(actor_label), Some(proc_label)) => {
472                write!(
473                    f,
474                    "<'{}.{}' {}.{}>",
475                    actor_label, proc_label, self.uid, self.proc_id.uid
476                )
477            }
478            (Some(actor_label), None) => {
479                write!(f, "<'{}' {}.{}>", actor_label, self.uid, self.proc_id.uid)
480            }
481            (None, Some(proc_label)) => {
482                write!(f, "<'.{}' {}.{}>", proc_label, self.uid, self.proc_id.uid)
483            }
484            (None, None) => {
485                write!(f, "<{}.{}>", self.uid, self.proc_id.uid)
486            }
487        }
488    }
489}
490
491impl FromStr for ActorId {
492    type Err = IdParseError;
493
494    fn from_str(s: &str) -> Result<Self, Self::Err> {
495        let dot = s.find('.').ok_or(IdParseError::InvalidActorIdFormat)?;
496        let actor_part = &s[..dot];
497        let proc_part = &s[dot + 1..];
498
499        let actor_uid: Uid = actor_part.parse().map_err(IdParseError::InvalidActorUid)?;
500        let proc_uid: Uid = proc_part
501            .parse()
502            .map_err(IdParseError::InvalidActorProcUid)?;
503
504        Ok(Self {
505            uid: actor_uid,
506            proc_id: ProcId {
507                uid: proc_uid,
508                label: None,
509            },
510            label: None,
511        })
512    }
513}
514
515/// Identifies a port on an actor.
516///
517/// Identity (Eq, Hash, Ord) is determined by `(actor_id, port)`.
518#[derive(Clone, Serialize, Deserialize)]
519pub struct PortId {
520    actor_id: ActorId,
521    port: Port,
522}
523
524impl PortId {
525    /// Create a new [`PortId`].
526    pub fn new(actor_id: ActorId, port: Port) -> Self {
527        Self { actor_id, port }
528    }
529
530    /// Returns the actor id.
531    pub fn actor_id(&self) -> &ActorId {
532        &self.actor_id
533    }
534
535    /// Returns the port.
536    pub fn port(&self) -> Port {
537        self.port
538    }
539
540    /// Returns the proc id (delegates to actor_id).
541    pub fn proc_id(&self) -> &ProcId {
542        self.actor_id.proc_id()
543    }
544}
545
546impl PartialEq for PortId {
547    fn eq(&self, other: &Self) -> bool {
548        self.actor_id == other.actor_id && self.port == other.port
549    }
550}
551
552impl Eq for PortId {}
553
554impl Hash for PortId {
555    fn hash<H: Hasher>(&self, state: &mut H) {
556        self.actor_id.hash(state);
557        self.port.hash(state);
558    }
559}
560
561impl PartialOrd for PortId {
562    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
563        Some(self.cmp(other))
564    }
565}
566
567impl Ord for PortId {
568    fn cmp(&self, other: &Self) -> Ordering {
569        self.actor_id
570            .cmp(&other.actor_id)
571            .then_with(|| self.port.cmp(&other.port))
572    }
573}
574
575impl fmt::Display for PortId {
576    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577        write!(f, "{}:{}", self.actor_id, self.port)
578    }
579}
580
581impl fmt::Debug for PortId {
582    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583        match (self.actor_id.label(), self.actor_id.proc_id().label()) {
584            (Some(actor_label), Some(proc_label)) => {
585                write!(
586                    f,
587                    "<'{}.{}' {}:{}>",
588                    actor_label, proc_label, self.actor_id, self.port
589                )
590            }
591            (Some(actor_label), None) => {
592                write!(f, "<'{}' {}:{}>", actor_label, self.actor_id, self.port)
593            }
594            (None, Some(proc_label)) => {
595                write!(f, "<'.{}' {}:{}>", proc_label, self.actor_id, self.port)
596            }
597            (None, None) => {
598                write!(f, "<{}:{}>", self.actor_id, self.port)
599            }
600        }
601    }
602}
603
604impl FromStr for PortId {
605    type Err = IdParseError;
606
607    fn from_str(s: &str) -> Result<Self, Self::Err> {
608        let colon = s.rfind(':').ok_or(IdParseError::InvalidPortIdFormat)?;
609        let actor_part = &s[..colon];
610        let port_part = &s[colon + 1..];
611
612        let actor_id: ActorId = actor_part.parse()?;
613        let port: Port = port_part
614            .parse()
615            .map_err(|_| IdParseError::InvalidPort(port_part.to_string()))?;
616
617        Ok(Self { actor_id, port })
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    #[test]
626    fn test_label_valid() {
627        assert!(Label::new("a").is_ok());
628        assert!(Label::new("abc").is_ok());
629        assert!(Label::new("my-service").is_ok());
630        assert!(Label::new("a1").is_ok());
631        assert!(Label::new("abc123").is_ok());
632        assert!(Label::new("a-b-c").is_ok());
633    }
634
635    #[test]
636    fn test_label_invalid_empty() {
637        assert_eq!(Label::new(""), Err(LabelError::Empty));
638    }
639
640    #[test]
641    fn test_label_invalid_too_long() {
642        let long = "a".repeat(64);
643        assert_eq!(Label::new(&long), Err(LabelError::TooLong));
644        // Exactly 63 is fine.
645        let exact = "a".repeat(63);
646        assert!(Label::new(&exact).is_ok());
647    }
648
649    #[test]
650    fn test_label_invalid_bad_start() {
651        assert_eq!(Label::new("1abc"), Err(LabelError::InvalidStart));
652        assert_eq!(Label::new("-abc"), Err(LabelError::InvalidStart));
653        assert_eq!(Label::new("Abc"), Err(LabelError::InvalidStart));
654    }
655
656    #[test]
657    fn test_label_invalid_bad_end() {
658        assert_eq!(Label::new("abc-"), Err(LabelError::InvalidEnd));
659    }
660
661    #[test]
662    fn test_label_invalid_char() {
663        assert_eq!(Label::new("ab_c"), Err(LabelError::InvalidChar('_')));
664        assert_eq!(Label::new("ab.c"), Err(LabelError::InvalidChar('.')));
665        assert_eq!(Label::new("aBc"), Err(LabelError::InvalidChar('B')));
666    }
667
668    #[test]
669    fn test_label_strip() {
670        assert_eq!(Label::strip("Hello-World").as_str(), "hello-world");
671        assert_eq!(Label::strip("123abc").as_str(), "abc");
672        assert_eq!(Label::strip("---abc---").as_str(), "abc");
673        assert_eq!(Label::strip("").as_str(), "nil");
674        assert_eq!(Label::strip("123").as_str(), "nil");
675        assert_eq!(Label::strip("My_Service!").as_str(), "myservice");
676    }
677
678    #[test]
679    fn test_label_strip_truncation() {
680        let long = format!("a{}", "b".repeat(100));
681        let stripped = Label::strip(&long);
682        assert!(stripped.as_str().len() <= MAX_LABEL_LEN);
683    }
684
685    #[test]
686    fn test_label_display_fromstr_roundtrip() {
687        let label = Label::new("my-service").unwrap();
688        let s = label.to_string();
689        assert_eq!(s, "my-service");
690        let parsed: Label = s.parse().unwrap();
691        assert_eq!(label, parsed);
692    }
693
694    #[test]
695    fn test_label_serde_roundtrip() {
696        let label = Label::new("my-service").unwrap();
697        let json = serde_json::to_string(&label).unwrap();
698        assert_eq!(json, "\"my-service\"");
699        let parsed: Label = serde_json::from_str(&json).unwrap();
700        assert_eq!(label, parsed);
701    }
702
703    #[test]
704    fn test_singleton_display_parse() {
705        let uid = Uid::singleton(Label::new("my-actor").unwrap());
706        let s = uid.to_string();
707        assert_eq!(s, "_my-actor");
708        let parsed: Uid = s.parse().unwrap();
709        assert_eq!(uid, parsed);
710    }
711
712    #[test]
713    fn test_instance_display_parse() {
714        let uid = Uid::Instance(0xd5d54d7201103869);
715        let s = uid.to_string();
716        assert_eq!(s, "d5d54d7201103869");
717        let parsed: Uid = s.parse().unwrap();
718        assert_eq!(uid, parsed);
719    }
720
721    #[test]
722    fn test_ordering_singleton_lt_instance() {
723        let singleton = Uid::singleton(Label::new("zzz").unwrap());
724        let instance = Uid::Instance(0);
725        assert!(singleton < instance);
726    }
727
728    #[test]
729    fn test_ordering_singletons() {
730        let a = Uid::singleton(Label::new("aaa").unwrap());
731        let b = Uid::singleton(Label::new("bbb").unwrap());
732        assert!(a < b);
733    }
734
735    #[test]
736    fn test_ordering_instances() {
737        let a = Uid::Instance(1);
738        let b = Uid::Instance(2);
739        assert!(a < b);
740    }
741
742    #[test]
743    fn test_uid_serde_roundtrip() {
744        let uids = vec![
745            Uid::singleton(Label::new("my-actor").unwrap()),
746            Uid::Instance(0xabcdef0123456789),
747            Uid::Instance(1),
748        ];
749        for uid in uids {
750            let json = serde_json::to_string(&uid).unwrap();
751            let parsed: Uid = serde_json::from_str(&json).unwrap();
752            assert_eq!(uid, parsed);
753        }
754    }
755
756    #[test]
757    fn test_uid_parse_errors() {
758        // Empty string is invalid hex.
759        assert!("".parse::<Uid>().is_err());
760        // Invalid singleton label.
761        assert!("_".parse::<Uid>().is_err());
762        assert!("_123bad".parse::<Uid>().is_err());
763        // Invalid hex.
764        assert!("xyz".parse::<Uid>().is_err());
765        // Hex too long.
766        assert!("00000000000000001".parse::<Uid>().is_err());
767    }
768
769    #[test]
770    fn test_unique_uid_generation() {
771        let a = Uid::instance();
772        let b = Uid::instance();
773        assert_ne!(a, b);
774    }
775
776    #[test]
777    fn test_short_hex_parse() {
778        let parsed: Uid = "1".parse().unwrap();
779        assert_eq!(parsed, Uid::Instance(1));
780    }
781
782    #[test]
783    fn test_proc_id_construction_and_accessors() {
784        let uid = Uid::Instance(0xabc);
785        let label = Label::new("my-proc").unwrap();
786        let pid = ProcId::new(uid.clone(), Some(label.clone()));
787        assert_eq!(pid.uid(), &uid);
788        assert_eq!(pid.label(), Some(&label));
789    }
790
791    #[test]
792    fn test_proc_id_eq_ignores_label() {
793        let uid = Uid::Instance(0x42);
794        let a = ProcId::new(uid.clone(), Some(Label::new("alpha").unwrap()));
795        let b = ProcId::new(uid, Some(Label::new("beta").unwrap()));
796        assert_eq!(a, b);
797    }
798
799    #[test]
800    fn test_proc_id_hash_ignores_label() {
801        use std::collections::hash_map::DefaultHasher;
802
803        let uid = Uid::Instance(0x42);
804        let a = ProcId::new(uid.clone(), Some(Label::new("alpha").unwrap()));
805        let b = ProcId::new(uid, Some(Label::new("beta").unwrap()));
806
807        let hash = |pid: &ProcId| {
808            let mut h = DefaultHasher::new();
809            pid.hash(&mut h);
810            h.finish()
811        };
812        assert_eq!(hash(&a), hash(&b));
813    }
814
815    #[test]
816    fn test_proc_id_ord_ignores_label() {
817        let a = ProcId::new(Uid::Instance(1), Some(Label::new("zzz").unwrap()));
818        let b = ProcId::new(Uid::Instance(2), Some(Label::new("aaa").unwrap()));
819        assert!(a < b);
820    }
821
822    #[test]
823    fn test_proc_id_display() {
824        let pid = ProcId::new(
825            Uid::Instance(0xd5d54d7201103869),
826            Some(Label::new("my-proc").unwrap()),
827        );
828        assert_eq!(pid.to_string(), "d5d54d7201103869");
829
830        let pid_singleton = ProcId::new(
831            Uid::singleton(Label::new("my-proc").unwrap()),
832            Some(Label::new("my-proc").unwrap()),
833        );
834        assert_eq!(pid_singleton.to_string(), "_my-proc");
835    }
836
837    #[test]
838    fn test_proc_id_debug() {
839        let pid = ProcId::new(
840            Uid::Instance(0xd5d54d7201103869),
841            Some(Label::new("my-proc").unwrap()),
842        );
843        assert_eq!(format!("{:?}", pid), "<'my-proc' d5d54d7201103869>");
844
845        let pid_no_label = ProcId::new(Uid::Instance(0xd5d54d7201103869), None);
846        assert_eq!(format!("{:?}", pid_no_label), "<d5d54d7201103869>");
847    }
848
849    #[test]
850    fn test_proc_id_fromstr_roundtrip() {
851        let pid = ProcId::new(
852            Uid::Instance(0xd5d54d7201103869),
853            Some(Label::new("my-proc").unwrap()),
854        );
855        let s = pid.to_string();
856        let parsed: ProcId = s.parse().unwrap();
857        assert_eq!(pid, parsed);
858    }
859
860    #[test]
861    fn test_proc_id_fromstr_singleton() {
862        let parsed: ProcId = "_my-proc".parse().unwrap();
863        assert_eq!(
864            *parsed.uid(),
865            Uid::singleton(Label::new("my-proc").unwrap())
866        );
867        assert_eq!(parsed.label(), None);
868    }
869
870    #[test]
871    fn test_proc_id_serde_roundtrip() {
872        let pid = ProcId::new(
873            Uid::Instance(0xabcdef),
874            Some(Label::new("my-proc").unwrap()),
875        );
876        let json = serde_json::to_string(&pid).unwrap();
877        let parsed: ProcId = serde_json::from_str(&json).unwrap();
878        assert_eq!(pid, parsed);
879        assert_eq!(parsed.label().map(|l| l.as_str()), Some("my-proc"));
880
881        let pid_none = ProcId::new(Uid::Instance(0xabcdef), None);
882        let json_none = serde_json::to_string(&pid_none).unwrap();
883        let parsed_none: ProcId = serde_json::from_str(&json_none).unwrap();
884        assert_eq!(parsed_none.label(), None);
885    }
886
887    #[test]
888    fn test_actor_id_construction_and_accessors() {
889        let actor_uid = Uid::Instance(0xabc);
890        let proc_id = ProcId::new(Uid::Instance(0xdef), Some(Label::new("my-proc").unwrap()));
891        let label = Label::new("my-actor").unwrap();
892        let aid = ActorId::new(actor_uid.clone(), proc_id.clone(), Some(label.clone()));
893        assert_eq!(aid.uid(), &actor_uid);
894        assert_eq!(aid.proc_id(), &proc_id);
895        assert_eq!(aid.label(), Some(&label));
896    }
897
898    #[test]
899    fn test_actor_id_eq_ignores_label() {
900        let actor_uid = Uid::Instance(0x42);
901        let proc_id = ProcId::new(Uid::Instance(0x99), Some(Label::new("proc").unwrap()));
902        let a = ActorId::new(
903            actor_uid.clone(),
904            proc_id.clone(),
905            Some(Label::new("alpha").unwrap()),
906        );
907        let b = ActorId::new(actor_uid, proc_id, Some(Label::new("beta").unwrap()));
908        assert_eq!(a, b);
909    }
910
911    #[test]
912    fn test_actor_id_neq_different_proc() {
913        let actor_uid = Uid::Instance(0x42);
914        let proc_a = ProcId::new(Uid::Instance(1), Some(Label::new("proc").unwrap()));
915        let proc_b = ProcId::new(Uid::Instance(2), Some(Label::new("proc").unwrap()));
916        let a = ActorId::new(
917            actor_uid.clone(),
918            proc_a,
919            Some(Label::new("actor").unwrap()),
920        );
921        let b = ActorId::new(actor_uid, proc_b, Some(Label::new("actor").unwrap()));
922        assert_ne!(a, b);
923    }
924
925    #[test]
926    fn test_actor_id_hash_ignores_label() {
927        use std::collections::hash_map::DefaultHasher;
928
929        let actor_uid = Uid::Instance(0x42);
930        let proc_id = ProcId::new(Uid::Instance(0x99), Some(Label::new("proc").unwrap()));
931        let a = ActorId::new(
932            actor_uid.clone(),
933            proc_id.clone(),
934            Some(Label::new("alpha").unwrap()),
935        );
936        let b = ActorId::new(actor_uid, proc_id, Some(Label::new("beta").unwrap()));
937
938        let hash = |aid: &ActorId| {
939            let mut h = DefaultHasher::new();
940            aid.hash(&mut h);
941            h.finish()
942        };
943        assert_eq!(hash(&a), hash(&b));
944    }
945
946    #[test]
947    fn test_actor_id_ord_proc_first() {
948        let a = ActorId::new(
949            Uid::Instance(0xff),
950            ProcId::new(Uid::Instance(1), Some(Label::new("p").unwrap())),
951            Some(Label::new("a").unwrap()),
952        );
953        let b = ActorId::new(
954            Uid::Instance(0x01),
955            ProcId::new(Uid::Instance(2), Some(Label::new("p").unwrap())),
956            Some(Label::new("a").unwrap()),
957        );
958        assert!(a < b, "proc_id should be compared first");
959    }
960
961    #[test]
962    fn test_actor_id_ord_then_uid() {
963        let proc_id = ProcId::new(Uid::Instance(1), Some(Label::new("p").unwrap()));
964        let a = ActorId::new(
965            Uid::Instance(1),
966            proc_id.clone(),
967            Some(Label::new("a").unwrap()),
968        );
969        let b = ActorId::new(Uid::Instance(2), proc_id, Some(Label::new("a").unwrap()));
970        assert!(a < b);
971    }
972
973    #[test]
974    fn test_actor_id_display() {
975        let aid = ActorId::new(
976            Uid::Instance(0xabc123),
977            ProcId::new(
978                Uid::Instance(0xdef456),
979                Some(Label::new("my-proc").unwrap()),
980            ),
981            Some(Label::new("my-actor").unwrap()),
982        );
983        assert_eq!(aid.to_string(), "0000000000abc123.0000000000def456");
984    }
985
986    #[test]
987    fn test_actor_id_debug() {
988        let aid = ActorId::new(
989            Uid::Instance(0xabc123),
990            ProcId::new(
991                Uid::Instance(0xdef456),
992                Some(Label::new("my-proc").unwrap()),
993            ),
994            Some(Label::new("my-actor").unwrap()),
995        );
996        assert_eq!(
997            format!("{:?}", aid),
998            "<'my-actor.my-proc' 0000000000abc123.0000000000def456>"
999        );
1000
1001        let aid_no_labels = ActorId::new(
1002            Uid::Instance(0xabc123),
1003            ProcId::new(Uid::Instance(0xdef456), None),
1004            None,
1005        );
1006        assert_eq!(
1007            format!("{:?}", aid_no_labels),
1008            "<0000000000abc123.0000000000def456>"
1009        );
1010    }
1011
1012    #[test]
1013    fn test_actor_id_fromstr_roundtrip() {
1014        let aid = ActorId::new(
1015            Uid::Instance(0xabc123),
1016            ProcId::new(
1017                Uid::Instance(0xdef456),
1018                Some(Label::new("my-proc").unwrap()),
1019            ),
1020            Some(Label::new("my-actor").unwrap()),
1021        );
1022        let s = aid.to_string();
1023        let parsed: ActorId = s.parse().unwrap();
1024        assert_eq!(aid, parsed);
1025    }
1026
1027    #[test]
1028    fn test_actor_id_fromstr_with_singletons() {
1029        let parsed: ActorId = "_my-actor._my-proc".parse().unwrap();
1030        assert_eq!(
1031            *parsed.uid(),
1032            Uid::singleton(Label::new("my-actor").unwrap())
1033        );
1034        assert_eq!(
1035            *parsed.proc_id().uid(),
1036            Uid::singleton(Label::new("my-proc").unwrap())
1037        );
1038    }
1039
1040    #[test]
1041    fn test_actor_id_fromstr_errors() {
1042        assert!("no-dot-here".parse::<ActorId>().is_err());
1043        assert!(".".parse::<ActorId>().is_err());
1044        assert!("abc.".parse::<ActorId>().is_err());
1045        assert!(".abc".parse::<ActorId>().is_err());
1046    }
1047
1048    #[test]
1049    fn test_actor_id_serde_roundtrip() {
1050        let aid = ActorId::new(
1051            Uid::Instance(0xabcdef),
1052            ProcId::new(
1053                Uid::Instance(0x123456),
1054                Some(Label::new("my-proc").unwrap()),
1055            ),
1056            Some(Label::new("my-actor").unwrap()),
1057        );
1058        let json = serde_json::to_string(&aid).unwrap();
1059        let parsed: ActorId = serde_json::from_str(&json).unwrap();
1060        assert_eq!(aid, parsed);
1061        assert_eq!(parsed.label().map(|l| l.as_str()), Some("my-actor"));
1062        assert_eq!(
1063            parsed.proc_id().label().map(|l| l.as_str()),
1064            Some("my-proc")
1065        );
1066    }
1067
1068    #[test]
1069    fn test_port_id_construction_and_accessors() {
1070        let actor_uid = Uid::Instance(0xabc);
1071        let proc_id = ProcId::new(Uid::Instance(0xdef), Some(Label::new("my-proc").unwrap()));
1072        let actor_id = ActorId::new(
1073            actor_uid,
1074            proc_id.clone(),
1075            Some(Label::new("my-actor").unwrap()),
1076        );
1077        let port = Port::from(42);
1078        let pid = PortId::new(actor_id.clone(), port);
1079        assert_eq!(pid.actor_id(), &actor_id);
1080        assert_eq!(pid.port(), port);
1081        assert_eq!(pid.proc_id(), &proc_id);
1082    }
1083
1084    #[test]
1085    fn test_port_id_eq() {
1086        let actor_id = ActorId::new(
1087            Uid::Instance(0x42),
1088            ProcId::new(Uid::Instance(0x99), Some(Label::new("proc").unwrap())),
1089            Some(Label::new("actor").unwrap()),
1090        );
1091        let a = PortId::new(actor_id.clone(), Port::from(10));
1092        let b = PortId::new(actor_id, Port::from(10));
1093        assert_eq!(a, b);
1094    }
1095
1096    #[test]
1097    fn test_port_id_neq_different_port() {
1098        let actor_id = ActorId::new(
1099            Uid::Instance(0x42),
1100            ProcId::new(Uid::Instance(0x99), Some(Label::new("proc").unwrap())),
1101            Some(Label::new("actor").unwrap()),
1102        );
1103        let a = PortId::new(actor_id.clone(), Port::from(10));
1104        let b = PortId::new(actor_id, Port::from(20));
1105        assert_ne!(a, b);
1106    }
1107
1108    #[test]
1109    fn test_port_id_hash() {
1110        use std::collections::hash_map::DefaultHasher;
1111
1112        let actor_id = ActorId::new(
1113            Uid::Instance(0x42),
1114            ProcId::new(Uid::Instance(0x99), Some(Label::new("proc").unwrap())),
1115            Some(Label::new("actor").unwrap()),
1116        );
1117        let a = PortId::new(actor_id.clone(), Port::from(10));
1118        let b = PortId::new(actor_id, Port::from(10));
1119        let hash = |pid: &PortId| {
1120            let mut h = DefaultHasher::new();
1121            pid.hash(&mut h);
1122            h.finish()
1123        };
1124        assert_eq!(hash(&a), hash(&b));
1125    }
1126
1127    #[test]
1128    fn test_port_id_ord() {
1129        let actor_id = ActorId::new(
1130            Uid::Instance(0x42),
1131            ProcId::new(Uid::Instance(0x99), Some(Label::new("proc").unwrap())),
1132            Some(Label::new("actor").unwrap()),
1133        );
1134        let a = PortId::new(actor_id.clone(), Port::from(1));
1135        let b = PortId::new(actor_id, Port::from(2));
1136        assert!(a < b);
1137    }
1138
1139    #[test]
1140    fn test_port_id_ord_actor_first() {
1141        let a = PortId::new(
1142            ActorId::new(
1143                Uid::Instance(0x01),
1144                ProcId::new(Uid::Instance(1), Some(Label::new("p").unwrap())),
1145                Some(Label::new("a").unwrap()),
1146            ),
1147            Port::from(99),
1148        );
1149        let b = PortId::new(
1150            ActorId::new(
1151                Uid::Instance(0x02),
1152                ProcId::new(Uid::Instance(1), Some(Label::new("p").unwrap())),
1153                Some(Label::new("a").unwrap()),
1154            ),
1155            Port::from(1),
1156        );
1157        assert!(a < b, "actor_id should be compared first");
1158    }
1159
1160    #[test]
1161    fn test_port_id_display() {
1162        let aid = ActorId::new(
1163            Uid::Instance(0xabc123),
1164            ProcId::new(
1165                Uid::Instance(0xdef456),
1166                Some(Label::new("my-proc").unwrap()),
1167            ),
1168            Some(Label::new("my-actor").unwrap()),
1169        );
1170        let pid = PortId::new(aid, Port::from(42));
1171        assert_eq!(pid.to_string(), "0000000000abc123.0000000000def456:42");
1172    }
1173
1174    #[test]
1175    fn test_port_id_debug_all_labels() {
1176        let aid = ActorId::new(
1177            Uid::Instance(0xabc123),
1178            ProcId::new(
1179                Uid::Instance(0xdef456),
1180                Some(Label::new("my-proc").unwrap()),
1181            ),
1182            Some(Label::new("my-actor").unwrap()),
1183        );
1184        let pid = PortId::new(aid, Port::from(42));
1185        assert_eq!(
1186            format!("{:?}", pid),
1187            "<'my-actor.my-proc' 0000000000abc123.0000000000def456:42>"
1188        );
1189    }
1190
1191    #[test]
1192    fn test_port_id_debug_no_labels() {
1193        let aid = ActorId::new(
1194            Uid::Instance(0xabc123),
1195            ProcId::new(Uid::Instance(0xdef456), None),
1196            None,
1197        );
1198        let pid = PortId::new(aid, Port::from(42));
1199        assert_eq!(
1200            format!("{:?}", pid),
1201            "<0000000000abc123.0000000000def456:42>"
1202        );
1203    }
1204
1205    #[test]
1206    fn test_port_id_debug_actor_label_only() {
1207        let aid = ActorId::new(
1208            Uid::Instance(0xabc123),
1209            ProcId::new(Uid::Instance(0xdef456), None),
1210            Some(Label::new("my-actor").unwrap()),
1211        );
1212        let pid = PortId::new(aid, Port::from(42));
1213        assert_eq!(
1214            format!("{:?}", pid),
1215            "<'my-actor' 0000000000abc123.0000000000def456:42>"
1216        );
1217    }
1218
1219    #[test]
1220    fn test_port_id_debug_proc_label_only() {
1221        let aid = ActorId::new(
1222            Uid::Instance(0xabc123),
1223            ProcId::new(
1224                Uid::Instance(0xdef456),
1225                Some(Label::new("my-proc").unwrap()),
1226            ),
1227            None,
1228        );
1229        let pid = PortId::new(aid, Port::from(42));
1230        assert_eq!(
1231            format!("{:?}", pid),
1232            "<'.my-proc' 0000000000abc123.0000000000def456:42>"
1233        );
1234    }
1235
1236    #[test]
1237    fn test_port_id_fromstr_roundtrip() {
1238        let aid = ActorId::new(
1239            Uid::Instance(0xabc123),
1240            ProcId::new(
1241                Uid::Instance(0xdef456),
1242                Some(Label::new("my-proc").unwrap()),
1243            ),
1244            Some(Label::new("my-actor").unwrap()),
1245        );
1246        let pid = PortId::new(aid, Port::from(42));
1247        let s = pid.to_string();
1248        let parsed: PortId = s.parse().unwrap();
1249        assert_eq!(pid, parsed);
1250    }
1251
1252    #[test]
1253    fn test_port_id_fromstr_errors() {
1254        // Missing colon.
1255        assert!(
1256            "0000000000abc123.0000000000def456"
1257                .parse::<PortId>()
1258                .is_err()
1259        );
1260        // Invalid port.
1261        assert!(
1262            "0000000000abc123.0000000000def456:notanumber"
1263                .parse::<PortId>()
1264                .is_err()
1265        );
1266    }
1267
1268    #[test]
1269    fn test_port_id_serde_roundtrip() {
1270        let aid = ActorId::new(
1271            Uid::Instance(0xabcdef),
1272            ProcId::new(
1273                Uid::Instance(0x123456),
1274                Some(Label::new("my-proc").unwrap()),
1275            ),
1276            Some(Label::new("my-actor").unwrap()),
1277        );
1278        let pid = PortId::new(aid, Port::from(42));
1279        let json = serde_json::to_string(&pid).unwrap();
1280        let parsed: PortId = serde_json::from_str(&json).unwrap();
1281        assert_eq!(pid, parsed);
1282    }
1283}