ito_common/id/
module_id.rs1use std::fmt;
2
3use super::IdParseError;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct ModuleId(String);
10
11impl ModuleId {
12 pub(crate) fn new(inner: String) -> Self {
13 Self(inner)
14 }
15
16 pub fn as_str(&self) -> &str {
18 &self.0
19 }
20}
21
22impl fmt::Display for ModuleId {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 f.write_str(&self.0)
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ParsedModuleId {
31 pub module_id: ModuleId,
33
34 pub module_name: Option<String>,
36}
37
38pub fn parse_module_id(input: &str) -> Result<ParsedModuleId, IdParseError> {
43 let trimmed = input.trim();
44 if trimmed.is_empty() {
45 return Err(IdParseError::new(
46 "Module ID cannot be empty",
47 Some("Provide a module ID like \"1\", \"001\", or \"001_my-module\""),
48 ));
49 }
50
51 let (num_part, name_part) = match trimmed.split_once('_') {
53 Some((left, right)) => (left, Some(right)),
54 None => (trimmed, None),
55 };
56
57 if num_part.is_empty() || !num_part.as_bytes().iter().all(|b| b.is_ascii_digit()) {
58 return Err(IdParseError::new(
59 format!("Invalid module ID format: \"{input}\""),
60 Some(
61 "Expected format: \"NNN\" or \"NNN_name\" (e.g., \"1\", \"001\", \"001_my-module\")",
62 ),
63 ));
64 }
65
66 let num: u32 = num_part.parse().map_err(|_| {
67 IdParseError::new(
68 "Module ID is required",
69 Some("Provide a module ID like \"1\", \"001\", or \"001_my-module\""),
70 )
71 })?;
72
73 if num > 999 {
74 return Err(IdParseError::new(
75 format!("Module ID {num} exceeds maximum (999)"),
76 Some("Module IDs must be between 0 and 999"),
77 ));
78 }
79
80 let module_id = ModuleId::new(format!("{num:03}"));
81
82 let module_name = if let Some(name) = name_part {
83 if name.is_empty() {
84 return Err(IdParseError::new(
85 format!("Invalid module ID format: \"{input}\""),
86 Some(
87 "Expected format: \"NNN\" or \"NNN_name\" (e.g., \"1\", \"001\", \"001_my-module\")",
88 ),
89 ));
90 }
91
92 let mut chars = name.chars();
93 let first = chars.next().unwrap_or('\0');
94 if !first.is_ascii_alphabetic() {
95 return Err(IdParseError::new(
96 format!("Invalid module ID format: \"{input}\""),
97 Some(
98 "Expected format: \"NNN\" or \"NNN_name\" (e.g., \"1\", \"001\", \"001_my-module\")",
99 ),
100 ));
101 }
102 for c in chars {
103 if !(c.is_ascii_alphanumeric() || c == '-') {
104 return Err(IdParseError::new(
105 format!("Invalid module ID format: \"{input}\""),
106 Some(
107 "Expected format: \"NNN\" or \"NNN_name\" (e.g., \"1\", \"001\", \"001_my-module\")",
108 ),
109 ));
110 }
111 }
112 Some(name.to_ascii_lowercase())
113 } else {
114 None
115 };
116
117 Ok(ParsedModuleId {
118 module_id,
119 module_name,
120 })
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn parse_module_id_pads_and_lowercases_name() {
129 let parsed = parse_module_id("1_Foo-Bar").unwrap();
130 assert_eq!(parsed.module_id.as_str(), "001");
131 assert_eq!(parsed.module_name.as_deref(), Some("foo-bar"));
132 }
133
134 #[test]
135 fn parse_module_id_rejects_overflow() {
136 let err = parse_module_id("1000").unwrap_err();
137 assert_eq!(err.error, "Module ID 1000 exceeds maximum (999)");
138 }
139}