Skip to main content

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// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
128// Display trait implementations
129// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
130
131impl std::fmt::Display for ActrType {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        write!(f, "{}+{}", self.manufacturer, self.name)
134    }
135}
136
137impl std::fmt::Display for ActrId {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        write!(
140            f,
141            "{:x}@{}:{}+{}",
142            self.serial_number, self.realm.realm_id, self.r#type.manufacturer, self.r#type.name
143        )
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_actor_id_string_repr_roundtrip() {
153        let original_id = ActrId {
154            realm: Realm { realm_id: 101 },
155            serial_number: 0x1a2b3c,
156            r#type: ActrType {
157                manufacturer: "acme".to_string(),
158                name: "echo-service".to_string(),
159            },
160        };
161
162        let string_repr = original_id.to_string_repr();
163        assert_eq!(string_repr, "1a2b3c@101:acme+echo-service");
164
165        let parsed_id = ActrId::from_string_repr(&string_repr).unwrap();
166        assert_eq!(original_id.realm.realm_id, parsed_id.realm.realm_id);
167        assert_eq!(original_id.serial_number, parsed_id.serial_number);
168        assert_eq!(
169            original_id.r#type.manufacturer,
170            parsed_id.r#type.manufacturer
171        );
172        assert_eq!(original_id.r#type.name, parsed_id.r#type.name);
173    }
174
175    #[test]
176    fn test_invalid_format_parsing() {
177        assert!(matches!(
178            ActrId::from_string_repr("invalid-string"),
179            Err(ActrError::InvalidFormat(_))
180        ));
181        assert!(matches!(
182            ActrId::from_string_repr("123@101:acme"),
183            Err(ActrError::InvalidTypeFormat(_))
184        ));
185        assert!(matches!(
186            ActrId::from_string_repr("123@acme+echo"),
187            Err(ActrError::InvalidFormat(_))
188        ));
189        assert!(matches!(
190            ActrId::from_string_repr("xyz@101:acme+echo"),
191            Err(ActrError::InvalidComponent(_))
192        ));
193    }
194
195    #[test]
196    fn test_actr_type_roundtrip_and_validation() {
197        let s = "acme+echo";
198        let ty = ActrType::from_string_repr(s).unwrap();
199        assert_eq!(ty.to_string_repr(), s);
200
201        // invalid format
202        assert!(matches!(
203            ActrType::from_string_repr("acme-echo"),
204            Err(ActrError::InvalidTypeFormat(_))
205        ));
206
207        // invalid manufacturer via Name validation
208        assert!(matches!(
209            ActrType::from_string_repr("1acme+echo"),
210            Err(ActrError::InvalidComponent(_))
211        ));
212
213        // invalid name via Name validation
214        assert!(matches!(
215            ActrType::from_string_repr("acme+echo!"),
216            Err(ActrError::InvalidComponent(_))
217        ));
218    }
219}