Skip to main content

osp_cli/dsl/stages/
common.rs

1use 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}