ito_common/id/
sub_module_id.rs1use std::fmt;
4
5use super::{IdParseError, ModuleId, is_all_ascii_digits};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub struct SubModuleId(String);
13
14impl SubModuleId {
15 pub(crate) fn new(inner: String) -> Self {
17 Self(inner)
18 }
19
20 pub fn as_str(&self) -> &str {
22 &self.0
23 }
24}
25
26impl fmt::Display for SubModuleId {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 f.write_str(&self.0)
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ParsedSubModuleId {
38 pub sub_module_id: SubModuleId,
40
41 pub parent_module_id: ModuleId,
43
44 pub sub_num: String,
46
47 pub sub_name: Option<String>,
49}
50
51pub fn parse_sub_module_id(input: &str) -> Result<ParsedSubModuleId, IdParseError> {
61 let trimmed = input.trim();
62 if trimmed.is_empty() {
63 return Err(IdParseError::new(
64 "Sub-module ID cannot be empty",
65 Some("Provide a sub-module ID like \"005.01\" or \"005.01_core-api\""),
66 ));
67 }
68
69 if trimmed.len() > 256 {
70 return Err(IdParseError::new(
71 format!(
72 "Sub-module ID is too long: {} bytes (max 256)",
73 trimmed.len()
74 ),
75 Some("Provide a shorter sub-module ID in the form \"NNN.SS\" or \"NNN.SS_name\""),
76 ));
77 }
78
79 let (id_part, name_part) = match trimmed.split_once('_') {
81 Some((left, right)) => (left, Some(right)),
82 None => (trimmed, None),
83 };
84
85 let Some((module_str, sub_str)) = id_part.split_once('.') else {
87 return Err(IdParseError::new(
88 format!("Invalid sub-module ID format: \"{input}\""),
89 Some(
90 "Expected format: \"NNN.SS\" or \"NNN.SS_name\" (e.g., \"005.01\", \"005.01_core-api\")",
91 ),
92 ));
93 };
94
95 if !is_all_ascii_digits(module_str) || !is_all_ascii_digits(sub_str) {
96 return Err(IdParseError::new(
97 format!("Invalid sub-module ID format: \"{input}\""),
98 Some(
99 "Expected format: \"NNN.SS\" or \"NNN.SS_name\" (e.g., \"005.01\", \"005.01_core-api\")",
100 ),
101 ));
102 }
103
104 let module_num: u32 = module_str.parse().map_err(|_| {
105 IdParseError::new(
106 "Sub-module ID is required",
107 Some("Provide a sub-module ID like \"005.01\" or \"005.01_core-api\""),
108 )
109 })?;
110
111 let sub_num: u32 = sub_str.parse().map_err(|_| {
112 IdParseError::new(
113 "Sub-module ID is required",
114 Some("Provide a sub-module ID like \"005.01\" or \"005.01_core-api\""),
115 )
116 })?;
117
118 if module_num > 999 {
119 return Err(IdParseError::new(
120 format!("Module number {module_num} exceeds maximum (999)"),
121 Some("Module numbers must be between 0 and 999"),
122 ));
123 }
124
125 if sub_num > 99 {
126 return Err(IdParseError::new(
127 format!("Sub-module number {sub_num} exceeds maximum (99)"),
128 Some("Sub-module numbers must be between 0 and 99"),
129 ));
130 }
131
132 let sub_name = match name_part {
134 None => None,
135 Some(name) => {
136 if name.is_empty() {
137 return Err(IdParseError::new(
138 format!("Invalid sub-module ID format: \"{input}\""),
139 Some(
140 "Expected format: \"NNN.SS\" or \"NNN.SS_name\" (e.g., \"005.01\", \"005.01_core-api\")",
141 ),
142 ));
143 }
144
145 let mut chars = name.chars();
146 let first = chars.next().unwrap_or('\0');
147 if !first.is_ascii_alphabetic() {
148 return Err(IdParseError::new(
149 format!("Invalid sub-module ID format: \"{input}\""),
150 Some(
151 "Expected format: \"NNN.SS\" or \"NNN.SS_name\" (e.g., \"005.01\", \"005.01_core-api\")",
152 ),
153 ));
154 }
155 for c in chars {
156 if !(c.is_ascii_alphanumeric() || c == '-') {
157 return Err(IdParseError::new(
158 format!("Invalid sub-module ID format: \"{input}\""),
159 Some(
160 "Expected format: \"NNN.SS\" or \"NNN.SS_name\" (e.g., \"005.01\", \"005.01_core-api\")",
161 ),
162 ));
163 }
164 }
165 Some(name.to_ascii_lowercase())
166 }
167 };
168
169 let parent_module_id = ModuleId::new(format!("{module_num:03}"));
170 let sub_num_str = format!("{sub_num:02}");
171 let sub_module_id = SubModuleId::new(format!("{parent_module_id}.{sub_num_str}"));
172
173 Ok(ParsedSubModuleId {
174 sub_module_id,
175 parent_module_id,
176 sub_num: sub_num_str,
177 sub_name,
178 })
179}
180
181#[cfg(test)]
182#[path = "sub_module_id_tests.rs"]
183mod sub_module_id_tests;