1use alloc::string::{String, ToString};
26use alloc::vec::Vec;
27use core::fmt;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct SampleSelector {
32 pub filter: Option<FilterExpression>,
34 pub metadata: Vec<MetadataExpression>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum FilterExpression {
41 Comparison {
43 field: String,
45 op: CompareOp,
47 value: Literal,
49 },
50 Boolean {
52 op: BoolOp,
54 lhs: alloc::boxed::Box<FilterExpression>,
56 rhs: alloc::boxed::Box<FilterExpression>,
58 },
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum CompareOp {
64 Eq,
66 NotEq,
68 Lt,
70 Le,
72 Gt,
74 Ge,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum BoolOp {
81 And,
83 Or,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum Literal {
90 Integer(i64),
92 Str(String),
94 Bool(bool),
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum MetadataExpression {
101 SampleState(SampleStateMatch),
103 ViewState(ViewStateMatch),
105 InstanceState(InstanceStateMatch),
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum SampleStateMatch {
112 Read,
114 NotRead,
116 Any,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum ViewStateMatch {
123 New,
125 NotNew,
127 Any,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum InstanceStateMatch {
134 Alive,
136 NotAliveDisposed,
138 NotAliveNoWriters,
140 Any,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum ParseError {
147 Unexpected {
149 pos: usize,
151 found: String,
153 },
154 UnexpectedEof,
156 UnbalancedParen,
158 InvalidNumber(String),
160 UnknownMetadataKey(String),
162 UnknownMetadataValue(String),
164}
165
166impl fmt::Display for ParseError {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 match self {
169 Self::Unexpected { pos, found } => {
170 write!(f, "unexpected token '{found}' at pos {pos}")
171 }
172 Self::UnexpectedEof => f.write_str("unexpected end of input"),
173 Self::UnbalancedParen => f.write_str("unbalanced parenthesis"),
174 Self::InvalidNumber(s) => write!(f, "invalid number literal '{s}'"),
175 Self::UnknownMetadataKey(s) => write!(f, "unknown metadata key '{s}'"),
176 Self::UnknownMetadataValue(s) => write!(f, "unknown metadata value '{s}'"),
177 }
178 }
179}
180
181#[cfg(feature = "std")]
182impl std::error::Error for ParseError {}
183
184pub fn parse_sample_selector(src: &str) -> Result<SampleSelector, ParseError> {
189 let mut p = Parser::new(src);
190 p.skip_whitespace();
191
192 let filter = if p.peek_metadata_key().is_some() {
196 None
197 } else {
198 Some(p.parse_filter_expression()?)
199 };
200
201 let mut metadata = Vec::new();
202 loop {
203 p.skip_whitespace();
204 if p.is_eof() {
205 break;
206 }
207 let _ = p.consume_char(',');
210 p.skip_whitespace();
211 if p.peek_metadata_key().is_none() {
212 break;
213 }
214 metadata.push(p.parse_metadata_expression()?);
215 }
216
217 if !p.is_eof() {
218 return Err(ParseError::Unexpected {
219 pos: p.pos,
220 found: p.peek_token(),
221 });
222 }
223 Ok(SampleSelector { filter, metadata })
224}
225
226struct Parser<'a> {
231 src: &'a [u8],
232 pos: usize,
233}
234
235impl<'a> Parser<'a> {
236 fn new(src: &'a str) -> Self {
237 Self {
238 src: src.as_bytes(),
239 pos: 0,
240 }
241 }
242
243 fn is_eof(&self) -> bool {
244 self.pos >= self.src.len()
245 }
246
247 fn peek(&self) -> Option<u8> {
248 self.src.get(self.pos).copied()
249 }
250
251 fn skip_whitespace(&mut self) -> bool {
252 while let Some(b) = self.peek() {
253 if b.is_ascii_whitespace() {
254 self.pos += 1;
255 } else {
256 break;
257 }
258 }
259 !self.is_eof()
260 }
261
262 fn consume_char(&mut self, c: char) -> bool {
263 if self.peek() == Some(c as u8) {
264 self.pos += 1;
265 true
266 } else {
267 false
268 }
269 }
270
271 fn peek_token(&self) -> String {
272 let mut end = self.pos;
273 while end < self.src.len() && !self.src[end].is_ascii_whitespace() && self.src[end] != b','
274 {
275 end += 1;
276 }
277 if end > self.pos {
278 String::from_utf8_lossy(&self.src[self.pos..end]).into_owned()
279 } else {
280 "<eof>".to_string()
281 }
282 }
283
284 fn parse_filter_expression(&mut self) -> Result<FilterExpression, ParseError> {
285 let mut lhs = self.parse_term()?;
286 loop {
287 self.skip_whitespace();
288 let saved = self.pos;
289 let op = if self.consume_keyword("AND") {
290 BoolOp::And
291 } else if self.consume_keyword("OR") {
292 BoolOp::Or
293 } else {
294 self.pos = saved;
295 break;
296 };
297 self.skip_whitespace();
298 let rhs = self.parse_term()?;
299 lhs = FilterExpression::Boolean {
300 op,
301 lhs: alloc::boxed::Box::new(lhs),
302 rhs: alloc::boxed::Box::new(rhs),
303 };
304 }
305 Ok(lhs)
306 }
307
308 fn parse_term(&mut self) -> Result<FilterExpression, ParseError> {
309 self.skip_whitespace();
310 if self.consume_char('(') {
311 let inner = self.parse_filter_expression()?;
312 self.skip_whitespace();
313 if !self.consume_char(')') {
314 return Err(ParseError::UnbalancedParen);
315 }
316 return Ok(inner);
317 }
318 let field = self.parse_identifier_path()?;
320 self.skip_whitespace();
321 let op = self.parse_compare_op()?;
322 self.skip_whitespace();
323 let value = self.parse_literal()?;
324 Ok(FilterExpression::Comparison { field, op, value })
325 }
326
327 fn parse_identifier_path(&mut self) -> Result<String, ParseError> {
328 self.skip_whitespace();
329 let start = self.pos;
330 while let Some(b) = self.peek() {
331 if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' {
332 self.pos += 1;
333 } else {
334 break;
335 }
336 }
337 if self.pos == start {
338 return Err(ParseError::Unexpected {
339 pos: self.pos,
340 found: self.peek_token(),
341 });
342 }
343 Ok(String::from_utf8_lossy(&self.src[start..self.pos]).into_owned())
344 }
345
346 fn parse_compare_op(&mut self) -> Result<CompareOp, ParseError> {
347 let op = match (self.peek(), self.src.get(self.pos + 1).copied()) {
348 (Some(b'='), _) => {
349 self.pos += 1;
350 CompareOp::Eq
351 }
352 (Some(b'!'), Some(b'=')) => {
353 self.pos += 2;
354 CompareOp::NotEq
355 }
356 (Some(b'<'), Some(b'=')) => {
357 self.pos += 2;
358 CompareOp::Le
359 }
360 (Some(b'<'), _) => {
361 self.pos += 1;
362 CompareOp::Lt
363 }
364 (Some(b'>'), Some(b'=')) => {
365 self.pos += 2;
366 CompareOp::Ge
367 }
368 (Some(b'>'), _) => {
369 self.pos += 1;
370 CompareOp::Gt
371 }
372 _ => {
373 return Err(ParseError::Unexpected {
374 pos: self.pos,
375 found: self.peek_token(),
376 });
377 }
378 };
379 Ok(op)
380 }
381
382 fn parse_literal(&mut self) -> Result<Literal, ParseError> {
383 match self.peek() {
384 Some(b'\'') | Some(b'"') => self.parse_string_literal(),
385 Some(b'-') | Some(b'0'..=b'9') => self.parse_number_literal(),
386 Some(b) if b.is_ascii_alphabetic() => {
387 let saved = self.pos;
389 if self.consume_keyword("true") {
390 return Ok(Literal::Bool(true));
391 }
392 if self.consume_keyword("false") {
393 return Ok(Literal::Bool(false));
394 }
395 self.pos = saved;
396 Err(ParseError::Unexpected {
397 pos: self.pos,
398 found: self.peek_token(),
399 })
400 }
401 _ => Err(ParseError::Unexpected {
402 pos: self.pos,
403 found: self.peek_token(),
404 }),
405 }
406 }
407
408 fn parse_string_literal(&mut self) -> Result<Literal, ParseError> {
409 let quote = self.peek().ok_or(ParseError::UnexpectedEof)?;
410 self.pos += 1;
411 let start = self.pos;
412 while let Some(b) = self.peek() {
413 if b == quote {
414 let s = String::from_utf8_lossy(&self.src[start..self.pos]).into_owned();
415 self.pos += 1;
416 return Ok(Literal::Str(s));
417 }
418 self.pos += 1;
419 }
420 Err(ParseError::UnexpectedEof)
421 }
422
423 fn parse_number_literal(&mut self) -> Result<Literal, ParseError> {
424 let start = self.pos;
425 if self.peek() == Some(b'-') {
426 self.pos += 1;
427 }
428 while let Some(b) = self.peek() {
429 if b.is_ascii_digit() {
430 self.pos += 1;
431 } else {
432 break;
433 }
434 }
435 let raw = String::from_utf8_lossy(&self.src[start..self.pos]).into_owned();
436 raw.parse::<i64>()
437 .map(Literal::Integer)
438 .map_err(|_| ParseError::InvalidNumber(raw))
439 }
440
441 fn consume_keyword(&mut self, kw: &str) -> bool {
442 let bytes = kw.as_bytes();
443 if self.pos + bytes.len() > self.src.len() {
444 return false;
445 }
446 for (i, b) in bytes.iter().enumerate() {
447 let actual = self.src[self.pos + i];
448 let matches = if kw.chars().all(|c| c.is_ascii_uppercase()) {
450 actual.eq_ignore_ascii_case(b)
451 } else {
452 actual == *b
453 };
454 if !matches {
455 return false;
456 }
457 }
458 if let Some(after) = self.src.get(self.pos + bytes.len()) {
460 if after.is_ascii_alphanumeric() || *after == b'_' {
461 return false;
462 }
463 }
464 self.pos += bytes.len();
465 true
466 }
467
468 fn peek_metadata_key(&self) -> Option<&'static str> {
469 for key in ["sample_state", "view_state", "instance_state"] {
470 let bytes = key.as_bytes();
471 if self.pos + bytes.len() <= self.src.len()
472 && &self.src[self.pos..self.pos + bytes.len()] == bytes
473 {
474 return Some(key);
475 }
476 }
477 None
478 }
479
480 fn parse_metadata_expression(&mut self) -> Result<MetadataExpression, ParseError> {
481 let key = self
482 .peek_metadata_key()
483 .ok_or_else(|| ParseError::UnknownMetadataKey(self.peek_token()))?;
484 self.pos += key.len();
485 self.skip_whitespace();
486 if !self.consume_char('=') {
487 return Err(ParseError::Unexpected {
488 pos: self.pos,
489 found: self.peek_token(),
490 });
491 }
492 self.skip_whitespace();
493 let val = self.parse_identifier_path()?;
494 match key {
495 "sample_state" => match val.as_str() {
496 "read" => Ok(MetadataExpression::SampleState(SampleStateMatch::Read)),
497 "not_read" => Ok(MetadataExpression::SampleState(SampleStateMatch::NotRead)),
498 "any" => Ok(MetadataExpression::SampleState(SampleStateMatch::Any)),
499 _ => Err(ParseError::UnknownMetadataValue(val)),
500 },
501 "view_state" => match val.as_str() {
502 "new" => Ok(MetadataExpression::ViewState(ViewStateMatch::New)),
503 "not_new" => Ok(MetadataExpression::ViewState(ViewStateMatch::NotNew)),
504 "any" => Ok(MetadataExpression::ViewState(ViewStateMatch::Any)),
505 _ => Err(ParseError::UnknownMetadataValue(val)),
506 },
507 "instance_state" => match val.as_str() {
508 "alive" => Ok(MetadataExpression::InstanceState(InstanceStateMatch::Alive)),
509 "not_alive_disposed" => Ok(MetadataExpression::InstanceState(
510 InstanceStateMatch::NotAliveDisposed,
511 )),
512 "not_alive_no_writers" => Ok(MetadataExpression::InstanceState(
513 InstanceStateMatch::NotAliveNoWriters,
514 )),
515 "any" => Ok(MetadataExpression::InstanceState(InstanceStateMatch::Any)),
516 _ => Err(ParseError::UnknownMetadataValue(val)),
517 },
518 _ => Err(ParseError::UnknownMetadataKey(key.to_string())),
519 }
520 }
521}
522
523#[cfg(test)]
524#[allow(
525 clippy::expect_used,
526 clippy::unwrap_used,
527 clippy::unreachable,
528 clippy::panic
529)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn parses_simple_equality_filter() {
535 let s = parse_sample_selector("speed = 42").expect("parse");
536 assert!(s.filter.is_some());
537 assert!(s.metadata.is_empty());
538 if let FilterExpression::Comparison { field, op, value } = s.filter.unwrap() {
539 assert_eq!(field, "speed");
540 assert_eq!(op, CompareOp::Eq);
541 assert_eq!(value, Literal::Integer(42));
542 } else {
543 unreachable!();
544 }
545 }
546
547 #[test]
548 fn parses_inequality_with_string_literal() {
549 let s = parse_sample_selector("name != 'sensor'").expect("parse");
550 if let FilterExpression::Comparison { field, op, value } = s.filter.unwrap() {
551 assert_eq!(field, "name");
552 assert_eq!(op, CompareOp::NotEq);
553 assert_eq!(value, Literal::Str("sensor".to_string()));
554 } else {
555 unreachable!();
556 }
557 }
558
559 #[test]
560 fn parses_dotted_field_path() {
561 let s = parse_sample_selector("position.x > 0").expect("parse");
562 if let FilterExpression::Comparison { field, .. } = s.filter.unwrap() {
563 assert_eq!(field, "position.x");
564 } else {
565 unreachable!();
566 }
567 }
568
569 #[test]
570 fn parses_and_conjunction() {
571 let s = parse_sample_selector("a > 1 AND b < 10").expect("parse");
572 if let FilterExpression::Boolean { op, .. } = s.filter.unwrap() {
573 assert_eq!(op, BoolOp::And);
574 } else {
575 unreachable!();
576 }
577 }
578
579 #[test]
580 fn parses_or_with_parenthesis() {
581 let s = parse_sample_selector("(a = 1) OR (b = 2)").expect("parse");
582 if let FilterExpression::Boolean { op, .. } = s.filter.unwrap() {
583 assert_eq!(op, BoolOp::Or);
584 } else {
585 unreachable!();
586 }
587 }
588
589 #[test]
590 fn parses_metadata_only_expression() {
591 let s = parse_sample_selector("sample_state=read").expect("parse");
592 assert!(s.filter.is_none());
593 assert_eq!(s.metadata.len(), 1);
594 assert_eq!(
595 s.metadata[0],
596 MetadataExpression::SampleState(SampleStateMatch::Read)
597 );
598 }
599
600 #[test]
601 fn parses_filter_plus_metadata() {
602 let s = parse_sample_selector("speed > 5, view_state=new").expect("parse");
603 assert!(s.filter.is_some());
604 assert_eq!(
605 s.metadata,
606 alloc::vec![MetadataExpression::ViewState(ViewStateMatch::New)]
607 );
608 }
609
610 #[test]
611 fn parses_all_three_metadata_kinds() {
612 let s = parse_sample_selector("sample_state=any, view_state=any, instance_state=alive")
613 .expect("parse");
614 assert_eq!(s.metadata.len(), 3);
615 assert!(matches!(
616 s.metadata[2],
617 MetadataExpression::InstanceState(InstanceStateMatch::Alive)
618 ));
619 }
620
621 #[test]
622 fn rejects_unknown_metadata_key() {
623 let err = parse_sample_selector("xyz_state=read").expect_err("error");
624 assert!(matches!(
628 err,
629 ParseError::UnknownMetadataKey(_)
630 | ParseError::Unexpected { .. }
631 | ParseError::UnknownMetadataValue(_)
632 ));
633 }
634
635 #[test]
636 fn rejects_unknown_metadata_value() {
637 let err = parse_sample_selector("sample_state=xyz").expect_err("error");
638 assert!(matches!(err, ParseError::UnknownMetadataValue(_)));
639 }
640
641 #[test]
642 fn rejects_unbalanced_parenthesis() {
643 let err = parse_sample_selector("(a = 1").expect_err("error");
644 assert!(matches!(err, ParseError::UnbalancedParen));
645 }
646
647 #[test]
648 fn rejects_trailing_garbage() {
649 let err = parse_sample_selector("a = 1 garbage").expect_err("error");
650 assert!(matches!(err, ParseError::Unexpected { .. }));
651 }
652
653 #[test]
654 fn comparison_operators_full_coverage() {
655 for (src, expected) in [
656 ("a = 1", CompareOp::Eq),
657 ("a != 1", CompareOp::NotEq),
658 ("a < 1", CompareOp::Lt),
659 ("a <= 1", CompareOp::Le),
660 ("a > 1", CompareOp::Gt),
661 ("a >= 1", CompareOp::Ge),
662 ] {
663 let s = parse_sample_selector(src).expect("parse");
664 if let FilterExpression::Comparison { op, .. } = s.filter.unwrap() {
665 assert_eq!(op, expected, "for src={src}");
666 } else {
667 unreachable!("expected Comparison for src={src}");
668 }
669 }
670 }
671
672 #[test]
673 fn boolean_literal_supported() {
674 let s = parse_sample_selector("active = true").expect("parse");
675 if let FilterExpression::Comparison { value, .. } = s.filter.unwrap() {
676 assert_eq!(value, Literal::Bool(true));
677 } else {
678 unreachable!();
679 }
680 }
681}