Skip to main content

osp_cli/dsl/parse/
quick.rs

1use super::key_spec::{ExactMode, KeySpec};
2
3/// Scope modifiers for quick-search stages.
4///
5/// Bare quick search is "key or value". `K` and `V` are explicit narrowers,
6/// not separate pipeline concepts.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum QuickScope {
9    KeyOrValue,
10    KeyOnly,
11    ValueOnly,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct QuickSpec {
16    pub scope: QuickScope,
17    pub key_spec: KeySpec,
18    pub key_not_equals: bool,
19}
20
21/// Parse the prefix operators used by the quick-search mini-language.
22///
23/// This parser is intentionally order-sensitive and loops until it reaches the
24/// actual token. That keeps odd-but-supported forms like `!?field` readable in
25/// one place instead of scattering prefix handling across the matcher code.
26pub fn parse_quick_spec(input: &str) -> QuickSpec {
27    let mut remaining = input.trim();
28    let mut scope: Option<QuickScope> = None;
29    let mut negated = false;
30    let mut existence = false;
31    let mut exact = ExactMode::None;
32    let mut strict_ambiguous = false;
33
34    loop {
35        if let Some(rest) = remaining.strip_prefix("!=") {
36            negated = true;
37            exact = ExactMode::CaseInsensitive;
38            remaining = rest.trim_start();
39            continue;
40        }
41        if let Some(rest) = remaining.strip_prefix("==") {
42            exact = ExactMode::CaseSensitive;
43            strict_ambiguous = true;
44            remaining = rest.trim_start();
45            continue;
46        }
47        if let Some(rest) = remaining.strip_prefix('!') {
48            negated = !negated;
49            remaining = rest.trim_start();
50            continue;
51        }
52        if let Some(rest) = remaining.strip_prefix('?') {
53            existence = true;
54            remaining = rest.trim_start();
55            continue;
56        }
57        if let Some(rest) = remaining.strip_prefix('=') {
58            exact = ExactMode::CaseInsensitive;
59            remaining = rest.trim_start();
60            continue;
61        }
62        if scope.is_none() {
63            let mut chars = remaining.chars();
64            let Some(first) = chars.next() else {
65                break;
66            };
67            let scope_candidate = match first {
68                'K' | 'k' => Some(QuickScope::KeyOnly),
69                'V' | 'v' => Some(QuickScope::ValueOnly),
70                _ => None,
71            };
72            if let Some(scope_value) = scope_candidate {
73                scope = Some(scope_value);
74                let rest = &remaining[first.len_utf8()..];
75                remaining = rest.trim_start();
76                continue;
77            }
78        }
79        break;
80    }
81
82    let scope = scope.unwrap_or(QuickScope::KeyOrValue);
83    let mut key_not_equals = false;
84    if matches!(scope, QuickScope::KeyOnly) && negated && exact == ExactMode::CaseInsensitive {
85        key_not_equals = true;
86        negated = false;
87    }
88
89    let key_spec = KeySpec {
90        token: remaining.trim().to_string(),
91        negated,
92        existence,
93        exact,
94        strict_ambiguous,
95    };
96
97    QuickSpec {
98        scope,
99        key_spec,
100        key_not_equals,
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::{QuickScope, parse_quick_spec};
107
108    #[test]
109    fn parses_key_scope() {
110        let parsed = parse_quick_spec("K uid");
111        assert_eq!(parsed.scope, QuickScope::KeyOnly);
112        assert_eq!(parsed.key_spec.token, "uid");
113    }
114
115    #[test]
116    fn parses_value_scope() {
117        let parsed = parse_quick_spec("V oistes");
118        assert_eq!(parsed.scope, QuickScope::ValueOnly);
119        assert_eq!(parsed.key_spec.token, "oistes");
120    }
121
122    #[test]
123    fn parses_key_not_equals_form() {
124        let parsed = parse_quick_spec("K !=uid");
125        assert!(parsed.key_not_equals);
126        assert_eq!(parsed.key_spec.token, "uid");
127    }
128}