1use crate::error::Error;
7use crate::limits::MAX_SPEC_NAME_LENGTH;
8
9pub fn parse_spec_set_id(s: &str) -> Result<String, Error> {
16 let s = s.trim();
17 if s.is_empty() {
18 return Err(Error::request(
19 "Spec set identifier cannot be empty",
20 Some("Use a spec set name"),
21 ));
22 }
23
24 if s.contains('~') {
25 return Err(Error::request(
26 "Spec set identifier cannot contain '~'",
27 Some("Use a plain spec name"),
28 ));
29 }
30
31 if s.contains('^') {
32 return Err(Error::request(
33 "Spec set identifier cannot contain '^'",
34 Some("Use a plain spec name"),
35 ));
36 }
37
38 if s.split_whitespace().count() != 1 {
39 return Err(Error::request(
40 "Spec set identifier must be a single spec name",
41 Some("Specs run from the workspace main base; do not include an extra repository qualifier"),
42 ));
43 }
44
45 if s.len() > MAX_SPEC_NAME_LENGTH {
46 return Err(Error::request(
47 format!(
48 "Spec name exceeds maximum length ({} characters)",
49 MAX_SPEC_NAME_LENGTH
50 ),
51 Some("Shorten the spec name"),
52 ));
53 }
54
55 Ok(s.to_string())
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn parse_name_only() {
64 assert_eq!(parse_spec_set_id("pricing").unwrap(), "pricing".to_string());
65 assert_eq!(
66 parse_spec_set_id(" pricing ").unwrap(),
67 "pricing".to_string()
68 );
69 }
70
71 #[test]
72 fn repository_qualifier_rejected() {
73 assert!(parse_spec_set_id("nl/tax pricing").is_err());
74 assert!(parse_spec_set_id("@lemma/std pricing").is_err());
75 }
76
77 #[test]
78 fn tilde_rejected() {
79 assert!(parse_spec_set_id("pricing~a1b2c3d4").is_err());
80 }
81
82 #[test]
83 fn caret_rejected() {
84 assert!(parse_spec_set_id("pricing^a1b2c3d4").is_err());
85 }
86
87 #[test]
88 fn empty_err() {
89 assert!(parse_spec_set_id("").is_err());
90 assert!(parse_spec_set_id(" ").is_err());
91 }
92}