actr_protocol/
name.rs

1use std::fmt::{self, Display};
2use std::ops::{Deref, DerefMut};
3use std::str::FromStr;
4use thiserror::Error;
5
6// Protobuf Package name
7// 语法要求:由字母、数字和下划线组成,各部分用 . 分隔(类似 Java 包名)
8// 命名规范:
9// - 全小写
10// - 建议采用反向域名形式(如 com.example.project)避免冲突
11// - 不能以数字开头,不能包含除 . 和 _ 外的特殊字符
12
13// Protobuf Service name
14// 语法要求:由字母、数字和下划线组成
15// 命名规范:
16// - 采用 PascalCase(帕斯卡命名法,首字母大写)
17// - 通常使用名词或名词短语
18// - 不能以数字开头,不能包含特殊字符
19
20// Protobuf Method name
21// 语法要求:由字母、数字和下划线组成
22// 命名规范:
23// - 采用 camelCase(驼峰命名法,首字母小写)
24// - 通常使用动词或动词短语
25// - 不能以数字开头,不能包含特殊字符
26
27#[derive(Debug, Clone, Eq, PartialEq, Hash)]
28pub struct PackageName(String);
29
30#[derive(Debug, Clone, Eq, PartialEq, Hash)]
31pub struct ServiceName(String);
32
33#[derive(Debug, Clone, Eq, PartialEq, Hash)]
34pub struct MethodName(String);
35
36/// A validated actor name.
37///
38/// Names must:
39/// - Start with an alphabetic character
40/// - End with an alphanumeric character
41/// - Contain only alphanumeric characters, hyphens, underscores, and dots
42/// - Be non-empty
43/// - Not exceed 32 characters in length
44#[derive(Debug, Clone, Eq, PartialEq, Hash)]
45pub struct Name(String);
46
47#[derive(Debug, Error, Eq, PartialEq)]
48pub enum NameError {
49    #[error("Name is empty")]
50    Empty,
51
52    #[error("Name exceeds 32 characters, length: {0}")]
53    TooLong(usize),
54
55    #[error("Name must start with an alphabetic character, found: {0}")]
56    InvalidStartChar(char),
57
58    #[error("Name must end with an alphanumeric character, found: {0}")]
59    InvalidEndChar(char),
60
61    #[error("Name contains invalid character: {0}")]
62    InvalidChar(char),
63}
64
65impl Name {
66    /// Creates a new validated Name.
67    ///
68    /// # Errors
69    ///
70    /// Returns `NameError` with specific reason if the name doesn't meet the validation criteria.
71    pub fn new(name: String) -> Result<Self, NameError> {
72        if name.is_empty() {
73            return Err(NameError::Empty);
74        }
75        if name.len() > 32 {
76            return Err(NameError::TooLong(name.len()));
77        }
78        let mut chars = name.chars();
79        let first = chars.next().ok_or(NameError::Empty)?;
80        if !first.is_alphabetic() {
81            return Err(NameError::InvalidStartChar(first));
82        }
83        let mut last = first;
84        for c in chars {
85            last = c;
86            if !c.is_alphanumeric() && c != '-' && c != '_' && c != '.' {
87                return Err(NameError::InvalidChar(c));
88            }
89        }
90        if !last.is_alphanumeric() {
91            return Err(NameError::InvalidEndChar(last));
92        }
93        Ok(Self(name))
94    }
95}
96
97impl FromStr for Name {
98    type Err = NameError;
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        Self::new(s.to_string())
101    }
102}
103
104impl Deref for Name {
105    type Target = String;
106    fn deref(&self) -> &Self::Target {
107        &self.0
108    }
109}
110
111impl DerefMut for Name {
112    fn deref_mut(&mut self) -> &mut Self::Target {
113        &mut self.0
114    }
115}
116
117impl TryFrom<String> for Name {
118    type Error = NameError;
119    fn try_from(s: String) -> Result<Self, Self::Error> {
120        Self::new(s)
121    }
122}
123
124impl Display for Name {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "{}", self.0)
127    }
128}
129
130// ----------------------------- PackageName -----------------------------
131
132#[derive(Debug, Error, Eq, PartialEq)]
133pub enum PackageNameError {
134    #[error("Package name is empty")]
135    Empty,
136
137    #[error("Package name exceeds 256 characters, length: {0}")]
138    TooLong(usize),
139
140    #[error("Package name must not start with a digit or '.', found: {0}")]
141    InvalidStartChar(char),
142
143    #[error("Package name must not end with '.', found: {0}")]
144    InvalidEndChar(char),
145
146    #[error("Package name contains invalid character: {0}")]
147    InvalidChar(char),
148
149    #[error("Package name contains empty segment")]
150    EmptySegment,
151
152    #[error("Package name segment must not start with a digit, found: {0}")]
153    InvalidSegmentStartChar(char),
154}
155
156impl PackageName {
157    pub fn new(name: String) -> Result<Self, PackageNameError> {
158        const MAX_LEN: usize = 256;
159        if name.is_empty() {
160            return Err(PackageNameError::Empty);
161        }
162        if name.len() > MAX_LEN {
163            return Err(PackageNameError::TooLong(name.len()));
164        }
165
166        let mut chars = name.chars();
167        let first = chars.next().ok_or(PackageNameError::Empty)?;
168
169        // First character rules: cannot be digit or '.'; letters must be lowercase
170        if first == '.' || first.is_ascii_digit() {
171            return Err(PackageNameError::InvalidStartChar(first));
172        }
173        if first.is_alphabetic() && first.is_uppercase() {
174            return Err(PackageNameError::InvalidChar(first));
175        }
176        if !(first.is_alphanumeric() || first == '_') {
177            // Only letters/digits/underscore are allowed ('.' only as separator handled below)
178            return Err(PackageNameError::InvalidChar(first));
179        }
180
181        let mut last = first;
182        let mut at_segment_start = false; // after first char we've already started first segment
183
184        for c in chars {
185            // Dot separates segments
186            if c == '.' {
187                if last == '.' {
188                    return Err(PackageNameError::EmptySegment);
189                }
190                at_segment_start = true;
191                last = c;
192                continue;
193            }
194
195            // Segment start cannot be digit
196            if at_segment_start && c.is_ascii_digit() {
197                return Err(PackageNameError::InvalidSegmentStartChar(c));
198            }
199            at_segment_start = false;
200
201            // Allowed chars inside segments
202            if !(c.is_alphanumeric() || c == '_') {
203                return Err(PackageNameError::InvalidChar(c));
204            }
205            // Enforce lowercase for alphabetic
206            if c.is_alphabetic() && c.is_uppercase() {
207                return Err(PackageNameError::InvalidChar(c));
208            }
209
210            last = c;
211        }
212
213        if last == '.' {
214            return Err(PackageNameError::InvalidEndChar(last));
215        }
216
217        Ok(Self(name))
218    }
219}
220
221impl FromStr for PackageName {
222    type Err = PackageNameError;
223    fn from_str(s: &str) -> Result<Self, Self::Err> {
224        Self::new(s.to_string())
225    }
226}
227
228impl Deref for PackageName {
229    type Target = String;
230    fn deref(&self) -> &Self::Target {
231        &self.0
232    }
233}
234
235impl DerefMut for PackageName {
236    fn deref_mut(&mut self) -> &mut Self::Target {
237        &mut self.0
238    }
239}
240
241impl TryFrom<String> for PackageName {
242    type Error = PackageNameError;
243    fn try_from(s: String) -> Result<Self, Self::Error> {
244        Self::new(s)
245    }
246}
247
248impl Display for PackageName {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        write!(f, "{}", self.0)
251    }
252}
253
254// ----------------------------- ServiceName -----------------------------
255
256#[derive(Debug, Error, Eq, PartialEq)]
257pub enum ServiceNameError {
258    #[error("Service name is empty")]
259    Empty,
260
261    #[error("Service name exceeds 64 characters, length: {0}")]
262    TooLong(usize),
263
264    #[error("Service name must start with an uppercase alphabetic character, found: {0}")]
265    InvalidStartChar(char),
266
267    #[error("Service name must end with an alphanumeric character, found: {0}")]
268    InvalidEndChar(char),
269
270    #[error("Service name contains invalid character: {0}")]
271    InvalidChar(char),
272}
273
274impl ServiceName {
275    pub fn new(name: String) -> Result<Self, ServiceNameError> {
276        const MAX_LEN: usize = 64;
277        if name.is_empty() {
278            return Err(ServiceNameError::Empty);
279        }
280        if name.len() > MAX_LEN {
281            return Err(ServiceNameError::TooLong(name.len()));
282        }
283
284        let mut chars = name.chars();
285        let first = chars.next().ok_or(ServiceNameError::Empty)?;
286        if !first.is_alphabetic() || !first.is_uppercase() {
287            return Err(ServiceNameError::InvalidStartChar(first));
288        }
289        let mut last = first;
290        for c in chars {
291            if !(c.is_alphanumeric() || c == '_') {
292                return Err(ServiceNameError::InvalidChar(c));
293            }
294            last = c;
295        }
296        if !last.is_alphanumeric() {
297            return Err(ServiceNameError::InvalidEndChar(last));
298        }
299        Ok(Self(name))
300    }
301}
302
303impl FromStr for ServiceName {
304    type Err = ServiceNameError;
305    fn from_str(s: &str) -> Result<Self, Self::Err> {
306        Self::new(s.to_string())
307    }
308}
309
310impl Deref for ServiceName {
311    type Target = String;
312    fn deref(&self) -> &Self::Target {
313        &self.0
314    }
315}
316
317impl DerefMut for ServiceName {
318    fn deref_mut(&mut self) -> &mut Self::Target {
319        &mut self.0
320    }
321}
322
323impl TryFrom<String> for ServiceName {
324    type Error = ServiceNameError;
325    fn try_from(s: String) -> Result<Self, Self::Error> {
326        Self::new(s)
327    }
328}
329
330impl Display for ServiceName {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        write!(f, "{}", self.0)
333    }
334}
335
336// ----------------------------- MethodName -----------------------------
337
338#[derive(Debug, Error, Eq, PartialEq)]
339pub enum MethodNameError {
340    #[error("Method name is empty")]
341    Empty,
342
343    #[error("Method name exceeds 64 characters, length: {0}")]
344    TooLong(usize),
345
346    #[error("Method name must start with a lowercase alphabetic character, found: {0}")]
347    InvalidStartChar(char),
348
349    #[error("Method name must end with an alphanumeric character, found: {0}")]
350    InvalidEndChar(char),
351
352    #[error("Method name contains invalid character: {0}")]
353    InvalidChar(char),
354}
355
356impl MethodName {
357    pub fn new(name: String) -> Result<Self, MethodNameError> {
358        const MAX_LEN: usize = 64;
359        if name.is_empty() {
360            return Err(MethodNameError::Empty);
361        }
362        if name.len() > MAX_LEN {
363            return Err(MethodNameError::TooLong(name.len()));
364        }
365
366        let mut chars = name.chars();
367        let first = chars.next().ok_or(MethodNameError::Empty)?;
368        if !first.is_alphabetic() || !first.is_lowercase() {
369            return Err(MethodNameError::InvalidStartChar(first));
370        }
371        let mut last = first;
372        for c in chars {
373            if !(c.is_alphanumeric() || c == '_') {
374                return Err(MethodNameError::InvalidChar(c));
375            }
376            last = c;
377        }
378        if !last.is_alphanumeric() {
379            return Err(MethodNameError::InvalidEndChar(last));
380        }
381        Ok(Self(name))
382    }
383}
384
385impl FromStr for MethodName {
386    type Err = MethodNameError;
387    fn from_str(s: &str) -> Result<Self, Self::Err> {
388        Self::new(s.to_string())
389    }
390}
391
392impl Deref for MethodName {
393    type Target = String;
394    fn deref(&self) -> &Self::Target {
395        &self.0
396    }
397}
398
399impl DerefMut for MethodName {
400    fn deref_mut(&mut self) -> &mut Self::Target {
401        &mut self.0
402    }
403}
404
405impl TryFrom<String> for MethodName {
406    type Error = MethodNameError;
407    fn try_from(s: String) -> Result<Self, Self::Error> {
408        Self::new(s)
409    }
410}
411
412impl Display for MethodName {
413    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414        write!(f, "{}", self.0)
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    #[test]
423    fn test_valid_name() {
424        assert!(Name::new("valid-name_123".to_string()).is_ok());
425        assert!(Name::new("A".to_string()).is_ok());
426        assert!(Name::new("actor-1".to_string()).is_ok());
427        assert!(Name::new("actor.1".to_string()).is_ok());
428        assert!(Name::new("com.example.actor".to_string()).is_ok());
429    }
430
431    #[test]
432    fn test_invalid_name() {
433        assert_eq!(Name::new("".to_string()).unwrap_err(), NameError::Empty);
434        assert_eq!(
435            Name::new("-invalid".to_string()).unwrap_err(),
436            NameError::InvalidStartChar('-')
437        );
438        assert_eq!(
439            Name::new(".invalid".to_string()).unwrap_err(),
440            NameError::InvalidStartChar('.')
441        );
442        assert_eq!(
443            Name::new("invalid-".to_string()).unwrap_err(),
444            NameError::InvalidEndChar('-')
445        );
446        assert_eq!(
447            Name::new("invalid.".to_string()).unwrap_err(),
448            NameError::InvalidEndChar('.')
449        );
450        assert_eq!(
451            Name::new("has invalid char!".to_string()).unwrap_err(),
452            NameError::InvalidChar(' ')
453        );
454        assert_eq!(
455            Name::new("too_long_name_that_exceeds_thirty_two_characters".to_string()).unwrap_err(),
456            NameError::TooLong(48)
457        );
458        assert_eq!(
459            Name::new("1starts_with_number".to_string()).unwrap_err(),
460            NameError::InvalidStartChar('1')
461        );
462    }
463
464    #[test]
465    fn test_valid_package_name() {
466        assert!(PackageName::new("com.example.project".to_string()).is_ok());
467        assert!(PackageName::new("a_b.c_d.e1".to_string()).is_ok());
468        assert!(PackageName::new("example".to_string()).is_ok());
469        assert!(PackageName::new("_internal.pkg".to_string()).is_ok());
470    }
471
472    #[test]
473    fn test_invalid_package_name() {
474        assert_eq!(
475            PackageName::new("".to_string()).unwrap_err(),
476            PackageNameError::Empty
477        );
478        assert_eq!(
479            PackageName::new(".leading".to_string()).unwrap_err(),
480            PackageNameError::InvalidStartChar('.')
481        );
482        assert_eq!(
483            PackageName::new("trailing.".to_string()).unwrap_err(),
484            PackageNameError::InvalidEndChar('.')
485        );
486        assert_eq!(
487            PackageName::new("com..example".to_string()).unwrap_err(),
488            PackageNameError::EmptySegment
489        );
490        assert_eq!(
491            PackageName::new("com.1example".to_string()).unwrap_err(),
492            PackageNameError::InvalidSegmentStartChar('1')
493        );
494        assert_eq!(
495            PackageName::new("Com.Example".to_string()).unwrap_err(),
496            PackageNameError::InvalidChar('C')
497        );
498        assert_eq!(
499            PackageName::new("com.exa$mple".to_string()).unwrap_err(),
500            PackageNameError::InvalidChar('$')
501        );
502    }
503
504    #[test]
505    fn test_valid_service_name() {
506        assert!(ServiceName::new("Echo".to_string()).is_ok());
507        assert!(ServiceName::new("UserService".to_string()).is_ok());
508        assert!(ServiceName::new("HTTPV1".to_string()).is_ok());
509        assert!(ServiceName::new("Service_Name".to_string()).is_ok());
510    }
511
512    #[test]
513    fn test_invalid_service_name() {
514        assert_eq!(
515            ServiceName::new("".to_string()).unwrap_err(),
516            ServiceNameError::Empty
517        );
518        assert_eq!(
519            ServiceName::new("service".to_string()).unwrap_err(),
520            ServiceNameError::InvalidStartChar('s')
521        );
522        assert_eq!(
523            ServiceName::new("_Service".to_string()).unwrap_err(),
524            ServiceNameError::InvalidStartChar('_')
525        );
526        assert_eq!(
527            ServiceName::new("Service-".to_string()).unwrap_err(),
528            ServiceNameError::InvalidChar('-')
529        );
530        assert_eq!(
531            ServiceName::new("Service_".to_string()).unwrap_err(),
532            ServiceNameError::InvalidEndChar('_')
533        );
534    }
535
536    #[test]
537    fn test_valid_method_name() {
538        assert!(MethodName::new("echo".to_string()).is_ok());
539        assert!(MethodName::new("doWork".to_string()).is_ok());
540        assert!(MethodName::new("get_v1".to_string()).is_ok());
541    }
542
543    #[test]
544    fn test_invalid_method_name() {
545        assert_eq!(
546            MethodName::new("".to_string()).unwrap_err(),
547            MethodNameError::Empty
548        );
549        assert_eq!(
550            MethodName::new("1call".to_string()).unwrap_err(),
551            MethodNameError::InvalidStartChar('1')
552        );
553        assert_eq!(
554            MethodName::new("Call".to_string()).unwrap_err(),
555            MethodNameError::InvalidStartChar('C')
556        );
557        assert_eq!(
558            MethodName::new("do-".to_string()).unwrap_err(),
559            MethodNameError::InvalidChar('-')
560        );
561        assert_eq!(
562            MethodName::new("do_".to_string()).unwrap_err(),
563            MethodNameError::InvalidEndChar('_')
564        );
565    }
566}