confium_attributes/
dsl.rs1use crate::ast::Predicate;
17
18pub const MAX_DSL_DEPTH: usize = 32;
23
24pub fn parse(expr: &str) -> Result<Predicate, ParseError> {
26 let trimmed = expr.trim();
27 parse_expr(trimmed, 0).map(|(p, _)| p)
28}
29
30#[derive(Debug, thiserror::Error)]
32pub enum ParseError {
33 #[error("unexpected end of input at: {0}")]
35 UnexpectedEof(String),
36 #[error("unexpected character '{0}' at position {1}")]
38 UnexpectedChar(char, usize),
39 #[error("unknown function: {0}")]
41 UnknownFunction(String),
42 #[error("argument count mismatch for {0}")]
44 ArgCount(String),
45 #[error("number parse error: {0}")]
47 NumberParse(String),
48 #[error("DSL recursion depth {depth} exceeds max {max}")]
50 DepthExceeded { depth: usize, max: usize },
51}
52
53fn parse_expr(s: &str, depth: usize) -> Result<(Predicate, &str), ParseError> {
54 if depth >= MAX_DSL_DEPTH {
55 return Err(ParseError::DepthExceeded {
56 depth,
57 max: MAX_DSL_DEPTH,
58 });
59 }
60 let s = s.trim();
61 let (name, rest) = parse_ident(s)?;
62 let rest = rest.trim_start();
63 let rest = eat(rest, '(')?;
64 let rest = rest.trim_start();
65
66 match name.as_str() {
67 "min_count" => {
68 let (attr, rest) = parse_string(rest)?;
69 let rest = eat(rest.trim_start(), ',')?;
70 let (count_str, rest) = parse_number(rest.trim_start())?;
71 let (_, rest) = parse_until_close(rest.trim_start())?;
72 let count: usize = count_str
73 .parse()
74 .map_err(|_| ParseError::NumberParse(count_str))?;
75 Ok((
76 Predicate::MinCount {
77 attribute: attr,
78 count,
79 },
80 rest,
81 ))
82 }
83 "min_distinct" => {
84 let (attr, rest) = parse_string(rest)?;
85 let rest = eat(rest.trim_start(), ',')?;
86 let (count_str, rest) = parse_number(rest.trim_start())?;
87 let (_, rest) = parse_until_close(rest.trim_start())?;
88 let count: usize = count_str
89 .parse()
90 .map_err(|_| ParseError::NumberParse(count_str))?;
91 Ok((
92 Predicate::MinDistinct {
93 attribute: attr,
94 count,
95 },
96 rest,
97 ))
98 }
99 "none" => {
100 let (attr, rest) = parse_string(rest)?;
101 let rest = parse_until_close(rest.trim_start())?.1;
102 Ok((Predicate::None { attribute: attr }, rest))
103 }
104 "any" => {
105 let (attr, rest) = parse_string(rest)?;
106 let rest = parse_until_close(rest.trim_start())?.1;
107 Ok((Predicate::Any { attribute: attr }, rest))
108 }
109 "all" => {
110 let (attr, rest) = parse_string(rest)?;
111 let rest = parse_until_close(rest.trim_start())?.1;
112 Ok((Predicate::All { attribute: attr }, rest))
113 }
114 "and" | "or" => {
115 let mut preds = Vec::new();
116 let mut s = rest;
117 loop {
118 let s2 = s.trim_start();
119 if s2.starts_with(')') {
120 break;
121 }
122 let (p, rest) = parse_expr(s2, depth + 1)?;
123 preds.push(p);
124 s = rest.trim_start();
125 if s.starts_with(',') {
126 s = &s[1..];
127 } else if s.starts_with(')') {
128 break;
129 } else {
130 return Err(ParseError::UnexpectedEof(s.into()));
131 }
132 }
133 let rest = eat(s, ')')?;
134 if name == "and" {
135 Ok((Predicate::And(preds), rest))
136 } else {
137 Ok((Predicate::Or(preds), rest))
138 }
139 }
140 "not" => {
141 let (inner, rest) = parse_expr(rest, depth + 1)?;
142 let rest = eat(rest.trim_start(), ')')?;
143 Ok((Predicate::Not(Box::new(inner)), rest))
144 }
145 other => Err(ParseError::UnknownFunction(other.into())),
146 }
147}
148
149fn parse_ident(s: &str) -> Result<(String, &str), ParseError> {
150 let mut chars = s.char_indices();
151 let mut end = 0;
152 for (i, c) in chars.by_ref() {
153 if c.is_alphanumeric() || c == '_' {
154 end = i + c.len_utf8();
155 } else {
156 break;
157 }
158 }
159 if end == 0 {
160 return Err(ParseError::UnexpectedEof(s.into()));
161 }
162 Ok((s[..end].to_string(), &s[end..]))
163}
164
165fn parse_string(s: &str) -> Result<(String, &str), ParseError> {
166 let s = eat(s, '"')?;
167 let end = s
168 .find('"')
169 .ok_or_else(|| ParseError::UnexpectedEof(s.into()))?;
170 Ok((s[..end].to_string(), &s[end + 1..]))
171}
172
173fn parse_number(s: &str) -> Result<(String, &str), ParseError> {
174 let end = s
175 .char_indices()
176 .take_while(|(_, c)| c.is_ascii_digit())
177 .last()
178 .map(|(i, c)| i + c.len_utf8())
179 .unwrap_or(0);
180 if end == 0 {
181 return Err(ParseError::NumberParse(s.into()));
182 }
183 Ok((s[..end].to_string(), &s[end..]))
184}
185
186fn eat(s: &str, ch: char) -> Result<&str, ParseError> {
187 let s = s.trim_start();
188 if s.starts_with(ch) {
189 Ok(&s[ch.len_utf8()..])
190 } else {
191 Err(ParseError::UnexpectedChar(
192 s.chars().next().unwrap_or(' '),
193 0,
194 ))
195 }
196}
197
198fn parse_until_close(s: &str) -> Result<(String, &str), ParseError> {
199 let s = eat(s, ')')?;
200 Ok((String::new(), s))
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn parses_min_count() {
209 let p = parse(r#"min_count("role:director", 5)"#).unwrap();
210 match p {
211 Predicate::MinCount { attribute, count } => {
212 assert_eq!(attribute, "role:director");
213 assert_eq!(count, 5);
214 }
215 _ => panic!("wrong variant"),
216 }
217 }
218
219 #[test]
220 fn parses_any() {
221 let p = parse(r#"any("expertise")"#).unwrap();
222 assert!(matches!(p, Predicate::Any { .. }));
223 }
224}
225
226#[cfg(test)]
227mod depth_tests {
228 use super::*;
229
230 #[test]
231 fn shallow_nesting_parses() {
232 let mut expr = String::from("any(\"x\")");
233 for _ in 0..5 {
234 expr = format!("not({expr})");
235 }
236 parse(&expr).expect("5-level not() should parse");
237 }
238
239 #[test]
240 fn deep_nesting_rejected() {
241 let mut expr = String::from("any(\"x\")");
243 for _ in 0..64 {
244 expr = format!("not({expr})");
245 }
246 let err = parse(&expr).expect_err("64-level not() should be rejected");
247 assert!(matches!(err, ParseError::DepthExceeded { .. }));
248 }
249}