1use std::fmt;
4
5use super::IdParseError;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct SpecId(String);
10
11impl SpecId {
12 pub fn as_str(&self) -> &str {
14 &self.0
15 }
16}
17
18impl fmt::Display for SpecId {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 f.write_str(&self.0)
21 }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ParsedSpecId {
27 pub spec_id: SpecId,
29}
30
31pub fn parse_spec_id(input: &str) -> Result<ParsedSpecId, IdParseError> {
36 let trimmed = input.trim();
37 if trimmed.is_empty() {
38 return Err(IdParseError::new(
39 "Spec ID cannot be empty",
40 Some("Provide a spec ID like \"cli-init\""),
41 ));
42 }
43
44 if trimmed.len() > 256 {
45 return Err(IdParseError::new(
46 format!("Spec ID is too long: {} bytes (max 256)", trimmed.len()),
47 Some("Provide a shorter spec ID"),
48 ));
49 }
50
51 if trimmed.contains('/') || trimmed.contains('\\') || trimmed.contains("..") {
52 return Err(IdParseError::new(
53 format!("Invalid spec ID format: \"{input}\""),
54 Some("Spec IDs must be a single path segment and cannot contain traversal tokens"),
55 ));
56 }
57
58 Ok(ParsedSpecId {
61 spec_id: SpecId(trimmed.to_string()),
62 })
63}
64
65#[cfg(test)]
66#[path = "spec_id_tests.rs"]
67mod spec_id_tests;