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