actr_protocol/
actr_ext.rs

1//! Actor identity formatting and parsing utilities
2//!
3//! This module provides string formatting and parsing helpers for `ActrType` and `ActrId`.
4//! String forms are stable for logging, configuration, and CLI interactions.
5
6use crate::{ActrId, ActrType, Realm, name::Name};
7use std::str::FromStr;
8use thiserror::Error;
9
10/// Errors for actor identity parsing and formatting
11#[derive(Error, Debug, PartialEq, Eq)]
12pub enum ActrError {
13    #[error(
14        "Invalid Actor ID format: '{0}'. Expected format: <serial_number>@<realm_id>:<manufacturer>+<name>"
15    )]
16    InvalidFormat(String),
17
18    #[error("Invalid component in actor identity: {0}")]
19    InvalidComponent(String),
20
21    #[error("Invalid actor type format: '{0}'. Expected format: <manufacturer>+<name>")]
22    InvalidTypeFormat(String),
23
24    /// 消息解码失败
25    #[error("Failed to decode protobuf message: {message}")]
26    DecodeFailure { message: String },
27
28    /// 未知的路由键
29    #[error("Unknown route key: {route_key}")]
30    UnknownRoute { route_key: String },
31
32    /// OutGate 尚未初始化
33    #[error("Gate not initialized: {message}")]
34    GateNotInitialized { message: String },
35
36    /// Feature not yet implemented
37    #[error("Feature not yet implemented: {feature}")]
38    NotImplemented { feature: String },
39
40    /// ACL 权限拒绝
41    #[error("Permission denied: {message}")]
42    PermissionDenied { message: String },
43
44    /// 依赖未找到 - Actr.lock.toml 中不存在该依赖
45    #[error("Dependency '{service_name}' not found: {message}")]
46    DependencyNotFound {
47        service_name: String,
48        message: String,
49    },
50}
51
52/// Helpers for `ActrType` string conversions
53pub trait ActrTypeExt: Sized {
54    /// Convert to stable string representation: "<manufacturer>+<name>".
55    fn to_string_repr(&self) -> String;
56
57    /// Parse from string representation. Performs validation on both parts.
58    fn from_string_repr(s: &str) -> Result<Self, ActrError>;
59}
60
61impl ActrTypeExt for ActrType {
62    fn to_string_repr(&self) -> String {
63        format!("{}+{}", self.manufacturer, self.name)
64    }
65
66    fn from_string_repr(s: &str) -> Result<Self, ActrError> {
67        let (manufacturer, name) = s
68            .split_once('+')
69            .ok_or_else(|| ActrError::InvalidTypeFormat(s.to_string()))?;
70
71        // Reuse generic name validation to keep rules consistent across the project
72        Name::new(manufacturer.to_string())
73            .map_err(|e| ActrError::InvalidComponent(format!("Invalid manufacturer: {e}")))?;
74        Name::new(name.to_string())
75            .map_err(|e| ActrError::InvalidComponent(format!("Invalid type name: {e}")))?;
76
77        Ok(ActrType {
78            manufacturer: manufacturer.to_string(),
79            name: name.to_string(),
80        })
81    }
82}
83
84/// Helpers for `ActrId` string conversions
85pub trait ActrIdExt: Sized {
86    /// Convert to "<serial_number_hex>@<realm_id>:<manufacturer>+<name>"
87    fn to_string_repr(&self) -> String;
88
89    /// Parse from string representation.
90    fn from_string_repr(s: &str) -> Result<Self, ActrError>;
91}
92
93impl ActrIdExt for ActrId {
94    fn to_string_repr(&self) -> String {
95        format!(
96            "{:x}@{}:{}+{}",
97            self.serial_number, self.realm.realm_id, self.r#type.manufacturer, self.r#type.name
98        )
99    }
100
101    fn from_string_repr(s: &str) -> Result<Self, ActrError> {
102        let (serial_part, rest) = s
103            .split_once('@')
104            .ok_or_else(|| ActrError::InvalidFormat("Missing '@' separator".to_string()))?;
105
106        let serial_number = u64::from_str_radix(serial_part, 16).map_err(|_e| {
107            ActrError::InvalidComponent(format!("Invalid serial number hex: {serial_part}"))
108        })?;
109
110        let (realm_part, type_part) = rest
111            .split_once(':')
112            .ok_or_else(|| ActrError::InvalidFormat("Missing ':' separator".to_string()))?;
113
114        let realm_id = u32::from_str(realm_part)
115            .map_err(|_e| ActrError::InvalidComponent(format!("Invalid realm ID: {realm_part}")))?;
116
117        let actr_type = ActrType::from_string_repr(type_part)?;
118
119        Ok(ActrId {
120            realm: Realm { realm_id },
121            serial_number,
122            r#type: actr_type,
123        })
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_actor_id_string_repr_roundtrip() {
133        let original_id = ActrId {
134            realm: Realm { realm_id: 101 },
135            serial_number: 0x1a2b3c,
136            r#type: ActrType {
137                manufacturer: "acme".to_string(),
138                name: "echo-service".to_string(),
139            },
140        };
141
142        let string_repr = original_id.to_string_repr();
143        assert_eq!(string_repr, "1a2b3c@101:acme+echo-service");
144
145        let parsed_id = ActrId::from_string_repr(&string_repr).unwrap();
146        assert_eq!(original_id.realm.realm_id, parsed_id.realm.realm_id);
147        assert_eq!(original_id.serial_number, parsed_id.serial_number);
148        assert_eq!(
149            original_id.r#type.manufacturer,
150            parsed_id.r#type.manufacturer
151        );
152        assert_eq!(original_id.r#type.name, parsed_id.r#type.name);
153    }
154
155    #[test]
156    fn test_invalid_format_parsing() {
157        assert!(matches!(
158            ActrId::from_string_repr("invalid-string"),
159            Err(ActrError::InvalidFormat(_))
160        ));
161        assert!(matches!(
162            ActrId::from_string_repr("123@101:acme"),
163            Err(ActrError::InvalidTypeFormat(_))
164        ));
165        assert!(matches!(
166            ActrId::from_string_repr("123@acme+echo"),
167            Err(ActrError::InvalidFormat(_))
168        ));
169        assert!(matches!(
170            ActrId::from_string_repr("xyz@101:acme+echo"),
171            Err(ActrError::InvalidComponent(_))
172        ));
173    }
174
175    #[test]
176    fn test_actr_type_roundtrip_and_validation() {
177        let s = "acme+echo";
178        let ty = ActrType::from_string_repr(s).unwrap();
179        assert_eq!(ty.to_string_repr(), s);
180
181        // invalid format
182        assert!(matches!(
183            ActrType::from_string_repr("acme-echo"),
184            Err(ActrError::InvalidTypeFormat(_))
185        ));
186
187        // invalid manufacturer via Name validation
188        assert!(matches!(
189            ActrType::from_string_repr("1acme+echo"),
190            Err(ActrError::InvalidComponent(_))
191        ));
192
193        // invalid name via Name validation
194        assert!(matches!(
195            ActrType::from_string_repr("acme+echo!"),
196            Err(ActrError::InvalidComponent(_))
197        ));
198    }
199}