osp_cli/dsl/stages/
common.rs1use anyhow::{Result, anyhow};
2
3use crate::dsl::parse::lexer::{Span, StageSegment, tokenize_stage};
4
5pub fn parse_terms(spec: &str) -> Vec<String> {
6 spec.split(|ch: char| ch == ',' || ch.is_whitespace())
7 .map(str::trim)
8 .filter(|item| !item.is_empty())
9 .map(ToOwned::to_owned)
10 .collect()
11}
12
13pub fn parse_stage_words(spec: &str) -> Result<Vec<String>> {
14 let trimmed = spec.trim();
15 if trimmed.is_empty() {
16 return Ok(Vec::new());
17 }
18
19 let segment = StageSegment {
20 raw: trimmed.to_string(),
21 span: Span {
22 start: 0,
23 end: trimmed.len(),
24 },
25 };
26 let tokens = tokenize_stage(&segment).map_err(|error| anyhow!(error.to_string()))?;
27 Ok(tokens.into_iter().map(|token| token.text).collect())
28}
29
30pub fn parse_optional_alias_after_key(
31 words: &[String],
32 index: usize,
33 verb: &str,
34) -> Result<(Option<String>, usize)> {
35 let Some(token) = words.get(index) else {
36 return Ok((None, 0));
37 };
38 if token.eq_ignore_ascii_case("AS") {
39 return Err(anyhow!("{verb}: AS must follow a key"));
40 }
41 if index + 2 < words.len() && words[index + 1].eq_ignore_ascii_case("AS") {
42 return Ok((Some(words[index + 2].clone()), 3));
43 }
44 Ok((None, 1))
45}
46
47pub fn parse_alias_after_as(words: &[String], index: usize, verb: &str) -> Result<Option<String>> {
48 let Some(token) = words.get(index) else {
49 return Ok(None);
50 };
51 if !token.eq_ignore_ascii_case("AS") {
52 return Ok(None);
53 }
54 let alias = words
55 .get(index + 1)
56 .ok_or_else(|| anyhow!("{verb}: missing alias after AS"))?;
57 Ok(Some(alias.clone()))
58}
59
60#[cfg(test)]
61mod tests {
62 use super::{
63 parse_alias_after_as, parse_optional_alias_after_key, parse_stage_words, parse_terms,
64 };
65
66 #[test]
67 fn parse_terms_splits_commas_and_whitespace() {
68 assert_eq!(
69 parse_terms(" uid, cn mail,,groups "),
70 vec!["uid", "cn", "mail", "groups"]
71 );
72 }
73
74 #[test]
75 fn parse_stage_words_handles_empty_and_quoted_input() {
76 assert_eq!(
77 parse_stage_words(" ").expect("empty spec should parse"),
78 Vec::<String>::new()
79 );
80 assert_eq!(
81 parse_stage_words("uid \"display name\"").expect("quoted words should parse"),
82 vec!["uid".to_string(), "display name".to_string()]
83 );
84 }
85
86 #[test]
87 fn alias_parsers_cover_valid_and_invalid_as_forms() {
88 let words = vec!["count".to_string(), "AS".to_string(), "total".to_string()];
89 assert_eq!(
90 parse_optional_alias_after_key(&words, 0, "A").expect("alias parse should work"),
91 (Some("total".to_string()), 3)
92 );
93 assert_eq!(
94 parse_alias_after_as(&words, 1, "A").expect("alias parse should work"),
95 Some("total".to_string())
96 );
97 assert_eq!(
98 parse_alias_after_as(&words, 0, "A").expect("non-AS token should return none"),
99 None
100 );
101
102 let err = parse_optional_alias_after_key(&["AS".to_string()], 0, "A")
103 .expect_err("leading AS should fail");
104 assert!(err.to_string().contains("AS must follow a key"));
105
106 let err = parse_alias_after_as(&["AS".to_string()], 0, "A")
107 .expect_err("missing alias should fail");
108 assert!(err.to_string().contains("missing alias after AS"));
109 }
110
111 #[test]
112 fn parse_stage_words_reports_lexer_errors() {
113 let err = parse_stage_words("\"unterminated").expect_err("unterminated quote should fail");
114 assert!(
115 err.to_string().contains("unterminated")
116 || err.to_string().contains("expected closing quote")
117 );
118 }
119
120 #[test]
121 fn optional_alias_parser_returns_none_when_alias_is_absent_or_index_missing() {
122 let words = vec!["count".to_string(), "group".to_string()];
123 assert_eq!(
124 parse_optional_alias_after_key(&words, 0, "A").expect("plain key should parse"),
125 (None, 1)
126 );
127 assert_eq!(
128 parse_optional_alias_after_key(&words, 5, "A").expect("missing index should parse"),
129 (None, 0)
130 );
131 }
132}